1. 简介
在本文中,我们将学习如何使用Tablesaw库来处理表格数据。首先,我们将导入一些数据。然后,我们将通过处理数据来获得一些见解。
我们将使用鳄梨价格数据集。简而言之,它包含美国多个市场的鳄梨价格和销售量的历史数据。
2. 在Tablesaw中导入数据
首先,我们需要导入数据。Tablesaw支持各种格式,包括我们的数据集格式CSV。因此,让我们首先从其CSV文件加载数据集:
CsvReadOptions csvReadOptions =
CsvReadOptions.builder(file)
.separator(',')
.header(true)
.dateFormat(formatter)
.build();
table = Table.read().usingOptions(csvReadOptions);
上面,我们通过将File对象传递给builder来创建CsvReadOptions类。然后,我们描述如何通过正确配置Option对象来读取CSV文件。
首先,我们使用separator()方法设置列分隔符。其次,我们将文件的第一行读取为标题。第三,我们提供DateTimeFormatter来正确解析日期和时间。最后,我们使用新创建的CsvReadOptions读取表格数据。
2.1 验证导入的数据
让我们使用structure()方法来检查表的设计,它返回另一个包含列名、索引和数据类型的表:
Structure of avocado.csv
Index | Column Name | Column Type |
------------------------------------------
0 | C0 | INTEGER |
1 | Date | LOCAL_DATE |
2 | AveragePrice | DOUBLE |
3 | Total Volume | DOUBLE |
... | ... | ... |
接下来,让我们使用shape()方法检查它的形状:
assertThat(table.shape()).isEqualTo("avocado.csv: 18249 rows X 14 cols");
此方法返回一个字符串,其中包含文件名,后跟行数和列数。我们的数据集总共包含18249行数据和14列。
3. Tablesaw内部的数据表示
Tablesaw主要处理表格和列,它们构成了数据框的基础。简而言之,表格是一组列,其中每列都有固定的类型。表格中的一行是一组值,每个值都分配给其匹配的列。
Tablesaw支持多种列类型,除了扩展Java中的原始类型外,它还提供文本和时间列。
3.1 文本类型
在Tablesaw中,有两种文本类型:TextColumn和StringColumn。第一种是通用类型,可以保存任何文本。另一方面,StringColumn在存储值之前,会将其编码为类似字典的数据结构。这样可以有效地保存数据,而不是在列内重复值。
例如,在鳄梨数据集中,区域和类型列属于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, "Tuyucheng");
从列中删除数据可能会有问题,尤其是在表格的情况下。因此,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()方法来应用过滤器。第一种方法将返回符合我们指定条件的值或行,而第二种方法则会删除它们。
要指定过滤标准,我们首先需要了解Selection。
6.1 Selection
Selection是一个逻辑位图,换句话说,它是一个包含布尔值的数组,这些布尔值用于掩码列向量上的值。例如,将Selection应用于一列将产生另一列,其中包含已过滤的值-例如,删除给定索引的掩码为0的值。此外,Selection向量的大小将与其原始列的大小相同。
让我们通过获取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提供了许多这样的内置辅助方法,因此,对于简单的任务,我们很少需要自己构建自定义Selection。对于复杂的任务,我们使用.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 summary
year | Mean [AveragePrice] | Max [AveragePrice] | Min [AveragePrice] | Std. Deviation [AveragePrice] |
----------------------------------------------------------------------------------------------------------------
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。或者,我们可以实现自定义AggregateFunction对象,但由于这超出了本文的范围,因此我们将尽量简单。
8. 保存数据
到目前为止,我们一直将数据打印到标准输出。在动态验证结果时,打印到控制台很有用,但我们需要将数据保存到文件中,以便其他人可以重复使用结果。因此,让我们直接在表上使用.write()方法:
summary.write().csv("summary.csv");
上面我们使用了.csv()方法将数据保存为CSV格式。目前,Tablesaw仅支持CSV格式和固定宽度格式,这与.print()方法在控制台上显示的内容类似。此外,我们使用CsvWriterOptions来自定义数据的CSV输出。
9. 总结
在本文中,我们探讨了如何使用Tablesaw库处理表格数据。
首先,我们解释了如何导入数据。然后,我们描述了数据的内部表示及其使用方法。接下来,我们探索了如何修改导入表的结构并创建过滤器以在聚合之前提取必要的数据。最后,我们将其保存到CSV文件中。

Be the first guy leaving a comment!