Spark實現行列轉換pivot和unpivot

背景

做過數據清洗ETL工作的都知道,行列轉換是一個常見的數據整理需求。在不同的編程語言中有不同的實現方法,比如SQL中使用case+group,或者Power BI的M語言中用拖放組件實現。今天正好需要在pyspark中處理一個數據行列轉換,就把這個方法記錄下來。

首先明確一下啥叫行列轉換,因爲這個叫法也不是很統一,有的地方叫轉置,有的地方叫透視,不一而足。我們就以下圖爲例,定義如下:

  • 從左邊這種變成右邊這種,叫透視(pivot)
  • 反之叫逆透視(unpivot)

 

image-20180611160900344

 

 

Spark實現

構造樣本數據

首先我們構造一個以格式保存數據的數據集

from pyspark.sql import SparkSession
spark = SparkSession.builder.appName('JupyterPySpark').enableHiveSupport().getOrCreate()

import pyspark.sql.functions as F

# 原始數據 
df = spark.createDataFrame([('2018-01','項目1',100), ('2018-01','項目2',200), ('2018-01','項目3',300),
                            ('2018-02','項目1',1000), ('2018-02','項目2',2000), ('2018-03','項目x',999)
                           ], ['年月','項目','收入'])
複製代碼

樣本數據如下,我們可以看到,每一個項目在指定月份都只有一行記錄,並且項目是稀疏的。即,不是每個項目都會出現在每一個月份中,如項目2僅出現在2018-01當中。

+-------+---+----+
|  年月| 項目|  收入|
+-------+---+----+
|2018-01|項目1| 100|
|2018-01|項目2| 200|
|2018-01|項目3| 300|
|2018-02|項目1|1000|
|2018-02|項目2|2000|
|2018-03|項目x| 999|
+-------+---+----+
複製代碼

透視Pivot

透視操作簡單直接,邏輯如下

  • 按照不需要轉換的字段分組,本例中是年月
  • 使用pivot函數進行透視,透視過程中可以提供第二個參數來明確指定使用哪些數據項;
  • 彙總數字字段,本例中是收入

代碼如下

df_pivot = df.groupBy('年月')\
                .pivot('項目', ['項目1','項目2','項目3','項目x'])\
                .agg(F.sum('收入'))\
                .fillna(0)
複製代碼

結果如下

+-------+----+----+---+---+
| 年月| 項目1| 項目2|項目3|項目x|
+-------+----+----+---+---+
|2018-03|   0|   0|  0|999|
|2018-02|1000|2000|  0|  0|
|2018-01| 100| 200|300|  0|
+-------+----+----+---+---+
複製代碼

逆透視Unpivot

