通過實例學習 PySpark
最近學習了一下 PySpark, 目標是在工作中能將其用上. 在實踐過程中發現, 通過一個個具體的問題來進行學習, 很多內容掌握起來更爲容易. 因此後面如果寫相關的文章, 也會採用實例的方式來介紹.
下面要解決的問題是:
假設用戶購買商品, 其點擊時間記錄在點擊表中, 其下單時間記錄在下單表中, 另外還有一張表記錄用戶的特徵. 現在的目標是, 獲取每個用戶從點擊時間到下單時間的時間間隔, 並和特徵進行拼接. 比如用戶 A:
用戶 點擊時間 下單時間 時間間隔 特徵
user1 2020-05-13 2020-05-14 24*3600s sex:male age:28
瞭解問題的目標之後, 首先是獲取原始數據.
原始數據獲取
## data.py
click_time = [
['user1', '2020-05-13 10:46:43'],
['user2', '2020-05-22 08:26:42'],
['user3', '2020-05-17 02:42:31'],
['user4', '2020-05-23 18:25:23'],
['user5', '2020-05-19 13:29:05'],
['user6', '2020-05-16 19:48:23'],
['user7', '2020-05-20 16:56:13'],
]
order_time = [
['user3', '2020-05-18 10:46:43'],
['user1', '2020-05-22 08:26:42'],
['user5', '2020-05-27 02:42:31'],
['user7', '2020-05-23 18:25:23'],
['user4', '2020-05-29 13:29:05'],
['user2', '2020-05-26 19:48:23'],
['user6', '2020-05-20 16:56:13'],
]
features = [
['age:26', 'weight:70', 'sex:male', 'id:user2'],
['weight:50', 'age:22', 'sex:female', 'id:user1'],
['weight:70', 'sex:male'],
['age:16', 'weight:63', 'sex:male', 'id:user7'],
['age:22', 'sex:male'],
['age:33', 'weight:72', 'sex:female', 'id:user5'],
['weight:63', 'age:46', 'sex:female', 'id:user4'],
['age:45', 'weight:73'],
]
with open('click_time.txt', 'w') as f:
content = '\n'.join(['\t'.join(item) for item in click_time])
f.write('{}\n'.format(content))
with open('order_time.txt', 'w') as f:
content = '\n'.join(['\t'.join(item) for item in order_time])
f.write('{}\n'.format(content))
with open('features.txt', 'w') as f:
content = '\n'.join(['\t'.join(item) for item in features])
f.write('{}\n'.format(content))
SparkSession
下一步, 創建 SparkSession.
from pyspark.sql import SparkSession, Row
spark = SparkSession.builder \
.appName('test') \
.master('local') \
.enableHiveSupport() \
.getOrCreate()
加載數據
數據文件生成後, 現在使用 PySpark 讀入文件生成 DataFrame. 目前我發現有三種讀入文件的方法.
加載點擊時間表
- 使用 HiveQL 語句讀入文件:
click_table = 'click_table'
click_file = 'click_time.txt'
spark.sql("""
create table if not exists `{}` (
id STRING,
click_time STRING
)
using hive options (fileFormat 'textfile', fieldDelim '\t')
""".format(click_table))
spark.sql("""
load data local inpath '{click_file}' into table `{click_table}`
""".format(click_file=click_file, click_table=click_table))
click_df = spark.sql("select * from `{}`".format(click_table))
其中 using hive options (fileFormat 'textfile', fieldDelim '\t')
也可以用 row format delimited fields terminated by '\t'
替換.
- 使用
spark.read
讀入文件:
click_file = 'click_time.txt'
df = spark.read.text(click_file).toDF('info')
- 使用
sc.textFile
讀入文件:
click_file = 'click_time.txt'
sc = spark.sparkContext
df = sc.textFile(click_file).map(lambda x: Row(info=x)).toDF()
需要注意的是, 使用第 2 以及第 3 種方法, 爲了獲得 id
以及 click_time
兩個 field, 還需要額外的處理:
def time_split(row):
id, time = row.split('\t')
return (id, time)
click_df = spark.createDataFrame(
df.rdd.map(lambda x: time_split(x[0])),
['id', 'click_time']
)
注意 df
是 DataFrame
對象, 使用 .rdd
轉換爲 RDD 對象, 之後使用 .map
方法處理 RDD
中的每個 Row
對象, 在 How to get a value from the Row object in Spark Dataframe?
中談到, Row
繼承於 namedtuple
, 因此代碼中的 x[0]
(通過索引訪問) 含義是取出 Row 中的值, 當然, 可以使用 x.info
(通過 field 訪問) 獲取 Row 中的值.
生成 click_df
後, 可以顯示部分數據看看是否符合預期:
click_df.limit(3).show()
## 或者
click_df.show(3)
效果如下:
注意: 後面爲了數據處理的一致性, 我一律採用第二種方法來讀入數據.
加載下單時間表
基本和加載點擊時間表邏輯相同.
order_file = 'order_time.txt'
df = spark.read.text(order_file).toDF('info')
order_df = spark.createDataFrame(
df.rdd.map(lambda x: time_split(x[0])),
['id', 'order_time']
)
加載 user 特徵表
注意對特徵表的過濾, 因爲有些記錄存在內容缺失, 比如找不到 id
或者 sex
.
def feature_split(row):
row = row.split('\t')
feature_dict = {item.split(':')[0]: item.split(':')[1] for item in row}
return feature_dict
df = spark.read.text('features.txt').toDF('info')
feature_df = spark.createDataFrame(
df.rdd.map(lambda x: feature_split(x[0])),
['id', 'sex', 'age', 'weight']
)
feature_df = feature_df.filter((feature_df.id.isNotNull()) & \
(feature_df.sex.isNotNull())
)
schema
注意在加載 user 特徵表時, 對於 sex
, age
, weight
之類的特徵, 沒有指定它們的類型, 可能默認就是字符串類型了. 爲了顯式指定對應的類型, 需要自定義 schema, 參考: pyspark: ValueError: Some of types cannot be determined after inferring. (我原來遇到過一個錯誤: ValueError: Some of types cannot be determined by the first 100 rows, please try again with sampling
也可以通過自定義 Schema 解決).
代碼如下:
在 StructField
中的第三個參數含義爲: Boolean nullable
, 即是否可以被設置爲 null
. (具體參見: spark.sql.types.StructField)
之所以下面的 schema 中全部設置爲 StringType
, 是因爲我在 feature_split
函數中將結果均表示成字符串的形式, 比如 age
中的結果不是 int 而是字符串. 如果希望設置 age
爲 IntegerType
, 那麼應該修改 feature_split
中的代碼, 將 age
對應的結果用 int()
方法做轉換.
from pyspark.sql.types import (StructType,
StructField,
StringType,
IntegerType,
DoubleType,
)
df = spark.read.text('features.txt').toDF('info')
schema = StructType([StructField("id", StringType(), True),
StructField("sex", StringType(), True),
StructField("age", StringType(), True),
StructField("weight", StringType(), True),
])
feature_df = spark.createDataFrame(
df.rdd.map(lambda x: feature_split(x[0])),
schema=schema
)
feature_df = feature_df.filter((feature_df.id.isNotNull()) & \
(feature_df.sex.isNotNull()) & \
(feature_df.age.isNotNull()) & \
(feature_df.weight.isNotNull())
)
獲取下單和點擊的時間間隔
爲了獲取下單和點擊的時間間隔, 需要將點擊時間表和下單時間表進行 Left Outer Join, 以獲取每個 user 對應的點擊時間以及下單時間. 然而, 由於 user 可能只進行點擊而未下單, 因此要對結果過濾.
join_df = click_df.join(order_df, click_df.id == order_df.id, how='left') \
.select(click_df.id, click_df.click_time, order_df.order_time) \
.filter(order_df.order_time.isNotNull())
上面代碼獲取了每個 user 對應的點擊時間以及下單時間, 爲了獲得時間間隔, 需要額外的函數進行處理:
from datetime import datetime
def convert2datetime(s, format='%Y-%m-%d %H:%M:%S'):
return datetime.strptime(s, format)
def convert2str(s, format='%Y-%m-%d %H:%M:%S'):
return datetime.strftime(s, format)
def convert(row):
id, click_time, order_time = row
click_time, order_time = list(map(convert2datetime, [click_time, order_time]))
diff = (order_time - click_time).total_seconds()
return (id, diff)
join_df = spark.createDataFrame(join_df.rdd.map(convert), ['id', 'diff'])
Join 特徵表
最後只需要 Join 特徵表就能達到我們最終的目的:
final_df = join_df.join(feature_df, join_df.id == feature_df.id, how='inner') \
.select(join_df.id, feature_df.sex, feature_df.age, feature_df.weight)
可以考慮使用 .show()
輸出結果看看是否符合預期. 此外, 如果想將結果保存在目錄中, 可以使用如下方式完成:
def create(row):
row = map(str, row)
line = '\t'.join(row)
return line
output_dir = 'output'
final_df.rdd.map(create).repartition(2).saveAsTextFile(output_dir)
另外注意, 如果 output_dir
已經存在, 需要提前刪除, 否則程序會報錯.
觀察 output
的文件:
可以發現結果保存在兩個分區中, 比如 part-00001
中保存着:
終曲 & 尾聲
不要忘記
spark.stop()
完整代碼
以上完整代碼如下, 運行起來, 去感受 Spark 的強大 😂😂😂
(發表完博客後補充: 文章在草稿中保存了幾天, 發出來後發現, 結果好像跟一開始設置的目標不太一樣啊 🤣🤣🤣 忘了把時間間隔加到結果中了, 不過這無傷大雅~ 果然寫博客還是得一氣呵成! )
from datetime import datetime
from pyspark.sql import SparkSession, Row
from pyspark.sql.types import (StructType,
StructField,
StringType,
IntegerType,
DoubleType,
)
def time_split(row):
id, time = row.split('\t')
return (id, time)
def feature_split(row):
row = row.split('\t')
feature_dict = {item.split(':')[0]: item.split(':')[1] for item in row}
return feature_dict
def convert2datetime(s, format='%Y-%m-%d %H:%M:%S'):
return datetime.strptime(s, format)
def convert2str(s, format='%Y-%m-%d %H:%M:%S'):
return datetime.strftime(s, format)
def convert(row):
id, click_time, order_time = row
click_time, order_time = list(map(convert2datetime, [click_time, order_time]))
diff = (order_time - click_time).total_seconds()
return (id, diff)
def create(row):
row = map(str, row)
line = '\t'.join(row)
return line
spark = SparkSession.builder \
.appName('test') \
.master('local') \
.enableHiveSupport() \
.getOrCreate()
click_file = 'click_time.txt'
df = spark.read.text(click_file).toDF('info')
click_df = spark.createDataFrame(
df.rdd.map(lambda x: time_split(x[0])),
['id', 'click_time']
)
click_df.show(3)
order_file = 'order_time.txt'
df = spark.read.text(order_file).toDF('info')
order_df = spark.createDataFrame(
df.rdd.map(lambda x: time_split(x[0])),
['id', 'order_time']
)
df = spark.read.text('features.txt').toDF('info')
schema = StructType([StructField("id", StringType(), True),
StructField("sex", StringType(), True),
StructField("age", StringType(), True),
StructField("weight", StringType(), True),
])
feature_df = spark.createDataFrame(
df.rdd.map(lambda x: feature_split(x[0])),
schema=schema
)
feature_df = feature_df.filter((feature_df.id.isNotNull()) & \
(feature_df.sex.isNotNull()) & \
(feature_df.age.isNotNull()) & \
(feature_df.weight.isNotNull())
)
join_df = click_df.join(order_df, click_df.id == order_df.id, how='left') \
.select(click_df.id, click_df.click_time, order_df.order_time) \
.filter(order_df.order_time.isNotNull())
join_df = spark.createDataFrame(join_df.rdd.map(convert), ['id', 'diff'])
feature_df.show(3)
final_df = join_df.join(feature_df, join_df.id == feature_df.id, how='inner') \
.select(join_df.id, feature_df.sex, feature_df.age, feature_df.weight)
output_dir = 'output'
final_df.rdd.map(create).repartition(2).saveAsTextFile(output_dir)
spark.stop()