PySpark简介 - 第一部分--创建DataFrames和从文件读取数据

200 阅读10分钟

PySpark介绍第1部分--创建DataFrames和从文件读取数据

这是我为介绍PySpark而编写的系列文章中的第一部分。我发现我自己查了很多资料作为参考,所以我把这个放在一起,作为自己的一种小抄,希望它也能帮助别人。

这个博文系列的其他部分可以在这里找到。

这些文章的整个目录在这里。

  • 第1部分 - 创建数据框架和从文件中读取数据
    • 创建一个数据框架
    • 读取CSV文件作为数据框架
    • 将JSON文件作为数据框架读取
    • 将Parquet文件作为数据框架来读取
    • 从SQL数据库中读取数据
  • 第2部分 - 选择、过滤和排序数据
    • 选择列
    • 筛选行
      • 连锁条件
      • 字符串匹配
      • 针对列表的匹配
      • 非 (~)
      • 对空值进行过滤
      • 丢弃重复的数据
    • 对数据进行排序
  • 第3部分 - 添加、更新和删除列
    • 添加列
      • 两列相加
      • 累积总和的expr
      • 其他算术运算符
      • 字符串串联
    • 更新列
      • 重命名列
      • 铸造列
      • 当...否则
      • UDFs
      • 填充空值
    • 删除列
  • 第4部分 - 总结数据
    • 总结统计(描述)
    • 获取列的最大值、最小值、平均值、stdev
    • 获取列的数量级
    • 计算行数和不同的值
  • 第5部分 - 聚合数据
    • 分组
    • 透视
    • 窗口化
    • 时间序列聚合
    • 聚合
    • 窗口化
    • 连接数据框架
    • 使用并联方式连接数据框架

创建一个数据框架

Spark上的数据处理是通过一个被称为DataFrame的数据结构完成的,并且提供了一个与这些数据结构进行交互的python API - PySpark。熟悉pandas和R的人在使用DataFrame的时候会很舒服,尽管有一些不同。例如,与pandas DataFrame不同,你没有可以访问的行和列的索引,以选择特定的行作为系列对象。然而,像pandas DataFrame一样,我们可以对列名进行索引,并且每一列必须是单一的数据类型。

DataFrames是表格结构,处理是在spark集群的工作节点上以并行方式对这些数据结构进行的。

让我们看看如何从我们定义的一些数据中创建一个DataFrame,比方说我们有一些天气数据。

weather_data = [
    {'Town': 'London', 'Temperature': 14, 'Humidity': 0.6, 'Wind': 8, 'Precipitation': 0.0},
    {'Town': 'Orlando', 'Temperature': 26, 'Humidity': 0.65, 'Wind': 10, 'Precipitation': 0.6},
    {'Town': 'Cairo', 'Temperature': 23, 'Humidity': 0.37, 'Wind': 11, 'Precipitation': 0.0},
    {'Town': 'Rio De Janeiro', 'Temperature': 32, 'Humidity': 0.76, 'Wind': 17, 'Precipitation': 0.9},
]

我们可以通过将PySparkRow 对象的列表传递给spark.createDataFrame 函数来创建一个DataFrame,其中每一行的属性都作为关键字参数(kwargs)传递给Row 对象。

让我们来看看。

from pyspark.sql import Row

weather_df = spark.createDataFrame(
    map(lambda x: Row(**x), weather_data)
)

display(weather_df)
温度湿度降水
倫敦140.680.0
奥兰多260.65100.6
开罗230.37110.0
里约热内卢320.76170.9

另外,我们也可以使用pandas作为中介,在这个过程中,我们首先转换为pandas DataFrame,然后使用相同的spark.createDataFrame 函数来创建我们的spark DataFame。

让我们来看看。

import pandas as pd

weather_df = spark.createDataFrame(
    pd.DataFrame(weather_data)
)

display(weather_df)
温度湿度降水
倫敦140.680.0
奥兰多260.65100.6
开罗230.37110.0
里约热内卢320.76170.9

将CSV文件作为DataFrames读取

我们可以通过使用spark.read.csv ,并将文件路径作为一个参数传入来读取CSV文件。如果CSV文件有一个头提供,你还需要传入header=True 标志。