Spark沒有提供內置函數來實現unpivot操作,不過我們可以使用Spark SQL提供的stack函數來間接實現需求。有幾點需要特別注意:

  • 使用selectExpr在Spark中執行SQL片段;
  • 如果字段名稱有中文,要使用反引號**`** 把字段包起來;

代碼如下

df_pivot.selectExpr("`年月`", 
                    "stack(4, '項目1', `項目1`,'項目2', `項目2`, '項目3', `項目3`, '項目x', `項目x`) as (`項目`,`收入`)")\
            .filter("`收入` > 0 ")\
            .orderBy(["`年月`", "`項目`"])\
            .show()
複製代碼

結果如下

+-------+---+----+
|     年月| 項目|  收入|
+-------+---+----+
|2018-01|項目1| 100|
|2018-01|項目2| 200|
|2018-01|項目3| 300|
|2018-02|項目1|1000|
|2018-02|項目2|2000|
|2018-03|項目x| 999|
+-------+---+----+


作者:wait4friend
鏈接:https://juejin.im/post/5b1e343f518825137c1c6a27
來源:掘金
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

SparkSQL 實現

 

spark從1.6開始引入,到現在2.4版本,pivot算子有了進一步增強,這使得後續無論是交給pandas繼續做處理,還是交給R繼續分析,都簡化了不少。大家無論在使用pandas、numpy或是R的時候,首先會做的就是處理數據,尤其是將列表,轉成成合適的形狀。

列表

在說透視表之前,我們先看看,什麼是列表,在傳統觀念上,列表的每一行代表一條記錄,而每一列代表一個屬性。

+-------+-------+-----+

|   date|project|value|

+-------+-------+-----+

|2018-01|     p1|  100|

|2018-01|     p2|  200|

|2018-01|     p3|  300|

|2018-02|     p1| 1000|

|2018-02|     p2| 2000|

|2018-03|     px|  999|

+-------+-------+-----+

舉個簡單的例子,如上表,一條記錄可能代表某個項目,在某個年月創造的價值。而在這個表裏面,某一列,就代表一個屬性,比如date代表日期,project代表項目名稱。而這裏每一行,代表一條獨立,完整的記錄,一條與另外一條記錄,沒有直接的關係。

這種結構,也是一般關係型數據庫的數據結構。

透視表

透視表沒有一個明確的定義,一般是觀念上是指,爲了方便進行數據分析,而對數據進行一定的重排,方便後續分析,計算等操作。透視表每一個元素及其對應的“座標”一起形成一條完整的記錄。

+-------+------+------+-----+-----+

|   date|    p1|    p2|   p3|   px|

+-------+------+------+-----+-----+

|2018-01| 100.0| 200.0|300.0|  0.0|

|2018-02|1000.0|2000.0|  0.0|  0.0|

|2018-03|   0.0|   0.0|  0.0|999.0|

+-------+------+------+-----+-----+

上面的表,是將列表進行重排後的透視表,其第一行和第一列可以理解成索引,而在表中根據索引可以確定一條唯一的值,他們一起組成一條相當於列表裏的數據。

通過一般的定義,我們能看出,透視表主要用於分析,所以,一般的場景我們都會先對數據進行聚合,以後再對數據分析,這樣也更有意義。就好像,將話費清單,做成透視表,儘管邏輯上沒有任何問題,但是結果是可能比現在的清單列表更難查閱。

PS:一些可以借鑑的名詞,目前維基百科並沒有收錄,也只能權且理解一下吧

建模擬數據

先來模擬個數據吧,按照前面的例子,建個csv,這裏多加了一列s2,是爲了做多透視列的,

date,project,value,s2
2018-01,p1,100,12
2018-01,p2,200,33
2018-01,p3,300,44
2018-02,p1,1000,22
2018-02,p2,2000,41
2018-03,px,999,22


spark API

我們先來看下DEMO程序

SparkConf sparkConf = new SparkConf().setAppName("JavaWordCount").setMaster("local");
SparkContext sc = SparkContext.getOrCreate(sparkConf);
SparkSession ss = new SparkSession(sc);
Dataset<Row> ds = ss.read()
         //csv分隔符 
        .option("sep", ",")
         //是否包含header
        .option("header", "true")
        //加載csv路徑
        .csv("E:\\devlop\\workspace\\sparkdemo\\src\\main\\java\\com\\dafei1288\\spark\\data1.csv");
Dataset<Row>  r = 
        //設置分組
        ds.groupBy(col("date"))
        //設置pivot
        .pivot("project")
        //設置聚合
        .agg(sum("value"));
r.show();


在加載csv的時候,我們設置了分隔符,以及讀取表頭。

對加載後的dataset只需要進行3步設置

groupBy 設置分組列

pivot 設置pivot列

agg 設置聚合方式,可以是求和、平均等聚合函數

我們得到的輸出結果如下:

+-------+------+------+-----+-----+

|   date|    p1|    p2|   p3|   px|

+-------+------+------+-----+-----+

|2018-03|  null|  null| null|999.0|

|2018-02|1000.0|2000.0| null| null|

|2018-01| 100.0| 200.0|300.0| null|

+-------+------+------+-----+-----+

請注意,這裏和sql有些區別,就是groupBy的時候,不需要將project列寫入了,如果寫入成了

groupBy(col("date"),col("project"))
那麼結果就是這樣了

+-------+-------+------+------+-----+-----+

|   date|project|    p1|    p2|   p3|   px|

+-------+-------+------+------+-----+-----+

|2018-01|     p3|  null|  null|300.0| null|

|2018-01|     p2|  null| 200.0| null| null|

|2018-01|     p1| 100.0|  null| null| null|

|2018-03|     px|  null|  null| null|999.0|

|2018-02|     p1|1000.0|  null| null| null|

|2018-02|     p2|  null|2000.0| null| null|

+-------+-------+------+------+-----+-----+

sparkSQL 

SparkConf sparkConf = new SparkConf().setAppName("JavaWordCount").setMaster("local");
SparkContext sc = SparkContext.getOrCreate(sparkConf);
SparkSession ss = new SparkSession(sc);
Dataset<Row> ds = ss.read() .option("sep", ",")
        .option("header", "true").csv("E:\\devlop\\workspace\\sparkdemo\\src\\main\\java\\com\\dafei1288\\spark\\data1.csv");
ds.registerTempTable("f");
Dataset<Row>  r = ds.sqlContext().sql(
"select * from (
    select date,project as p,sum(value) as ss from f group by date,project
   )
  pivot (  
      sum(ss) 
      for p in ( 'p1','p2','p3','px' )  
   ) 
   order by date");
r.na().fill(0).show();


可以看到,這裏我們將讀取的csv註冊成了表f,使用spark sql語句,這裏和oracle的透視語句類似

pivot語法: pivot( 聚合列  for  待轉換列 in (列值) )     

其語法還是比較簡單的。

爲了展示數據好看一點,我特意使用語句

r.na().fill(0)
將空值`null`替換成了0。

+-------+------+------+-----+-----+

|   date|    p1|    p2|   p3|   px|

+-------+------+------+-----+-----+

|2018-01| 100.0| 200.0|300.0|  0.0|

|2018-02|1000.0|2000.0|  0.0|  0.0|

|2018-03|   0.0|   0.0|  0.0|999.0|

+-------+------+------+-----+-----+

多聚合列

上文提到了,多做了一列,就是爲了這個DEMO準備的,使用如下SparkSQL語句,設置多聚合列透視表

select * from (
    select date,project as p,sum(value) as ss,sum(s2) as ss2 from f group by date,project
)
pivot (  
      sum(ss),sum(ss2)  
     for p in ( 'p1','p2','p3','px' ) 

order by date


這裏爲例方便看,我就截圖了

爲了防止OOM的情況,spark對pivot的數據量進行了限制,其可以通過spark.sql.pivotMaxValues 來進行修改,默認值爲10000,這裏是指piovt後的列數。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章