Spark 中DataFrame數據的行轉列

1 需求

在做數據處理時我們可能會經常用到 Apache Spark 的 DataFrame來對數據進行處理,需要將行數據轉成列數據來處理,例如一些指標數據一般會保存在KV類型數據庫,根據幾個字段作爲key,將計算指標作爲value保存起來,這樣多個用戶多個指標就會形成一個窄表,我們在使用這個數據時又希望按照每個用戶來展示,將同一個用戶的多個指標放到一行,這就需要將DataFrame數據進行行列轉換,然後再通過Spark做進一步的處理,將最終的數據保存或提供給調用方。

Spark 中DataFrame數據的行轉列需要用到Spark中的Pivot(透視),簡單來說將用行Row形式的保存的數據轉換爲列Column形式的數據叫做透視;反之叫做逆透視。pivot算子org.apache.spark.sql.RelationalGroupedDataset類中,主要有如下6個重載的方法,查看這個方法源碼的註釋,我們可以看到這個方法是在Spark 1.6.0開始引入的(前4個是1.6.0之後,後2個是從2.4.0之後),而且建議我們最好指定第二個參數(列字段集合),否則效率會很低。

/**
   * Pivots a column of the current `DataFrame` and performs the specified aggregation.
   * There are two versions of pivot function: one that requires the caller to specify the list
   * of distinct values to pivot on, and one that does not. The latter is more concise but less
   * efficient, because Spark needs to first compute the list of distinct values internally.
   *
   * {{{
   *   // Compute the sum of earnings for each year by course with each course as a separate column
   *   df.groupBy("year").pivot("course", Seq("dotNET", "Java")).sum("earnings")
   *
   *   // Or without specifying column values (less efficient)
   *   df.groupBy("year").pivot("course").sum("earnings")
   * }}}
   *
   * @param pivotColumn Name of the column to pivot.
   * @param values List of values that will be translated to columns in the output DataFrame.
   * @since 1.6.0
   */

在這裏插入圖片描述

2 準備數據

例如現在有如下銷售的不同類目的各個季度的銷售額的數據,第一列數據爲商品類目,第二列是季度:第一季度Q1、第二季度Q2、第三季度Q3、第四季度Q4,第三列是銷售額單位爲萬元。

import org.apache.spark.sql.Row
import org.apache.spark.sql.types._

object DF_Data {
  val scc = new SparkConfClass()

  /**
    *  category|  quarter|  sales
    *  種類 | 季度 | 銷售額
    */
  val store_sales = scc.getSc.parallelize(Array(
     "Books|Q4|4.66",
    "Books|Q1|1.58",
    "Books|Q3|2.84",
    "Books|Q2|1.50",
    "Women|Q1|1.41",
    "Women|Q2|1.36",
    "Women|Q3|2.54",
    "Women|Q4|4.16",
    "Music|Q1|1.50",
    "Music|Q2|1.44",
    "Music|Q3|2.66",
    "Music|Q4|4.36",
    "Children|Q1|1.54",
    "Children|Q2|1.46",
    "Children|Q3|2.74",
    "Children|Q4|4.51",
    "Sports|Q1|1.47",
    "Sports|Q2|1.40",
    "Sports|Q3|2.62",
    "Sports|Q4|4.30",
    "Shoes|Q1|1.51",
    "Shoes|Q2|1.48",
    "Shoes|Q3|2.68",
    "Shoes|Q4|4.46",
    "Jewelry|Q1|1.45",
    "Jewelry|Q2|1.39",
    "Jewelry|Q3|2.59",
    "Jewelry|Q4|4.25",
//    "null|Q1|0.04",
    "null|Q2|0.04",
//    "null|Q3|0.07",
    "null|Q4|0.13",
    "Electronics|Q1|1.56",
    "Electronics|Q2|1.49",
    "Electronics|Q3|2.77",
    "Electronics|Q4|4.57",
    "Home|Q1|1.57",
    "Home|Q2|1.51",
    "Home|Q3|2.79",
    "Home|Q4|4.60",
    "Men|Q1|1.60",
    "Men|Q2|1.54",
    "Men|Q3|2.86",
    "Men|Q4|4.71"
  ))
  val schemaStoreSales = StructType(
    "category|quarter".split("\\|")
      .map(column => StructField(column, StringType, true))
  ).add("sales", DoubleType, true)
  val store_salesRDDRows = store_sales.map(_.split("\\|"))
    .map(line => Row(
      line(0).trim,
      line(1).trim,
      line(2).trim.toDouble
    ))
}

上述代碼中SparkConfClass類爲自定義的一個Spark 類,主要將常用的SparkConf、SparkContext、SparkContext、以及關閉操作封裝到一個類。

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.sql.SQLContext

class SparkConfClass() extends Serializable {
  @transient
  private val conf = new SparkConf().setAppName("pivot_demo").setMaster("local[4]")
    .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
  @transient
  private val sc: SparkContext = new SparkContext(conf)
  sc.setLogLevel("ERROR")
  @transient
  private val sqlContext: SQLContext = new SQLContext(sc)