populations = spark.read.csv('/mnt/tmp/city_population.csv', header=True)
display(populations)
人口年份城市
6.91991倫敦
7.22001伦敦
8.22011伦敦
2.52001曼彻斯特
2.72011曼彻斯特

默认情况下,这里的3列都是字符串,分隔符默认选择为逗号,有两种方法可以解决这个问题,我们可以在读取CSV的时候传入一些选项。

population_df = spark.read.options(header=True, inferSchema=True, delimiter=',').csv('/mnt/tmp/city_population.csv')

现在我们的Population 是一个DoubleTypeYear 是一个IntegerTypeCity 是一个StringType

如果我们想自己定义这些,以确保我们的数据类型符合我们的期望,我们可以提供一个模式来做到这一点。

from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DoubleType

population_schema = StructType(fields=[
    StructField('Population', DoubleType()),
    StructField('Year', IntegerType()),
    StructField('City', StringType()),
])

population_df = spark.read.csv('/mnt/tmp/city_population.csv', header=True, schema=population_schema)
display(population_df)
人口年份城市
6.91991倫敦
7.22001伦敦
8.22011伦敦
2.52001曼彻斯特
2.72011曼彻斯特

从同一目录下读取多个CSV文件

我们可以通过提供一个目录名或使用星号的wilcard来提供同一目录下多个CSV的路径。

假设我们在一个数据目录下有两个CSV文件,我们想要读取,一个是2013年的数据,一个是2014年的数据。我在这里使用Databricks,所以我将使用Databricks文件系统工具dbutils.fs ,以列出我的目录。

dbutils.fs.ls('/mnt/tmp/ae')

输出[9]:

FileInfo(path='dbfs:/mnt/tmp/ae/ae\_2013.csv', name='ae\_2013.csv', size=1816),  
  
FileInfo(path='dbfs:/mnt/tmp/ae/2014.csv', name='ae\_2014.csv', size=1816)  

我可以通过提供目录把这两个文件读进去。

ae_attendance = spark.read.csv('/mnt/tmp/ae/*', header=True)
display(ae_attendance.limit(10))
日期总出席率总出席人数 > 4小时
W/E 06/01/2013412,21628,702
2013年1月13日 W/E389,23620,628
2013年1月20日W/E360,73915,279
2013年1月27日 W/E388,03620,031
2013年2月3日W/E423,11424,538
2013年2月10日W/E415,03921,682
2013年2月17日W/E409,58624,150
2013年2月24日西区400,72621,980
2013年3月3日,W/E423,61027,622
2013年3月10日430,76931,483

我们可以看到,我们有2013年的数据,如果我们看最后几行,我们还可以看到我们有2014年的数据。

import pprint
pprint.pprint(ae_attendance.tail(num=10))
Row(Date='W/E 26/10/2014', Total Attendance='427,291', Total Attendence > 4 hours='26,789'),  
 
Row(Date='W/E 02/11/2014', 总人数='417,460', 总人数>4小时='26,212'),  
 
Row(Date='W/E 09/11/2014', Total Attendance='418,413', 总出席人数 > 4小时='27,364'),  
 
Row(Date='W/E 16/11/2014', Total Attendance='429,287', Total Attendence >4小时='30,547'),  
 
Row(Date='W/E 23/11/2014', Total Attendance='430,386', Total Attendence > 4 hours='26,324'),  
 
Row(Date='W/E 30/11/2014', Total Attendance='433,100', Total Attendence > 4 hours='28,007'),  
 
Row(Date='W/E 07/12/2014', 总人数='436,377', 总人数>4小时='35,912'),  
 
Row(Date='W/E 14/12/2014', 总人数='440,447', 总出席人数 > 4小时='44,859'),  
 
Row(Date='W/E 21/12/2014', Total Attendance='446,501', Total Attendence >4小时='49,825'),  
 
Row(Date='W/E 28/12/2014', Total Attendance='403,314', Total Attendence > 4 hours='38,279')  
 

读取JSON文件作为数据框架

JSON文件可以使用spark.read.json ,如果你有你的数据的格式,就可以读取。

