spark-SaprkSQL:缺失值處理,聚合操作,連接操作,UDF函數,窗口函數

1.缺失值處理

SparkSQL提供了DataFrameNaFunctions缺失值處理框架,使用Dataset的na函數來獲取

(1)如何使用 SparkSQL 處理 null 和 NaN ?

首先要將數據讀取出來, 此次使用的數據集直接存在 NaN, 在指定 Schema 後, 可直接被轉爲 Double.NaN

val schema = StructType(
  List(
    StructField("id", IntegerType),
    StructField("year", IntegerType),
    StructField("month", IntegerType),
    StructField("day", IntegerType),
    StructField("hour", IntegerType),
    StructField("season", IntegerType),
    StructField("pm", DoubleType)
  )
)

val df = spark.read
  .option("header", value = true)
  .schema(schema)
  .csv("dataset/beijingpm_with_nan.csv")

1.丟棄包含 null 和 NaN 的行

  1. 當某行數據所有值都是 null 或者 NaN 的時候丟棄此行
df.na.drop("all").show()
  1. 當某行中特定列所有值都是 null 或者 NaN 的時候丟棄此行
 df.na.drop("all", List("pm", "id")).show()
  1. 當某行數據任意一個字段爲 null 或者 NaN 的時候丟棄此行
df.na.drop().show()//這個和下面的指定any是等價的
df.na.drop("any").show()//any是一行數據只要有一個nan就丟棄
  1. 當某行中特定列任意一個字段爲 null 或者 NaN 的時候丟棄此行
df.na.drop(List("pm", "id")).show()
df.na.drop("any", List("pm", "id")).show()

2.填充包含 null 和 NaN 的列

  1. 填充所有包含 null 和 NaN 的列
df.na.fill(0).show()
  1. 填充特定包含 null 和 NaN 的列
df.na.fill(0, List("pm")).show()
  1. 根據包含 null 和 NaN 的列的不同來填充
import scala.collection.JavaConverters._
df.na.fill(Map[String, Any]("pm" -> 0).asJava).show

(2)如何使用 SparkSQL 處理異常字符串 ?

  1. 讀取數據集, 這次讀取的是最原始的那個 PM 數據集
val df = spark.read
.option("header", value = true)
.csv("dataset/BeijingPM20100101_20151231.csv")
  1. 使用函數直接轉換非法的字符串
