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)
| 镇 | 温度 | 湿度 | 风 | 降水 |
|---|---|---|---|---|
| 倫敦 | 14 | 0.6 | 8 | 0.0 |
| 奥兰多 | 26 | 0.65 | 10 | 0.6 |
| 开罗 | 23 | 0.37 | 11 | 0.0 |
| 里约热内卢 | 32 | 0.76 | 17 | 0.9 |
另外,我们也可以使用pandas作为中介,在这个过程中,我们首先转换为pandas DataFrame,然后使用相同的spark.createDataFrame 函数来创建我们的spark DataFame。
让我们来看看。
import pandas as pd
weather_df = spark.createDataFrame(
pd.DataFrame(weather_data)
)
display(weather_df)
| 镇 | 温度 | 湿度 | 风 | 降水 |
|---|---|---|---|---|
| 倫敦 | 14 | 0.6 | 8 | 0.0 |
| 奥兰多 | 26 | 0.65 | 10 | 0.6 |
| 开罗 | 23 | 0.37 | 11 | 0.0 |
| 里约热内卢 | 32 | 0.76 | 17 | 0.9 |
将CSV文件作为DataFrames读取
我们可以通过使用spark.read.csv ,并将文件路径作为一个参数传入来读取CSV文件。如果CSV文件有一个头提供,你还需要传入header=True 标志。
populations = spark.read.csv('/mnt/tmp/city_population.csv', header=True)
display(populations)
| 人口 | 年份 | 城市 |
|---|---|---|
| 6.9 | 1991 | 倫敦 |
| 7.2 | 2001 | 伦敦 |
| 8.2 | 2011 | 伦敦 |
| 2.5 | 2001 | 曼彻斯特 |
| 2.7 | 2011 | 曼彻斯特 |
默认情况下,这里的3列都是字符串,分隔符默认选择为逗号,有两种方法可以解决这个问题,我们可以在读取CSV的时候传入一些选项。
population_df = spark.read.options(header=True, inferSchema=True, delimiter=',').csv('/mnt/tmp/city_population.csv')
现在我们的Population 是一个DoubleType ,Year 是一个IntegerType ,City 是一个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.9 | 1991 | 倫敦 |
| 7.2 | 2001 | 伦敦 |
| 8.2 | 2011 | 伦敦 |
| 2.5 | 2001 | 曼彻斯特 |
| 2.7 | 2011 | 曼彻斯特 |
从同一目录下读取多个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/2013 | 412,216 | 28,702 |
| 2013年1月13日 W/E | 389,236 | 20,628 |
| 2013年1月20日W/E | 360,739 | 15,279 |
| 2013年1月27日 W/E | 388,036 | 20,031 |
| 2013年2月3日W/E | 423,114 | 24,538 |
| 2013年2月10日W/E | 415,039 | 21,682 |
| 2013年2月17日W/E | 409,586 | 24,150 |
| 2013年2月24日西区 | 400,726 | 21,980 |
| 2013年3月3日,W/E | 423,610 | 27,622 |
| 2013年3月10日 | 430,769 | 31,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 | 安德烈 | 女 | 1 | Jacobs | 质量控制副总裁 |
| 20 | 唐纳德 | 男 | 2 | 戴维斯 | 四级地质学家 |
| 29 | 菲利普 | 男 | 3 | 哈珀 | 销售总监 |
| 60 | 男 | 男 | 4 | 亨特 | 金融分析师 |
| 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 | 类型2 | HP | 攻击力 | 防御 | 攻击力 | 防御 | 速度 | 寿命 | 传奇 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 布巴瑟 | 草类 | 毒药 | 45 | 49 | 49 | 65 | 空 | 45 | 1 | 错误 |
| 2 | 伊维萨乌 | 草地 | 毒药 | 60 | 62 | 63 | 80 | 80.0 | 60 | 1 | 假的 |
| 3 | 金牛座 | 草地 | 毒药 | 80 | 82 | 83 | 100 | 100.0 | 80 | 1 | 假的 |
| 3 | 巨型维努沙尔 | 草地 | 毒药 | 80 | 100 | 123 | 122 | 120.0 | 80 | 1 | 假的 |
| 4 | 魅力四射 | 火灾 | 空 | 39 | 52 | 43 | 60 | 50.0 | 65 | 1 | 假的 |
| 5 | 炭疽 | 火灾 | 空 | 58 | 64 | 58 | 80 | 65.0 | 80 | 1 | 假的 |
| 6 | 蜥蜴 | 火焰 | 飞翔 | 78 | 84 | 78 | 109 | 85.0 | 100 | 1 | 假的 |
| 6 | 蜥蜴Mega 蜥蜴X | 火 | 龙 | 78 | 130 | 111 | 130 | 85.0 | 100 | 1 | 假的 |
| 6 | 蜥蜴Mega 蜥蜴Y | 火焰 | 飞翔 | 78 | 104 | 78 | 159 | 115.0 | 100 | 1 | 假的 |
| 7 | 小松鼠 | 水 | 空 | 44 | 48 | 65 | 50 | 64.0 | 43 | 1 | 错误 |
与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 | 地平线4 | XB358977 | 1 | 9 | 65 | 真 | |
| 2 | 红色死亡的救赎2 | XB360914 | 2 | 1 | 121 | 真 | |
| 3 | 守望先锋 | XB360915 | 2 | 1 | 124 | 真 | |
| 4 | 玩家未知的战场》(PlayerUnknown's Battlegrounds | XB361190 | 1 | 9 | 36 | 真 | |
| 5 | DOOM Eternal | XB359948 | 1 | 9 | 1 | 真实 |