[    {'id': 1, 'name': 'Ben'},    {'id': 2, 'name': 'Alex'},]

你还需要提供multiline=True 选项,否则spark将尝试把带有[] 的行也作为一条记录读进去。

staff_details = spark.read.options(multiline=True).json('/mnt/tmp/staff_details.json')

默认情况下,这些列是按字母顺序阅读的(在后面的文章中,我们将研究选择列的问题,它可以用来重新排列列的顺序)。

display(staff_details.limit(10))
年龄姓氏性别id姓氏职业
22安德烈1Jacobs质量控制副总裁
20唐纳德2戴维斯四级地质学家
29菲利普3哈珀销售总监
604亨特金融分析师
31茱迪女性5市场部经理
21尼古拉斯6木头教师
34罗杰7华伦图书管理员
64女性8库克软件测试工程师 IV
57邓尼斯9奥蒂兹内部审计员
44布伦达女性10艾略特助理媒体策划师

如果你有嵌套的JSON结构,或者你想提供你自己的模式,你可以这样做,我有一个专门的博文 -使用PySpark来读取和平坦JSON数据,并强制执行模式

将Parquet文件作为DataFrames读取

Parquet文件是一种列式文件存储格式,在大型数据集上可以大大节省空间,改善扫描和反序列化时间。在处理大数据时,它通常是处理中间数据集的默认选择文件,因为在大数据中经常需要按列查询数据,而不是像CSV中那样按行查询。

使用Parquet文件,你可以选择只读取你需要的列,并且有灵活的压缩选项来满足你的需求。

让我们来看看使用 spark.read.parquet 从 Parquet 文件中读取的情况。

pokemon = spark.read.parquet('/mnt/tmp/pokemon.parquet')
display(pokemon.limit(10))
#名称类型1类型2HP攻击力防御攻击力防御速度寿命传奇
1布巴瑟草类毒药45494965451错误
2伊维萨乌草地毒药6062638080.0601假的
3金牛座草地毒药808283100100.0801假的
3巨型维努沙尔草地毒药80100123122120.0801假的
4魅力四射火灾3952436050.0651假的
5炭疽火灾5864588065.0801假的
6蜥蜴火焰飞翔78847810985.01001假的
6蜥蜴Mega 蜥蜴X7813011113085.01001假的
6蜥蜴Mega 蜥蜴Y火焰飞翔7810478159115.01001假的
7小松鼠4448655064.0431错误

与CSV文件不同,我们不需要指定要在这里推断模式,因为它与parquet文件的元数据一起存储。

从SQL数据库中读取数据

如果你想从SQL数据库中读取数据,你可以使用JDBC连接来实现。

如果你想连接到Azure SQL数据库,你需要安装maven库。"com.microsoft.azure:spark-mssql-connector_<version>"

我经常会把它作为部署管道的一部分,把Databricks CLI作为CD管道的一部分来安装,把它连接到我的Databricks集群,然后用线安装。

databricks libraries install --cluster-id $(DATABRICKS-CLUSTER-ID) --maven-coordinates "com.microsoft.azure:spark-mssql-connector_2.12_3.0:1.0.0-alpha"

我也在Databricks secrets utility中存储我的数据库密钥,但现在让我们看看一个没有secrets utility的例子以保持简单。

JDBC_URL = "jdbc:sqlserver://{}.database.windows.net:{};database={};"
sql_server = 'REDACTED'
sql_port = 1433
sql_db = 'REDACTED'
jdbc_url = JDBC_URL.format(sql_server, sql_port, sql_db)
sql_user = 'REDACTED'
sql_password = 'REDACTED!'
product_df = (
    spark.read
        .format("jdbc")
        .option("url", jdbc_url)
        .option("dbtable", 'dbo.Products')
        .option("user", sql_user)
        .option("password", sql_password)
        .load()
)
display(product_df.limit(5))
身份证名称种类类别子类别标识品牌名称是否激活图片Uri
1地平线4XB3589771965
2红色死亡的救赎2XB36091421121
3守望先锋XB36091521124
4玩家未知的战场》(PlayerUnknown's BattlegroundsXB3611901936
5DOOM EternalXB359948191真实