3、Spark DataFrame理解和使用之单个DataFrame的变换操作

接上篇博文,继续介绍Spark DataFrame的理解和使用。

对于单个DataFrame常见的变换操作有:

  • 创建一个DataFrame(创建空的DF ,从文件中读取数据创建DF)

  • 增加一行或一列

  • 删除一行或一列

  • 把行变成列,把列变成行

  • 根据某列的值对行进行排序

1、创建 DataFrames(createDataFrame()方法、toDF()方法)

1.1 创建一个空的DataFrame

  (1)自定义schema的方法

#自定义schema,创建一个空的DataFrame
from pyspark.sql.types import StructField, StructType, StringType, IntegerType
myManualSchema = StructType([
			StructField("id", StringType(), True),
			StructField("name", IntegerType(), True)
			])
df = spark.createDataFrame(spark.sparkContext.emptyRDD(), myManualSchema)

(2)直接使用已有的dataframe的schema来创建新的dataframe

#当新建的DataFrame结构与已知的DataFrame结构一样的时候,可以直接调用另一个DF.schema
df2 = spark.createDataFrame(spark.sparkContext.emptyRDD(), df1.schema)

1.2 手动输入几条数据,构造一个简单的测试DataFrame

#python
#手动输入几条数据,构造一个简单的测试DataFrame
sparkStatus = spark.createDataFrame([
                (500, "Vice President"),
                (250, "PMC Member"),
                (100, "Contributor")]).toDF("id", "status")

1.3 读取文件创建DataFrame

(1)自动推断schema

//scala
//1、从JSON文件中读取数据创建DF
val df = spark.read.format("json").load("/data/test.json")

//2、从CSV文件中读取数据创建DataFrame,自动推断字段名schema
val df =  spark.read.format("csv")
.option("header", "true")
.option("mode", "FAILFAST")
.option("inferSchema", "true")
.load("some/path/to/file.csv")

'''
注意:自动推断schema 不好的地方就是推断的类型可能会不符合你的原本需求。
比如有一列日期数据:  '2021-03-31' 
你希望使用string类型来存储,而spark会自动把它推断成日期datetime类型,
然后你select或show下这列数据就会发现,数据变成了 '2021-03-31 00:00:00'
所以,推荐大家自定义schema,而不是使用自动推断
'''

(2)手动定义schema

#python
#3、从CSV文件中读取数据创建DataFrame,手工定义Schema
from pyspark.sql.types import StructField, StructType, StringType, IntegerType
myManualSchema = StructType([
			StructField("id", StringType(), True),
			StructField("name", IntegerType(), True)
			])
df = spark.read.format("csv").schema(myManualSchema).load(hdfs_csv_path)
df.cache()
df.createOrReplaceTempView("a_tempview_name")

1.4 使用createDataFrame作用在Seq类型上将Row对象动态的转成DF

//scala
import org.apache.spark.sql.Row
import org.apache.spark.sql.types._
val mySchema = new StructType(Seq(
    StructField("col9",StringType,true),
    StructField("col8",LongType,false)
))

val myRows = Seq(Row("hello",9L),Row("world",8L))
val myRDD = spark.sparkContext.parallelize(myRows)
val myDf = spark.createDataFrame(myRDD,mySchema)
myDf.show()

1.5 使用toDF作用在Seq类型上动态创建

//scala
val myDF = Seq(("Hello", 1L)).toDF("col1", "col2")

19、DataFrame数据写出到文件

19.1 DataFrame写出数据到HDFS,并输出字段名

#option里可以设置输出选项,比如header是否输出字段名schema。注意,每个分区CSV小文件都会有header
df2.write.format("csv").mode("overwrite").option("sep", ",").option("header",True).save(hdfs_result)

2、查看DF中的列数据(select() 和 selectExpr()方法)

2.1  用select()方法查看DataFrame中的列

前一篇博文中已经介绍过引用列的语法(函数或语法糖)。这几种列的引用方法是可以互换使用的,安心。

//查看需要的列
df.select("col1","col2").show(2)

//使用col/column函数、和语法糖来查看DF的列。它们之间可以互相穿插使用。
//col()/column()引用列可单独使用,也可与DataFrame一起使用df.col(),df.column()
//但语法糖就不能与DataFrame一起使用。
import org.apache.spark.sql.functions.{expr,col,column}
df.select(
    df.col("col1"),
    col("col1"),
    column("col1"),
    'col1,
    $"col1",
    expr("col1")
   ).show(2)

但唯一需要注意的是在select()中,当使用col/column方法或语法糖对列进行引用时,不能混搭string的列名引用,编译会报错。错误示例如下:

