Pandas 是
Python
界最流行的資料統計基礎庫之一。但像我這樣和Java/Scala
打交道的人,還是期望JVM
有類似的解決方案。網上一搜發現生態還是很豐富的:大資料領域的 Spark ,支援 GUI 的資料探勘套件 weka ,主攻機器學習的 smile ,擅長聚合變換的 joinery 等等。但小蟲最終還是選擇了簡約而不簡單的 tablesaw 。緣由還得從那句老話說起: 離開場景談應用都是耍流氓 。
應用場景
資料處理流程
小蟲在工作中使用 Spark
將業務產生的海量使用者行為按模組加工:過濾冗餘/簡單彙總,並匯出至 PostgreSQL
的不同表中,儲存正交化的基礎資料。比如使用者的登陸/購買行為分別記錄到, LoginTable
和 ShoppingTable
。然後在 二次處理
模組中建立不同服務,比如 S1,S2。S1 直接訪問資料庫,S2 的既要訪問資料庫,又要訪問 S1 的統計結果,還要依賴本地的 csv 檔案。
計算需求分類
- 查詢計算。整表查詢,列的統計,變換等。
- 篩選排序。條件查詢,自定義多重排序等。
- 聚合連線。比如
LoginTable
記錄終端型別:android/iOS;ShoppingTable
記錄了使用者購買物品的種類和數量。當要對比不同終端使用者購買行為差異時,就要將兩個表連線並按終端型別聚合。 - 模型驗證。評估決策需要的分析模型多變,要經過反覆調整得到最終結果。關鍵在於 快速迭代 。
特點彙總
緯度 | 特點 | 說明 |
---|---|---|
資料量 | 較小單機可承載 | 原始資料由 spark 彙總 |
格式 | 資料庫,本地 csv,json | json 多來自於 restful 服務 |
計算 | 增刪改查,條件查詢,表連線,聚合,統計 | 內建運算元越多,擴充套件性越高越好 |
互動 | 輸出到指定格式,視覺化,可互動 等 | 網頁渲染,終端,[Jupyter Notebook](https://jupyter.org/) |
整合 | 輕量,以庫而非服務的形式 | 便於嵌入程式和其他邏輯互動 |
為何選擇 tablesaw
很簡單,就因為它完美契合小蟲的應用場景。借用 tablesaw官網 的特性列表:
- 資料匯入:RDBMS,Excel,CSV,Json,HTML 或固定寬度檔案。除了支援本地訪問,還支援通過 http,S3 等遠端訪問。
- 資料匯出:CSV,Json,HTML 或固定寬度檔案。
- 表格操作:類似
Pandas DataFrame
,增刪改查,連線,排序。
以上是基礎功能,小蟲覺得下面幾個點更有意思:
- 基於Plotly 的視覺化框架。擺脫
java
的 UI 系統,更好的和 Web 對接。支援 2D、3D 檢視,圖表型別也很豐富:曲線,散點,箱形統計,蠟燭圖,熱力圖,餅狀圖等。更重要的是:- 互動式圖表。特別適合多種資料集對比,以及三維視角旋轉。
- 圖表匯出為字串形式
Javascript
。方便結合 Web Service 渲染 html。
- 與
smile
對接。tablesaw 可將表格匯出為smile
識別的資料格式,便於利用其強大的機器學習庫。
好了,說了這麼多,直接上乾貨吧。
基本應用
安裝
tablesaw
包含多個庫,小蟲推薦安裝 tablesaw-core
和 tablesaw-jsplot
。前者是基礎庫,後者用於渲染圖表。其它如 tablesaw-html, tablesaw-json, tablesaw-breakerx 主要是對資料格式變化的支援,可按需選擇。其實結合需求寫兩行程式碼就行,輕量又靈活。以 tablesaw-core
為例說明 Jar 包安裝方法:
maven倉庫配置:
<dependency>
<groupId>tech.tablesaw</groupId>
<artifactId>tablesaw-core</artifactId>
<version>0.37.3</version>
</dependency>
複製程式碼
sbt倉庫配置:
libraryDependencies += "tech.tablesaw" % "tablesaw-core" % "0.37.3"
複製程式碼
表格建立
兩種基本方式:
- 從資料來源讀取直接建立
- 建立空表格編碼增加列或行
下面先定義需要處理的 csv 檔案格式。第一列為日期,第二列為姓名,第三列為工時(當日工作時長,單位是小時),第四列為報酬(單位是元)。然後舉三個典型例子來說明匯入的不同方式。
1. CSV 直接匯入
// 讀取csv檔案input.csv 自動推測schema
val tbl = Table.read().csv("input.csv")
// 產看讀入的表格內容
println(tbl.printAll())
// 檢視schema
println(tbl.columnArray().mkString("\n"))
複製程式碼
輸出表格內容為:
date | name | 工時 | 報酬 |
---|---|---|---|
2019-01-08 | tom | 8 | 1000 |
2019-01-09 | jerry | 7 | 500 |
2019-01-10 | 張三 | 8 | 999 |
2019-01-10 | jerry | 8 | 550 |
2019-01-10 | tom | 8 | 1000 |
2019-01-11 | 張三 | 6 | 800 |
2019-01-11 | 李四 | 12 | 1500 |
2019-01-11 | 王五 | 8 | 900 |
2019-01-11 | tom | 6.5 | 800 |
可以發現能夠比較完美的推測,並對中文支援良好。輸出 schema 為:
Date column: date
String column: name
Double column: 工時
Integer column: 報酬
tablesaw
目前支援的資料型別有以下幾種:SHORT, INTEGER, LONG ,FLOAT ,BOOLEAN ,STRING ,DOUBLE ,LOCAL_DATE ,LOCAL_TIME ,LOCAL_DATE_TIME, INSTANT, TEXT, SKIP。絕大部分列和普通資料表型別沒有差異,需要強調的是兩類:
- INSTANT。可以精確到納秒的時間戳,自 Java 8 引入。
- SKIP。指定列忽略不讀入。
2. 指定 schema 從 CSV 匯入
有時自動推測並不會非常精準,比如期望使用 LONG ,但識別為 INTEGER ;或在讀入後追加資料時型別會有變化,比如報酬讀入是整型但隨後動態增加會有浮點資料。這時就需要預先設定 csv 的 schema ,這時可以利用 tablesaw 提供的 CsvReadOptions
實現。比如預先設定報酬為浮點:
import tech.tablesaw.api.ColumnType
import tech.tablesaw.io.csv.CsvReadOptions
// 按序指定csv 各列的資料型別
val colTypes: Array[ColumnType] = Array(ColumnType.LOCAL_DATE, ColumnType.STRING, ColumnType.DOUBLE, ColumnType.DOUBLE)
val csvReadOptions = CsvReadOptions.builder("demo.csv").columnTypes(colTypes)
val tbl = Table.read().usingOptions(csvReadOptions)
// 檢視schema
println(tbl.columnArray().mkString("\n"))
複製程式碼
輸出 schema 為:
Date column: date
String column: name
Double column: 工時
Double column: 報酬
3. 編碼設定 schema 和資料填充
該方法適合各種場景,可以執行時從不同資料來源匯入資料。
基本流程是:
-
建立空表格,同時設定名稱
-
設定 schema:向表格中按序增加指定了
名稱
和資料型別
的列。 -
向表格中按行追加資料。每行中的元素分別新增到指定列中。
將之前的例子做些變化,假設資料來自於網路,序列化到本地記憶體的資料結構為:
// 以case class 的形式定義資料來源轉化到本地的記憶體結構
case class RowData(date: LocalDate, name: String, workTime: Double, salary: Double)
複製程式碼
建立一個函式將獲取的資料集合新增到表格中:
// @param tableName 表格名稱
// @param colNames 表格各列的名稱列表
// @param colTypes 表格各列的資料型別列表
// @param rows 列資料
def createTable(tblName: String, colNames: Seq[String], colTypes: Seq[ColumnType], rows: Seq[RowData]): Table = {
// 建立表格設定名稱
val tbl = Table.create(tblName)
// 建立schema :按序增加列
val colCnt = math.min(colTypes.length, colNames.length)
val cols = (0 until colCnt).map { i =>
colTypes(i).create(colNames(i))
}
tbl.addColumns(cols: _*)
// 新增資料
rows.foreach { row =>
tbl.dateColumn(0).append(row.date)
tbl.stringColumn(1).append(row.name)
tbl.doubleColumn(2).append(row.workTime)
tbl.doubleColumn(3).append(row.salary)
}
tbl
}
複製程式碼
上面的說明了資料新增的完整過程:建立表格,增加列,列中追加元素。基於這三個基本操作基本可以實現所有的建立和形變。
列處理
列操作是表格處理的基礎。前面介紹了列的資料型別,名稱設定和元素追加,下面繼續介紹幾個基礎操作。
1. 遍歷與形變
比如按序輸出 demo 表格中所有記錄的姓名:
// 獲取姓名列,根據列名索引
val nameCol = tbl.stringColumn("name")
// 根據行號遍歷
(0 until nameCol.size()).foreach( i =>
println(nameCol.get(i))
)
// 直接使用column 提供的遍歷介面
nameCol.forEacch(println)
複製程式碼
除了遍歷外,另一種常見應用是將列形變到另外一列:型別不變值變化;型別變化。以工時為例,我們將工時不小於 8 則視為全勤:
// 根據列的索引獲取工時一列
val workTimeCol = tbl.doubleColumn(2)
// 形變1: map,輸出列型別與輸入列保持一致
val fullTimeCol = workTimeCol.map { time =>
// 工時型別是Double,因此需要將形變結果也轉化為 Double,否則編譯失敗
if (time >= 8)
1.0
else
0.0
}
// 形變 2: mapInto,輸入/輸出列的資料型別可以不同,但需提前建立大小相同的目標列
val fullTimeCol = BooleanColumn.create("全勤",
workTimeCol.size()) // 建立記錄全勤標籤的Boolean列
val mapFunc: Double2BooleanFunction =
(workTime: Double) => workTime >= 8.0 // 基於SAM 建立對映函式
workTimeCol.mapInto(mapFunc, fullTimeCol) // 形變
tbl.addColumns(fullTimeCol) // 將列新增到表格中
複製程式碼
輸出結果為:
date | name | 工時 | 報酬 | 全勤 |
---|---|---|---|---|
2019-01-08 | tom | 8 | 1000 | true |
2019-01-09 | jerry | 7 | 500 | false |
2019-01-10 | 張三 | 8 | 999 | true |
2019-01-10 | jerry | 8 | 550 | true |
2019-01-10 | tom | 8 | 1000 | true |
2019-01-11 | 張三 | 6 | 800 | false |
2019-01-11 | 李四 | 12 | 1500 | true |
2019-01-11 | 王五 | 8 | 900 | true |
2019-01-11 | tom | 6.5 | 800 | false |
2. 列運算
tablesaw
提供了豐富的針對列的運算函式,而且針對不同資料型別提供了不同特化介面。建議優先查閱 API 文件,最後考慮寫程式碼。這裡介紹幾個大類:
- 多列交叉運算。比如一列中所有元素和同一資料計算,或者兩列元素按序交叉計算。比如每人的時薪:
// 第三列報酬除以第二列工時得到時薪
tbl.doubleColumn(3).divide(tbl.doubleColumn(2))
複製程式碼
- 單列的統計。均值,標準差,最大 N 個值,最小 N 個值,視窗函式等。
// 第三列報酬的標準差
tbl.doubleColumn(3).workTimeCol.standardDeviation()
複製程式碼
- 排序。數值,時間,字串型別預設支援增序、降序,也支援自定義排序。
3. 過濾
tablesaw
對列的過濾條件定義為 Selection
,不同的條件可以按“與、或、非”組合。每種型別的列均提供 "is" 作為字首的介面直接生成條件。下面舉個例子,找到工作時間在 2019-01-09 - 2019-01-10
之間工時等於 8 且報酬小於 1000 的所有記錄:
// 設定時間的過濾條件
val datePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val dateSel = tbl.dateColumn(0)
.isBetweenIncluding(LocalDate.parse("2019-01-09", datePattern),
LocalDate.parse("2019-01-10", datePattern))
// 設定工時過濾條件
val workTimeSel = tbl.doubleColumn(2).isEqualTo(8.0)
// 設定報酬過濾條件
val salarySel = tbl.doubleColumn(3).isLessThan(1000)
// 綜合各條件過濾表格
tbl.where(dateSel.and(workTimeSel).and(salarySel))
複製程式碼
輸出結果符合預期:
date | name | 工時 | 報酬 | 全勤 |
---|---|---|---|---|
2019-01-10 | 張三 | 8 | 999 | true |
2019-01-10 | jerry | 8 | 550 | true |
表格處理
除了基礎操作可以參考官網說明外,有三種表格的操作特別值得一提:連線,分組聚合,分表。
連線
將有公共列名的兩個表連線起來,基本方式是以公共列為 key,將各表同行其它列資料拼接起來生成新表。根據方式的不同組合有所差異:
- inner. 公共列中的資料取交集,其他過濾。
- outer. 公共列中的資料取並集,缺失的資料設定預設空值。具體又可以分為三類:
- leftOuter. 結果表公共列資料與左側表完全相同,不在其中的過濾,缺失的設定空值。
- rightOuter. 結果表公共列資料與右側表完全相同,不在其中的過濾,缺失的設定空值。
- fullOuter. 結果表公共列資料為兩個表的並集,缺失的設定空值。
舉個例子,增加一個新表 tbl2 記錄每人的工作地點:
name | 地點 |
---|---|
張三 | 總部 |
李四 | 門店 1 |
王五 | 門店 2 |
採用 inner 方式和 demo 表連線:
val tbl3 = tbl.joinOn("name").inner(tbl2)
複製程式碼
tbl3 的內容是:
date | name | 工時 | 報酬 | 全勤 | 地點 |
---|---|---|---|---|---|
2019-01-10 | 張三 | 8 | 999 | true | 總部 |
2019-01-11 | 張三 | 6 | 800 | false | 總部 |
2019-01-11 | 李四 | 12 | 1500 | true | 門店 1 |
2019-01-11 | 王五 | 8 | 900 | true | 門店 2 |
可以發現,按照 name 的交集連線,tom 和 jerry 都被過濾掉了。
分組聚合
類似於 SQL 中的 groupby,介面為: tbl.summarize(col1, col2, col3, aggFunc1, aggFunc2 ...).by(groupCol1, groupCol2)
。其中 by 的參數列示分組列名集合。summarize 的 col1, col2, col3
表示分組後需要被聚合處理的列名集合, aggFunc1, aggFunc2
表示聚合函式,會被用於所有的聚合列。舉個例子計算每人的總報酬:
tbl3.summarize("報酬", sum).by("name")
複製程式碼
name | Sum [報酬] |
---|---|
tom | 2800 |
jerry | 1050 |
張三 | 1799 |
李四 | 1500 |
王五 | 900 |
分表
和分組聚合不同,按列分組後,可能並不需要將同組資料聚合為一個值,而是要儲存下來做更加複雜的操作,這時就需要分表。介面很簡單: tbl.splitOn(col ...)
設定分表的列名集合。比如:
// 按照名稱和地點分表,並將生成的各個子表儲存到 List 中
tbl.splitOn("name", "地點").asTableList()
複製程式碼
視覺化
tablesaw
可以將表格匯出為互動式 html,也支援除錯時直接調研呼叫瀏覽器開啟,並針對不同型別圖表做了個性化封裝。舉個簡單例子,檢視每人報酬的時間變化曲線:
//含義是:將tbl 按照 name 列分組,以 date 列為時間軸,顯示 報酬 的變化曲線
//並將圖表的名稱設定為:薪酬變化曲線
val fig = TimeSeriesPlot.create("薪酬變化曲線", tbl, "date", "報酬", "name")
Plot.show(fig)
複製程式碼
其它型別的圖表還有很多,使用方法大同小異,只需根據官方文件傳入正確引數即可。
小結
小蟲向大家簡單介紹了 tablesaw
的功能和使用方法,從我自己的使用經驗而言,我最喜歡它的的地方在於:
- api 介面的統一,清晰
- 互動式圖表生成簡單,能夠和 web 對接
此外, tablesaw
的開發和維護也如火如荼,期待後續有更多的有趣的功能新增進來。