  def getSc: SparkContext = {
    sc
  }

  def getSqlContext: SQLContext = {
    sqlContext
  }

  def closeSc(): Unit = {
    sc.stop()
  }
}

3 使用 Pivot 行轉列

object PivotDemo {
  def main(args: Array[String]): Unit = {
    val store_salesFrame = DF_Data.scc.getSqlContext.createDataFrame(DF_Data.store_salesRDDRows, DF_Data.schemaStoreSales)
    store_salesFrame.show(20, false)

	//使用Spark中的函數,例如 round、sum 等
    import org.apache.spark.sql.functions._
    store_salesFrame.groupBy("category")
      .pivot("quarter")
      .agg(round(sum("sales"), 2))
      .show(false)
  }
}

4 初次處理的結果

我們的數據轉成DataFrame後如下

+--------+-------+-----+
|category|quarter|sales|
+--------+-------+-----+
|   Books|     Q4| 4.66|
|   Books|     Q1| 1.58|
|   Books|     Q3| 2.84|
|   Books|     Q2|  1.5|
|   Women|     Q1| 1.41|
|   Women|     Q2| 1.36|
|   Women|     Q3| 2.54|
|   Women|     Q4| 4.16|
|   Music|     Q1|  1.5|
|   Music|     Q2| 1.44|
|   Music|     Q3| 2.66|
|   Music|     Q4| 4.36|
|Children|     Q1| 1.54|
|Children|     Q2| 1.46|
|Children|     Q3| 2.74|
|Children|     Q4| 4.51|
|  Sports|     Q1| 1.47|
|  Sports|     Q2|  1.4|
|  Sports|     Q3| 2.62|
|  Sports|     Q4|  4.3|
+--------+-------+-----+
only showing top 20 rows

按照類目,將每個季度轉成列,如下,可以看到原始數據中categorynull的行缺少第一和第三季度的值,但是經過pivot轉換後,沒有的列對應的值爲null,這裏需要注意,否則做統計時null值處理後可能還是null值

+-----------+----+----+----+----+
|category   |Q1  |Q2  |Q3  |Q4  |
+-----------+----+----+----+----+
|Home       |1.57|1.51|2.79|4.6 |
|Sports     |1.47|1.4 |2.62|4.3 |
|Electronics|1.56|1.49|2.77|4.57|
|Books      |1.58|1.5 |2.84|4.66|
|Men        |1.6 |1.54|2.86|4.71|
|Music      |1.5 |1.44|2.66|4.36|
|Women      |1.41|1.36|2.54|4.16|
|Shoes      |1.51|1.48|2.68|4.46|
|Jewelry    |1.45|1.39|2.59|4.25|
|Children   |1.54|1.46|2.74|4.51|
|null       |null|0.04|null|0.13|
+-----------+----+----+----+----+

5 下一步

通過上一步已經將行數據轉換爲列數據,轉換後的數據也是一個sql.DataFrame,那麼我們就可將其註冊爲臨時視圖(這裏叫 TempView ),如果是全局的,查詢的時候記得在表名前加上global_temp

註冊成臨時視圖後,我們就可以像操作表數據一樣用SQL操作這個數據了,例如現在需要返回,每個商品類目的每個季度的銷售額、總銷售額,精確到小數點兩位。

	import org.apache.spark.sql.functions._
    store_salesFrame.groupBy("category")
       // 指定行轉列的各個字段集合,如果知道具體的字段,最好指定上
      .pivot("quarter", Seq("Q1", "Q2", "Q3", "Q4"))
      // 對於同一category的數據,如果quarter值相同時就對其求和,並保留兩位小數
      .agg(round(sum("sales"), 2))
        .createOrReplaceGlobalTempView("category")
//        .createTempView("category")

    DF_Data.scc.getSqlContext
      .sql(
        """
          |SELECT category, Q1, Q2, Q3, Q4, ROUND(NVL(Q1, 0.0) + NVL(Q2, 0.0) + NVL(Q3, 0.0) + NVL(Q4, 0.0), 2) AS total
          |FROM global_temp.category
        """.stripMargin)
      .show(false)

存在null值時我們需要調用NVL處理下,結果如下

+-----------+----+----+----+----+-----+
|category   |Q1  |Q2  |Q3  |Q4  |total|
+-----------+----+----+----+----+-----+
|Home       |1.57|1.51|2.79|4.6 |10.47|
|Sports     |1.47|1.4 |2.62|4.3 |9.79 |
|Electronics|1.56|1.49|2.77|4.57|10.39|
|Books      |1.58|1.5 |2.84|4.66|10.58|
|Men        |1.6 |1.54|2.86|4.71|10.71|
|Music      |1.5 |1.44|2.66|4.36|9.96 |
|Women      |1.41|1.36|2.54|4.16|9.47 |
|Shoes      |1.51|1.48|2.68|4.46|10.13|
|Jewelry    |1.45|1.39|2.59|4.25|9.68 |
|Children   |1.54|1.46|2.74|4.51|10.25|
|null       |null|0.04|null|0.13|0.17 |
+-----------+----+----+----+----+-----+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章