1. 引言
本文将介绍如何使用 Tablesaw 库处理表格数据。首先我们会导入数据,然后通过操作数据获取洞察。
我们将使用 avocado prices 数据集。该数据集包含美国多个市场牛油果价格和销量的历史数据。
2. 在 Tablesaw 中导入数据
首先需要导入数据。Tablesaw 支持多种格式,包括我们数据集使用的 CSV 格式。下面从 CSV 文件加载数据:
CsvReadOptions csvReadOptions =
CsvReadOptions.builder(file)
.separator(',')
.header(true)
.dateFormat(formatter)
.build();
table = Table.read().usingOptions(csvReadOptions);
我们通过构建器创建 CsvReadOptions
对象,并正确配置选项:
- 使用
separator()
设置列分隔符 - 使用
header(true)
将首行作为表头 - 提供
DateTimeFormatter
解析日期时间 - 最后用配置好的选项读取表数据
2.1. 验证导入数据
使用 structure()
方法检查表结构,返回包含列名、索引和数据类型的表:
avocado.csv 的结构
Index | 列名 | 列类型 |
------------------------------------------
0 | C0 | INTEGER |
1 | Date | LOCAL_DATE |
2 | AveragePrice | DOUBLE |
... | ... | ... |
再用 shape()
方法检查表形状:
assertThat(table.shape()).isEqualTo("avocado.csv: 18249 行 X 14 列");
该方法返回包含文件名、行数和列数的字符串。我们的数据集包含 18249 行和 14 列。
3. Tablesaw 内部数据表示
Tablesaw 主要通过表和列工作,它们构成了数据框的基础。简单来说,表是列的集合,每列有固定类型。表中的行是一组值,每个值对应其匹配的列。
Tablesaw 支持多种列类型。除了扩展 Java 基本类型的列,还提供文本和时间列。
3.1. 文本类型
Tablesaw 有两种文本类型:TextColumn
和 StringColumn
:
TextColumn
:通用类型,直接存储原始文本StringColumn
:存储前先用字典结构编码值,避免重复值,提高存储效率
例如在牛油果数据集中,region 和 type 列是 StringColumn
类型。列中的重复值被高效存储,指向同一个文本实例:
StringColumn type = table.stringColumn("type");
List<String> conventional = type.where(type.isEqualTo("conventional")).asList().stream()
.limit(2)
.toList();
assertThat(conventional.get(0)).isSameAs(conventional.get(1));
3.2. 时间类型
Tablesaw 提供四种时间类型,对应 Java 对象:DateColumn
、DateTimeColumn
、TimeColumn
和 InstantColumn
。如前所述,导入时可配置如何解析这些值。
4. 操作列
接下来学习如何操作导入的数据并提取洞察。在 Tablesaw 中,我们可以转换单个列或操作整个表。
4.1. 创建新列
通过调用各列类型的静态 create()
方法创建新列。例如创建名为 time
的 TimeColumn
:
TimeColumn time = TimeColumn.create("Time");
使用 addColumns()
方法将列添加到表:
Table table = Table.create("test");
table.addColumns(time);
assertThat(table.columnNames()).contains("time");
4.2. 添加或修改列数据
使用 append()
方法在列末尾添加数据:
DoubleColumn averagePrice = table.doubleColumn("AveragePrice");
averagePrice.append(1.123);
assertThat(averagePrice.get(averagePrice.size() - 1)).isEqualTo(1.123);
对于表,必须为每列提供值以确保所有列至少有一个值。否则当列大小不同时会抛出 IllegalArgumentException
:
DoubleColumn averagePrice2 = table.doubleColumn("AveragePrice").copy();
averagePrice2.setName("AveragePrice2");
averagePrice2.append(1.123);
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> table.addColumns(averagePrice2));
使用 set()
方法修改列向量中的特定值(需知道要修改值的索引):
stringColumn.set(2, "Baeldung");
从列中删除数据可能有问题,尤其是对表而言。Tablesaw 不允许从列向量中删除值,而是使用 setMissing()
将要删除的值标记为缺失:
DoubleColumn averagePrice = table.doubleColumn("AveragePrice").setMissing(0);
assertThat(averagePrice.get(0)).isNull();
这不会从向量中删除值占位符,而是将其设为 null
,因此向量大小保持不变。
5. 数据排序
接下来对导入的数据排序。首先根据一组列对表行排序。使用 sortAscending()
和 sortDescending()
方法,它们接受列名。让我们按日期排序获取最早和最晚的日期:
Table ascendingDateSortedTable = table.sortAscendingOn("Date");
assertThat(ascendingDateSortedTable.dateColumn("Date").get(0)).isEqualTo(LocalDate.parse("2015-01-04"));
Table descendingDateSortedTable = table.sortDescendingOn("Date");
assertThat(descendingDateSortedTable.dateColumn("Date").get(0)).isEqualTo(LocalDate.parse("2018-03-25"));
但这些方法限制很大,例如无法混合升序和降序排序。为解决这些限制,使用 sortOn()
方法。它默认按列名升序排序。要对特定列降序排序,在列名前加减号“-”。例如按年份升序、平均价格降序排序:
Table ascendingYearAndAveragePriceSortedTable = table.sortOn("year", "-AveragePrice");
assertThat(ascendingYearAndAveragePriceSortedTable.intColumn("year").get(0)).isEqualTo(2015);
assertThat(ascendingYearAndAveragePriceSortedTable.numberColumn("AveragePrice").get(0)).isEqualTo(2.79);
这些方法不适用于所有场景。Tablesaw 允许为 sortOn()
方法提供自定义的 Comparator<VRow>
实现。
6. 数据过滤
过滤器允许我们从原始表中获取数据子集。过滤表会返回另一个表,使用 where()
和 dropWhere()
方法应用过滤器:
where()
:返回符合指定条件的行dropWhere()
:删除符合条件的行
要指定过滤条件,首先需要理解 Selection
。
6.1. 选择器
Selection
是一个逻辑位图,即包含布尔值的数组,用于掩码列向量中的值。例如将选择器应用于列会生成包含过滤值的新列(如删除掩码为 0 的索引对应的值)。选择器向量大小与原始列相同。
通过获取 2017 年平均价格高于 2 美元的数据实践:
DateColumn dateTable = table.dateColumn("Date");
DoubleColumn averagePrice = table.doubleColumn("AveragePrice");
Selection selection = dateTable.isInYear(2017).and(averagePrice.isGreaterThan(2D));
Table table2017 = table.where(selection);
assertThat(table2017.intColumn("year")).containsOnly(2017);
assertThat(table2017.doubleColumn("AveragePrice")).allMatch(avrgPrice -> avrgPrice > 2D);
我们使用了 DateColumn
的 isInYear()
和 DoubleColumn
的 isGreaterThan()
方法,通过 and()
方法组合成类查询语言。Tablesaw 提供许多内置辅助方法,简单任务很少需要自定义选择器。复杂任务可通过 and()
、andNot()
、or()
等列过滤器组合。
或者通过创建 Predicate
并传递给每列的 eval()
方法编写自定义过滤器,该方法返回用于过滤表或列的 Selection
对象。
7. 数据汇总
操作数据后,我们需要从中提取洞察。使用 summarize()
方法聚合数据。例如从牛油果数据集中提取平均价格的最小值、最大值、均值和标准差:
Table summary = table.summarize("AveragePrice", max, min, mean, stdDev).by("year");
System.out.println(summary.print());
首先将要聚合的列名和 AggregateFunction
列表传递给 summarize()
,然后使用 by()
方法按年份分组结果。最后在标准输出打印结果:
avocado.csv 汇总
year | 平均价格均值 | 平均价格最大值 | 平均价格最小值 | 平均价格标准差 |
----------------------------------------------------------------------------------------------------------------
2015 | 1.375590382902939 | 2.79 | 0.49 | 0.37559477067238917 |
2016 | 1.3386396011396013 | 3.25 | 0.51 | 0.39370799476072077 |
2017 | 1.5151275777700104 | 3.17 | 0.44 | 0.4329056466203253 |
2018 | 1.3475308641975308 | 2.3 | 0.56 | 0.3058577391135024 |
Tablesaw 为常见操作提供了聚合函数。也可以实现自定义 AggregateFunction
,但这超出了本文范围。
8. 保存数据
目前我们一直将数据打印到标准输出。控制台打印适合临时验证结果,但需要将数据保存到文件以便他人重用。直接在表上使用 write()
方法:
summary.write().csv("summary.csv");
使用 csv()
方法将数据保存为 CSV 格式。Tablesaw 目前仅支持 CSV 格式和固定宽度格式(与 print()
方法在控制台显示的格式类似)。此外,可使用 CsvWriterOptions
自定义 CSV 输出。
9. 结论
本文探讨了使用 Tablesaw 库处理表格数据的方法。
首先介绍了数据导入,然后描述了数据内部表示及操作方法。接着探讨了修改导入表结构、创建过滤器提取必要数据,最后进行聚合并保存为 CSV 文件。
完整源代码可在 GitHub 获取。