//错误的混搭使用
df.select(expr("userid"),"userid").show()
df.select(col("userid"),"userid").show()
df.select(column("userid"),"userid").show()
df.select($"userid","userid").show()
df.select('userid,"userid").show()

2.2 select(expr())函数查看DF中的列

expr表达式是一种非常灵活的对列进行引用 或操作的方式。 注意:expr()接受的参数只有一个。

//使用expr表达式修改列名
df.select(expr("col1 as newCol"),expr("*")).show(2)

//通过alias别名重新回来
df.select(expr("col1 as newCol").alias("col1"))

2.3 selectExpr() 函数查看DF中的列

因为select(expr())查询和操作DF太常用了,spark就有了一个简写的接口函数selectExpr 来方便大家使用。

//selectExpr()
df.selectExpr("userid as user_id","user_name").show()

// 当有需要写复杂的表达式来构建新的DF时,selectExpr()的power就显现出来了
df.selectExpr(
"*",  // 包括原来的全部列
"(user_name = login_name) as TrueUser"  //写一个判断条件作为新列TrueUser
).show()

使用selectExpr()表达式以及聚合函数,对整个DF做聚合计算

//聚合函数count
 df.selectExpr("count(userid)","count(distinct(userid))").show()
//聚合函数sum,avg
df.selectExpr("sum(gvm)","count(1)","count(distinct(userid))").show()

看到这里,对select(),select(expr()),selectExpr()有没有一种莫名的熟悉感呢?是不是与Sql中的select 用法莫名的一致呢?

 

3、将常值转成 Spark理解的类型(lit()函数)

上面讲selectExpr()表达式时,我举了一个示例,比较两列是否相同。工作场景中,我们经常需要根据指定值对行进行筛选。比如userid是否等于"gao" 或者 gvm是否大于等于10K等。我们指定的这个用来做判断条件的string或Int类型的值 是一个value,并不是spark DF中的column , 该如何转成spark类型使得它能与其他列进行比较呢?或者怎么在DF中使用外部输入的常数值呢?

lit()函数就是将一个常量值转成spark可以理解的类型的函数。

import org.apache.spark.sql.functions.{expr,col,column}
//使用lit()函数将常量值生成新列
df.select(expr("*"),lit(1).as("one")).show(2)
//对值进行比较
df.select(expr("*"),lit("gao").as("vipuser")).select(expr("userid == vipuser"),expr("*")).show(5)

4、增加列(withColumn()方法)

spark中给DataFrame增加列更正规的方法是 withColumn(),它接收两个参数,第一个是列名,第二个生成列值的表达式。我们可以用withColumn() 方法和 expr() 表达式组合使用,使生成更复杂、更有意义的列数据。

//withColumn()增加列。接收两个参数,第一个是列名,第二个生成列值的表达式
df.withColumn("vipUser",lit("gao")).show()

//使用表达式expr方法让新增加的列数据具有更复杂的生成规则和含义
df.withColumn("IfvipUser",expr("userid == vipUser"))

5、重命名列(withColumnRenamed()方法)

withColumnRenamed()方法接收两个参数,都是string类型,其中第一个参数是原列名,第二个参数是要修改成的列名。

//重命名列
df.withColumnRenamed("OldColName","NewColName").columns

注: withColum一个投机取巧的用处,就是用来重命名列。(我不建议这么用,更推荐使用其对应的专门方法。尤其在跟别人合作工程的时候,使用专门的方法交接和沟通更有效。不要使用不常规的方法来炫技,增加大家工作的负担。)而且,这个方法是通过新增一列来实现重命名,不晓得在计算和存储上是否会浪费存储空间,还是会在Catalyst优化阶段就做了逻辑和物理计划优化?

df.withColumn("user_name",expr("userName")).columns

6、保留字符和关键字(转义符`)

在列名中可能会遇到保留字符和关键字:空格、破折号。这时候我们需要使用转义符``来对列名进行转义才能正常使用列。

6.1  使用转义符``进行转义

//构造一个含有保留字符(空格和破折号)的列名
val dfWithDashColName = df.withColumn("This is a test-col name", expr("userid"))

//引用列名含有保留字符的列
dfWithDashColName.selectExpr(
    "userid",  //正常的列引用
    "`This is a test-col name`",  //含有保留字符的列引用
    "`This is a test-col name` as `new col name`"  //含有保留字符的列重命名成含有保留字符的列
).show()

问个容易让人混淆和头晕的问题来增强大家对转义符的理解。

上面的两个代码例子中,列名"This is a test-col name"含有保留字符,为什么第一个代码构建新列时,withColumn()方法使用时没有加上转义符号``?  第二个代码例子在第三行引用含有保留字符的列时加上转义符``是好理解的,那么第四行重命名列时,a s后面的新列名 为什么也需要加上转义符? 

如果你能很轻松的回答上来,那么你就已经完全掌握了转义符的使用了。

注:在spark-sql中也是使用转义符``来转移含有保留字符的列名。

//使用spark-Sql需要将DataFrame注册成视图
dfWithDashColName.createOrReplaceTempView("dfTableDash")

//Sql引用含有保留字符的列名
select `This is a test-dash col` ,  `This is a test-col name` as `new col name`
from dfTableDash limit 20

6.2  使用col()、column()方法时,如果列名包含保留字符则不需要转义符转义,直接引用列名。(因为col方法参数是String)

       而使用expr()方法时,如果列名包含保留字符则需要使用转义符转义,不可直接引用列名。(因为expr方法参数是表达式)

//正确的使用方法。 col()不转义,expr()要转义
dfWithDashColName.select(col("This is a test-col name"),expr("`This is a test-col name`")).show()

//错误使用1: expr()中不用转义符转义含有保留字符的列名,会报错
dfWithDashColName.select(expr("This is a test-col name")).show()
//错误使用2:直接使用转义符
dfWithDashColName.select(`This is a test-col name`).show()
dfWithDashColName.select("`This is a test-col name`").show()

7、大小写敏感

默认spark是大小写不敏感的。你可以通过配置把它设置成大小写敏感的。

//使用Sql设置大小写敏感
set spark.sql.caseSensitive true;

8、删除列(drop()方法)

//删除一列
df.drop("col8")
//删除多列,列名用逗号分开就可以
df.drop("col10","col13")

当然你也可以用通过select方法来实现列的删除,但如之前所讲,我仍然建议你使用专门的drop方法来实现。规范又显而易见,对于维护工作来说是最有效率的。

 

9、修改列类型(cast()方法)

//转成Long类型,生成新列,列命名为count2
df.withColumn("count2", col("count").cast("long"))

注意:上面4中讲到withColumn()是增加列的方法,但如果使用cast()修改列类型时,我用原来的列名来命名类型转换后的列,那么会是什么样的结果呢?会增加一列?还是不会?

DataFrame中也遵循列名不重复规则,如果命名成原来的列名,那么列数不会增加,但是该列的数据类型已经改变。事实上是经过cast转换类型的新列替换了原来的列。这里不要混乱了。

//转成Long类型,没有生成新列。 
df.withColumn("count", col("count").cast("long"))

10、过滤行(where() 、filter()方法)

在DataFrame的行过滤上,where()和filter()方法是一样的,接收的参数类型也一样。当然Scala版本中filter()也接收任意一个函数作为过滤条件。 行过滤的逻辑是先创建一个判断条件表达式,根据表达式生成true或false,然后过滤掉使表达式值为false的行。

10.1  判断条件表达式在DataFrame中就是创建一个String表达式或者列操作。

//判断条件为string类型的表达式
df.where("count < 2").show()
df.filter("count < 2").show()

//判断条件为列操作
df.where(col("count") < 2).show()
df.filter(col("count") < 2).show()

10.2  当有多个过滤条件时,把过滤条件按顺序串联起来就好了。spark会在优化执行的过程中帮你编译的,不要自己写在一个where中,因为spark自己内部的解析逻辑可能并不会解成你想要的样子。

//多个过滤条件串联起来,相当于AND
df.where(col("count") < 2)
  .where(col("country") =!= "China")
  .show(2)

10.3  过滤等于、不等于。需要注意的是,scala中等于使用===,不等于使用=!=,可能跟平时的代码习惯不一样。

//过滤 =!=
df.where(col("userid") =!= "gaol").show()
//过滤 ===
df.where(col("userid") === "gaol").show()
df.where(!(col("userid") =!= "gaol")).show()

11、行去重 (distinct()方法)

distinct()方法对DataFrame中的一列、多列进行去重处理。

// 对多列进行去重,最后返回全部去重后的结果作为新列返回
df.select("COL_NAME1", "COL_NAME2").distinct()
// 对一列进行去重
df.select("ORIGIN_COUNTRY_NAME").distinct()

注:在spark-sql中也是使用disctinct()方法对一列、多列进行去重处理。

--对多列进行去重
SELECT COUNT(DISTINCT(COL_NAME1, COL_NAME2)) FROM dfTable
--对一列进行去重
SELECT COUNT(DISTINCT COL_NAME1) FROM dfTable

12、随机采样(sample()方法)

按照有放回或无放回的随机抽样方法,抽取DataFrame中指定百分比的行作为样本,生成新的DataFrame。

val seed = 5
val withReplacement = false
val fraction = 0.5
df.sample(withReplacement, fraction, seed).count()

13、随机拆分(randomSplit()方法)

按照设置的权重将一个DataFrame随机拆分成两个子DataFrame。这常用在机器学习生成训练集、测试集、验证集的时候。同随机采样一样,也需要指定随机的seed,同时权重和加起来要为1,如果不为1,spark会自动规范化为1。

//按照设置的随机数种子,按照0.25 : 0.75的权重将DF拆分
val seed  = 62
val dataFrames = df.randomSplit(Array(0.25, 0.75), seed)

//拆分后的DF查看和引用
dataFrames(0).count() 
dataFrames(1).count() 

14、追加行(union()方法)

spark中DataFrame是不可变的,所以不可能向已有DF中追加行,否则就是改变了DF,而DF是不可变的。那么就只能通过union已有的DF和新的DF来实现需求了。要union两个DF,必须要求它们具有相同的schema,不然会失败。 但注意:union()方法是基于位置的合并,并不是基于schema的。

df.union(df2).show()

15、对行进行排序(sort()、orderBy()方法)

15.1  sort() 和 orderBy() 方法在按值大小进行排序上作用是一样的。它们接收一个或多个列表达式、或列string作为参数。


//使用string类型引用列名,按一列生序排序
df.sort("count").show(5)
//使用列名的string形式引用,按两列排序
df.orderBy("count", "gmv").show(5)
//使用col方法引用列
df.orderBy(col("count"), col("gmv")).show(5)

如果要指明排序方向,需要导入desc,asc类。sort()方法默认是生序。

import org.apache.spark.sql.functions.{desc, asc}
//使用expr表达式引用列, 并按降序排序
df.orderBy(expr("count desc")).show(2)
//使用string引用列,分别按降序、升序排列不同列
df.orderBy(desc("count"), asc("DEST_COUNTRY_NAME")).show(2)

注意:可以使用 asc_nulls_first, desc_nulls_first, asc_nulls_last,  desc_nulls_last来指明,排序时你是希望返回的排序结果中缺失值null在前还是在后。

df.orderBy(desc("count"), asc_nulls_last("DEST_COUNTRY_NAME")).show(2)

15.2 分区内排序( sortWithinPartitions()方法 ) 

全局排序会涉及数据的shuffle,出于优化的目的,有时建议使用 sortWithinPartitions()方法 先在每个分区内排序。尤其当DF是读取多个数据文件生成的时候。

spark.read.format("snappy").load("/data/test/*-test.snappy")
          .sortWithinPartitions("count")

16、限制输出(limit()方法)

限制输出,只留取TopN。

df.limit(5).show()

17、重新分区和合并分区(repartition()和 coalesce()方法)

17.1  重新分区方法repartition()

repartition()方法会触发一个全部数据的重新shuffle,不管有没有必要所有数据都要重新shuffle。重新分区后,数据在集群上的物理分布、分区schema,以及分区数都会发生改变。

当你很确定你会经常查询某列,按照该列或多列重新分区后得到的分区数更多时,才会考虑重新分区。

// 查看分区数
df.rdd.getNumPartitions 
// 按照指定列重新分区
df.repartition(col("vipLevel"))
// 指定想要生成的分区个数(可选)
df.repartition(5, col("vipLevel"))

17.2 合并分区coalesce()

coalesce()方法不会触发一个全局的shuffle。

//合并成2个分区
df.coalesce(2)
//df先按照vipLevel列分成5个分区,再合并成2个分区
df.repartition(5, col("vipLevel")).coalesce(2)

18、向驱动器中收集行(collect()方法)

spark的数据是分布式存储在集群上的,如果你想获取一些数据在本机Local模式上操作,就需要将数据收集到driver驱动器中。目前还没有专门定义的方法,但一般我们可以使用下面几种有效的方法来实现。

collect() 返回DataFrame中的全部数据。

df.collect()

limit()、take()、show() 返回有限条。

// 其他收集数据到driver的方法
val collectDF = df.limit(10) //DataFrame 
collectDF.take(5)  //Array
collectDF.show()   //unit
collectDF.show(5, false)  //unit
collectDF.collect() //Array

toLocalIterator()方法,是一个迭代器,收集每个分区中的数据到driver中,方便于按分区进行迭代计算。

df.toLocalIterator()

注意:将数据收集到驱动器中,尤其是当数据集很大,或者分区数据集很大时,很容易将驱动器搞崩溃掉。而且数据收集到驱动器中去计算就不是分布式的并行计算了,而是one-by-one的串行计算会更慢。所以,除了查看小数据,一般不建议用。

 


Mark

花了一个下午和两个晚上的时间,终于把本文的内容完成了。回看下这些知识点都很简单,但写博文的过程中也让自己重新认识了下spark的运行原理,感觉还是很有收获和必要的。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值