df.select('No as "id", 'year, 'month, 'day, 'hour, 'season,
when('PM_Dongsi === "NA", 0) //字段下是NA的值,把NA轉換爲0
.otherwise('PM_Dongsi cast DoubleType)//如果是不是NA值,把正常值轉爲DoubleType類型
.as("pm"))
.show()
  1. 使用 where 直接過濾
df.select('No as "id", 'year, 'month, 'day, 'hour, 'season, 'PM_Dongsi)
.where('PM_Dongsi =!= "NA")
.show()
  1. 使用 DataFrameNaFunctions 替換, 但是這種方式被替換的值和新值必須是同類型
df.select('No as "id", 'year, 'month, 'day, 'hour, 'season, 'PM_Dongsi)
.na.replace("PM_Dongsi", Map("NA" -> "NaN"))
.show()

2.聚合操作

(1)groupBy

groupBy 算子會按照列將 Dataset 分組, 並返回一個 RelationalGroupedDataset 對象, 通過 RelationalGroupedDataset 可以對分組進行聚合

//創建sparksession
private val spark = SparkSession.builder()
  .master("local[6]")
  .appName("aggregation")
  .getOrCreate()

import spark.implicits._
//讀取數據
private val schema = StructType(
  List(
    StructField("id", IntegerType),
    StructField("year", IntegerType),
    StructField("month", IntegerType),
    StructField("day", IntegerType),
    StructField("hour", IntegerType),
    StructField("season", IntegerType),
    StructField("pm", DoubleType)
  )
)

private val pmDF = spark.read
  .schema(schema)
  .option("header", value = true)
  .csv("dataset/pm_without_null.csv")

//使用functions函數來完成聚合
import org.apache.spark.sql.functions._

val groupedDF: RelationalGroupedDataset = pmDF.groupBy('year)
groupedDF.agg(avg('pm) as "pm_avg")//聚合, 可以使用 sql.functions 中的函數來配合進行操作
    .orderBy('pm_avg)
    .show()
 //5. 除了使用 functions 進行聚合, 還可以直接使用 RelationalGroupedDataset 的 API 進行聚合
 groupedDF.avg("pm")
  .orderBy('pm_avg)
  .show()

groupedDF.max("pm")
  .orderBy('pm_avg)
  .show()

(2)使用groupBy實現多維度聚合

一個數據結果集,包含總計,小計(不同維度的聚合操作)

import org.apache.spark.sql.functions._
//不同年,不同來源PM值得總數
val groupPostAndYear = pmFinal.groupBy('source, 'year)
  .agg(sum("pm") as "pm")//聚合, 可以使用 sql.functions 中的函數來配合進行操作
  
//按照不同數據源來統計PM值得總數
val groupPost = pmFinal.groupBy('source)
  .agg(sum("pm") as "pm")//聚合, 可以使用 sql.functions 中的函數來配合進行操作
  .select('source, lit(null) as "year", 'pm)//lit(null) 把null顯示爲一列

groupPostAndYear.union(groupPost) //將兩個不同維度的結果集合並在一起
  .sort('source, 'year asc_nulls_last, 'pm)
  .show()

在這裏插入圖片描述

(2)rollup

rollup 操作符其實就是 groupBy 的一個擴展, rollup 會對傳入的列進行滾動 groupBy, groupBy 的次數爲列數量 + 1, 最後一次是對整個數據集進行聚合
rollup(A,B) ==> group(A,null) + group(A,B) + group(null)

import org.apache.spark.sql.functions._

val sales = Seq(
  ("Beijing", 2016, 100),
  ("Beijing", 2017, 200),
  ("Shanghai", 2015, 50),
  ("Shanghai", 2016, 150),
  ("Guangzhou", 2017, 50)
).toDF("city", "year", "amount")
sales.rollup("city", "year")
  .agg(sum("amount") as "amount")//聚合, 可以使用 sql.functions 中的函數來配合進行操作
  .sort('city.desc_nulls_last, 'year.asc_nulls_last)
  .show()

在這裏插入圖片描述

(3)cube

import org.apache.spark.sql.functions._

pmFinal.cube('source, 'year)
  .agg(sum("pm") as "pm_total")
  .sort('source.asc_nulls_last, 'year.asc_nulls_last)
  .show()

在這裏插入圖片描述

(4)SparkSQL 中支持的 SQL 語句實現 cube 功能

pmFinal.createOrReplaceTempView("pm_final")//表名稱
spark.sql(
  """
    |select source, year, sum(pm)
    |from pm_final
    |group by source, year 
    |grouping sets((source, year), (source), (year), ())//實現cube功能,grouping sets等效於多個group by後數據做union合併操作,使用前必須先用group by拆分一下在用
    |order by source asc nulls last, year asc nulls last
  """.stripMargin)
  .show()

在這裏插入圖片描述

3.連接操作

(1)使用場景

  1. 一種是把兩張表在邏輯上連接起來, 一條語句中同時訪問兩張表
  2. 還有一種方式就是表連接自己, 一條語句也能訪問自己中的多條數據

(2)無類型連接 join

1.join(2,1.連接字段==2.連接字段,連接類型)

(3)示例

在這裏插入圖片描述

val person = Seq((0, "Lucy", 0), (1, "Lily", 0), (2, "Tim", 2), (3, "Danial", 0))
  .toDF("id", "name", "cityId")

val cities = Seq((0, "Beijing"), (1, "Shanghai"), (2, "Guangzhou"))
  .toDF("id", "name")

person.join(cities, person.col("cityId") === cities.col("id"))
  .select(person.col("id"),
    person.col("name"),
    cities.col("name") as "city")
  .show()

在這裏插入圖片描述

(3)連接類型 Join Types

1.交叉連接cross

交叉連接就是笛卡爾積, 就是兩個表中所有的數據兩兩結對
交叉連接是一個非常重的操作, 在生產中, 儘量不要將兩個大數據集交叉連接, 如果一定要交叉連接, 也需要在交叉連接後進行過濾, 優化器會進行優化
SQL 語句

select * from person cross join cities

Dataset 操作

 person.crossJoin(cities)
.where(person.col("cityId") === cities.col("id"))
.show()

在這裏插入圖片描述

2.內連接inner

內連接就是按照條件找到兩個數據集關聯的數據, 並且在生成的結果集中只存在能關聯到的數據
SQL 語句

select * from person inner join cities on person.cityId = cities.id

Dataset 操作

 person.join(right = cities,
joinExprs = person("cityId") === cities("id"),
joinType = "inner")
.show()

在這裏插入圖片描述

3.全外連接outer, full, fullouter

內連接和外連接的最大區別, 就是內連接的結果集中只有可以連接上的數據, 而外連接可以包含沒有連接上的數據, 根據情況的不同, 外連接又可以分爲很多種, 比如所有的沒連接上的數據都放入結果集, 就叫做全外連接
SQL 語句

select * from person full outer join cities on person.cityId = cities.id

Dataset 操作

person.join(right = cities,
joinExprs = person("cityId") === cities("id"),
joinType = "full") // "outer", "full", "full_outer"這三個都可以,都是全外連接
.show()

在這裏插入圖片描述

(4)左外連接

左外連接是全外連接的一個子集, 全外連接中包含左右兩邊數據集沒有連接上的數據, 而左外連接只包含左邊數據集中沒有連接上的數據
在這裏插入圖片描述
SQL 語句

  select * from person left join cities on person.cityId = cities.id

Dataset 操作

person.join(right = cities,
joinExprs = person("cityId") === cities("id"),
joinType = "left")
.show()

(5)右外連接

右外連接和左外連接剛好相反, 左外是包含左側未連接的數據, 和兩個數據集中連接上的數據, 而右外是包含右側未連接的數據, 和兩個數據集中連接上的數據
SQL 語句

  select * from person right join cities on person.cityId = cities.id

Dataset 操作

person.join(right = cities,
joinExprs = person("cityId") === cities("id"),
joinType = "right") // rightouter, right
.show()

在這裏插入圖片描述

4.UDF

(1)UDF使用場景

UDF:User Defined Function,用戶自定義函數。
我們在使用sparkSQL的sql語句對錶進行操作時,想要對有些字段使用自己自定義的函數對指定的數據對象進行操作時,可以使用udf對自定義的函數進行註冊,註冊過後的函數就可以在saprkSQL的sql語句中使用.

(2)UDF用法

1.通過匿名函數註冊UDF

下面的UDF的功能是計算某列的長度,該列的類型爲String

org.apache.spark.sql
spark.udf.register("strLen", (str: String) => str.length())//這種的就屬於匿名函數註冊
spark.sql("select name,strLen(name) as name_len from user").show

在這裏插入圖片描述

2.通過實名函數註冊UDF

實名函數的註冊有點不同,要在後面加 _(注意前面有個空格)
定義一個實名函數,然後對其進行註冊

/**
 * 根據年齡大小返回是否成年 成年:true,未成年:false
*/
def isAdult(age: Int) = { //這種就屬於實名函數
  if (age < 18) {
    false
  } else {
    true
  }
}
org.apache.spark._
spark.udf.register("isAdult", isAdult _)

(3)示例

在這裏插入圖片描述
需求:revenue這一列是int類型,變爲字符串型,例如:我要6000變爲6k這樣的字符串

import org.apache.spark._
val toStrUDF = udf(tostring _) // _表示其餘的參數
//或者這樣
spark.udf.register("toStrUDF",tostring _)//實名函數(有函數名稱的定義def tostring)要加_ 匿名函數(無def 名稱的這類)不加_

source.select('product,'category,toStrUDF('revenue))
	.show()
def tostring(revenue:Int){
	(revenue/1000)+"k"
}

5.窗口函數

(1)窗口函數的使用場景

窗口函數分爲兩個部分:1.窗口定義部分2.函數部分
窗口函數和 GroupBy 最大的區別, 就是 GroupBy 的聚合對每一個組只有一個結果例如:source.groupBy('xxx).agg(sum('yyy))最後我們得到一個x組下y的累加值, 而窗口函數可以對每一條數據都有一個結果
在這裏插入圖片描述
在這裏插入圖片描述

(2)示例


import org.apache.spark.sql.expressions.{Window, WindowSpec}
import org.apache.spark.sql.{Dataset, SaveMode, SparkSession}
import org.junit.Test

class WindowFunction {

  @Test
  def firstSecond(): Unit = {
    val spark = SparkSession.builder()
      .appName("window")
      .master("local[6]")
      .getOrCreate()

    import spark.implicits._

    val data = Seq(
      ("Thin", "Cell phone", 6000),
      ("Normal", "Tablet", 1500),
      ("Mini", "Tablet", 5500),
      ("Ultra thin", "Cell phone", 5000),
      ("Very thin", "Cell phone", 6000),
      ("Big", "Tablet", 2500),
      ("Bendable", "Cell phone", 3000),
      ("Foldable", "Cell phone", 3000),
      ("Pro", "Tablet", 4500),
      ("Pro2", "Tablet", 6500)
    )

    val source = data.toDF("product", "category", "revenue")

    val window: WindowSpec = Window.partitionBy('category)//先進行分組,在降序排序
      .orderBy('revenue.desc)
    import org.apache.spark.sql.functions._
    source.select('product, 'category, 'revenue, dense_rank() over window as "rank")//dense_rank生成
      .where('rank <= 2)
      .show()

  }
}

2.

在這裏插入圖片描述在這裏插入圖片描述

(3)示例2

在這裏插入圖片描述

//1.創建數據集
val spark = SparkSession.builder()
  .appName("window")
  .master("local[6]")
  .getOrCreate()

import spark.implicits._
import org.apache.spark.sql.functions._

val data = Seq(
  ("Thin", "Cell phone", 6000),
  ("Normal", "Tablet", 1500),
  ("Mini", "Tablet", 5500),
  ("Ultra thin", "Cell phone", 5500),
  ("Very thin", "Cell phone", 6000),
  ("Big", "Tablet", 2500),
  ("Bendable", "Cell phone", 3000),
  ("Foldable", "Cell phone", 3000),
  ("Pro", "Tablet", 4500),
  ("Pro2", "Tablet", 6500)
)

val source = data.toDF("product", "category", "revenue")

//創建窗口, 按照 revenue 分組, 並倒敘排列
val windowSpec = Window.partitionBy('category)
  .orderBy('revenue.desc)

//應用窗口
source.select(
  'product, 'category, 'revenue,
  ((max('revenue) over windowSpec) - 'revenue) as 'revenue_difference
).show()

在這裏插入圖片描述

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