以CSDN为场景,基于用户观看文章行为数据,利用pyspark分别对用户和文章进行标签画像。
主要涉及pyspark以下应用:
- 基础的数据清洗
- 统计指标计算
- 聚合分布
- 自定义udf
- 表关联
涉及数据:
- 用户基础信息表
- 文章基础信息表
- 用户登录ip流水表
- 用户行为表
输出画像:
-
用户画像数据
- 观看文章次数
- 观看文章id list
- 观看文章类型分布
- 最爱看的文章类型
- 用户最近登录ip和登录时间
-
文章画像数据
- 多少用户观看
- 用户年龄段分布
- 用户城市分布
- 用户观看总时长
数据示例
用户基础信息: [uid, 年龄,性别]
df_user.show()
+---+---+------+
|uid|age|gender|
+---+---+------+
| 1| 10| 0|
| 2| 20| 1|
| 3| 21| 1|
| 4| 22| 0|
| 5| 23| 1|
| 6| 10| 0|
| 7| 30| 1|
+---+---+------+
文章基础信息:[tid, title, type]
df_title.show(5
+---+-----------------+----+
|tid| title|type|
+---+-----------------+----+
| 1| 老人与海| 0|
| 2|pyspark入门到精通| 1|
| 3| python入门| 2|
| 4| 机器学习| 3|
| 5| 深度学习| 3|
+---+-----------------+----+
用户登录ip流水表:[uid, login_ip, login_time]
df_login_ip.show(5)
+---+-----------+--------------+
|uid| login_ip| login_time|
+---+-----------+--------------+
| 1|10.11.11.11|20220101100001|
| 1|10.11.11.12|20220101100002|
| 1|10.11.11.11|20220101100003|
| 2|10.11.21.10|20220101100002|
| 2|10.11.21.10|20220101100005|
+---+-----------+--------------+
用户行为表:[uid, tid, view_time, close_time],
记录用户观看文章的行为数据,需要记录观看开始时间和关闭文章时间
# 避免基础信息表有重复数据, 去重
df_user = df_user.drop_duplicates()
df_title = df_title.drop_duplicates()
# 避免标题缺失, 缺失值填充, fillna被填充值和填充值类型要统一
df_title = df_title.fillna(subset=['title'], '未知')
# 行为数据
df_user_view.show(5)
+---+---+--------------+--------------+
|uid|tid| view_time| close_time|
+---+---+--------------+--------------+
| 1| 1|20220101100001|20220101100010|
| 1| 1|20220101100011|20220101100013|
| 1| 3|20220101100013|20220101100018|
| 2| 2|20220101100002|20220101100014|
| 2| 3|20220101100014|20220101100020|
+---+---+--------------+--------------+```
---
# 完整代码
step1, 用户行为表关联用户信息和文章信息
```c
df_user_view = df_user_view\
.join(df_title, on='tid', how='left')\
.join(df_user, on='uid', how='left')
df_user_view.show(5)
+---+---+--------------+--------------+-----------------+----+---+------+
|uid|tid| view_time| close_time| title|type|age|gender|
+---+---+--------------+--------------+-----------------+----+---+------+
| 1| 1|20220101100001|20220101100010| 老人与海| 0| 10| 0|
| 1| 1|20220101100011|20220101100013| 老人与海| 0| 10| 0|
| 2| 2|20220101100002|20220101100014|pyspark入门到精通| 1| 20| 1|
| 3| 2|20220101100001|20220101100013|pyspark入门到精通| 1| 21| 1|
| 4| 1|20220101100003|20220101100013| 老人与海| 0| 22| 0|
+---+---+--------------+--------------+-----------------+----+---+------+
step2, 根据用户id聚合计算用户标签
@F.udf(returnType=StringType())
def calc_map(x_list):
"""
通用udf, 将list转为分布形式, 并按照出现次数从大到小排序
list -> map
"""
if x_list is not None:
frequency = collections.Counter(x_list)
frequency_sort = sorted(frequency.items(), key=lambda tup: tup[1], reverse=True)
frequency_sort_str = [f'{tup[0]}:{tup[1]}' for tup in frequency_sort]
return '|'.join(frequency_sort_str)
else:
return '未知'
# 观看文章次数
# 观看tid集合
# 观看文章类型分布
df_user_title_tag = df_user_view\
.groupby('uid')\
.agg(F.count('tid').alias('title_cnt'),
F.collect_list('tid').alias('tid_list'),
calc_map(F.collect_list('type')).alias('type_map'))
df_user_title_tag.show(5)
+---+---------+---------+--------+
|uid|title_cnt| tid_list|type_map|
+---+---------+---------+--------+
| 1| 3|[1, 1, 3]| 0:2|2:1|
| 2| 2| [2, 3]| 1:1|2:1|
| 3| 2| [2, 2]| 1:2|
| 4| 2| [1, 2]| 0:1|1:1|
| 5| 2| [1, 4]| 0:1|3:1|
+---+---------+---------+--------+
# 观看最多的文章id, 取文章类型分布里面第一个即可
df_user_title_tag = df_user_title_tag\
.withColumn('top1_tid', F.split(F.split('type_map', '\\|')[0], ':')[0])
df_user_title_tag.show(5)
+---+---------+---------+--------+--------+
|uid|title_cnt| tid_list|type_map|top1_tid|
+---+---------+---------+--------+--------+
| 1| 3|[1, 1, 3]| 0:2|2:1| 0|
| 2| 2| [2, 3]| 1:1|2:1| 1|
| 3| 2| [2, 2]| 1:2| 1|
| 4| 2| [1, 2]| 0:1|1:1| 0|
| 5| 2| [1, 4]| 0:1|3:1| 0|
+---+---------+---------+--------+--------+
# 用户最近ip登录, 利用widow函数取最近登录ip和登录时间
win_func = Window.partitionBy('uid').orderBy(F.desc('login_time'))
df_last_login_ip = df_login_ip\
.withColumn('rn', F.row_number().over(win_func))\
.filter(F.col('rn') == 1)\
.select('uid',
F.col('login_ip').alias('last_login_ip'),
F.col('login_time').alias('last_login_time'))
df_user_title_tag = df_user_title_tag.join(df_last_login_ip, on='uid', how='left')
df_user_title_tag.show(5)
+---+---------+---------+--------+--------+-------------+---------------+
|uid|title_cnt| tid_list|type_map|top1_tid|last_login_ip|last_login_time|
+---+---------+---------+--------+--------+-------------+---------------+
| 6| 2| [3, 5]| 2:1|3:1| 2| 10.11.11.12| 20220101100002|
| 5| 2| [1, 4]| 0:1|3:1| 0| 10.11.11.11| 20220101100001|
| 1| 3|[1, 1, 3]| 0:2|2:1| 0| 10.11.11.11| 20220101100003|
| 3| 2| [2, 2]| 1:2| 1| 10.11.11.12| 20220101100002|
| 2| 2| [2, 3]| 1:1|2:1| 1| 10.11.21.10| 20220101100005|
+---+---------+---------+--------+--------+-------------+---------------+
- 自此用户画像相关标签计算完毕,比如用户uid=1,
- 观看文章3次,
- 对应文章id分别是[1,1,3],
- 观看文章类型0有2次,类型2有1次,
- 最喜欢文章类型是1
- 最近登录ip为10.11.11.11, 登录时间为:20220101100003
step3, 根据文章id聚合计算文章标签
# 还是根据用户行为表进行操作
# 对用户年龄进行分箱操作, 统计年龄段的分布
df_user_view = df_user_view.withColumn('age_box',
F.when(F.col('age') < 10, 0)
.when((F.col('age') >= 10) & (F.col('age') < 20), 1)
.otherwise(2))
df_user_view.show(5)
+---+---+--------------+--------------+-----------------+----+---+------+-------+
|uid|tid| view_time| close_time| title|type|age|gender|age_box|
+---+---+--------------+--------------+-----------------+----+---+------+-------+
| 1| 1|20220101100001|20220101100010| 老人与海| 0| 10| 0| 1|
| 1| 1|20220101100011|20220101100013| 老人与海| 0| 10| 0| 1|
| 2| 2|20220101100002|20220101100014|pyspark入门到精通| 1| 20| 1| 2|
| 3| 2|20220101100001|20220101100013|pyspark入门到精通| 1| 21| 1| 2|
| 4| 1|20220101100003|20220101100013| 老人与海| 0| 22| 0| 2|
+---+---+--------------+--------------+-----------------+----+---+------+-------+
# 将ip映射为城市明文, 注意需要uid和time两个一起关联查询
ip_city = {
'10.11.11.11': '北京',
'10.11.11.12': '上海',
'10.11.21.10': '深圳',
}
df_login_ip = df_login_ip.select(F.col('uid').alias('login_uid'), 'login_time', 'login_ip')
# 根据 uid, time 两个字段关联得到对应ip
df_user_view = df_user_view.join(df_login_ip,
[df_user_view.uid==df_login_ip.login_uid, df_user_view.view_time==df_login_ip.login_time],
how='left')
# ip映射成城市
get_city_func = F.udf(lambda x: ip_city.get(x), StringType())
df_user_view = df_user_view.withColumn('city',get_city_func(F.col('login_ip')))
df_user_view = df_user_view.select('uid', 'tid', 'view_time', 'close_time', 'age_box', 'city')
df_user_view.show(5)+---+---+--------------+--------------+-------+----+
|uid|tid| view_time| close_time|age_box|city|
+---+---+--------------+--------------+-------+----+
| 1| 1|20220101100001|20220101100010| 1|北京|
| 1| 1|20220101100011|20220101100013| 1|null|
| 2| 2|20220101100002|20220101100014| 2|深圳|
| 3| 2|20220101100001|20220101100013| 2|北京|
| 4| 1|20220101100003|20220101100013| 2|北京|
+---+---+--------------+--------------+-------+----+
# 聚合
df_title_tag = df_user_view\
.groupby('tid')\
.agg(F.countDistinct('uid').alias('user_ucnt'),
calc_map(F.collect_list('age_box')).alias('user_age_map'),
calc_map(F.collect_list('city')).alias('user_city_map'))
df_title_tag.show(5)
+---+---------+------------+-------------+
|tid|user_ucnt|user_age_map|user_city_map|
+---+---------+------------+-------------+
| 5| 1| 1:1| |
| 1| 3| 1:2|2:2| 北京:2|
| 3| 3| 1:2|2:1| |
| 2| 3| 2:4|北京:1|深圳:1|
| 4| 2| 2:3| 北京:2|
+---+---------+------------+-------------+
# 文章被观看时长, 简单来说就是 最少有1个用户在线的时长总和
@udf(returnType=FloatType())
def calc_online_time(view_list, close_list):
"""
计算时长
"""
time_list = [(t, 0) for t in view_list] + [(t, 1) for t in close_list]
time_sorted_list = sorted(time_list, key=lambda x: (x[0], x[1]))
init_point = time_sorted_list[0][0] # init point
flag = time_list[0][1]
cnt = 0
cnt += 1 if flag == 0 else -1 # 初始化有1个参会
_duration = 0 # 时长
for point, flag in time_sorted_list[1:]:
temp = point - init_point
# 有一个用户即算时长
if cnt >= 1:
_duration += temp
cnt += 1 if flag == 0 else -1
init_point = point
return _duration
df_view_duration = df_user_view \
.groupby('tid') \
.agg(calc_online_time(F.collect_list('view_time'),
F.collect_list('close_time')).alias('duration'))
df_title_tag = df_title_tag.join(df_view_duration, on='tid', how='left')
df_title_tag.show(5)
+---+---------+------------+-------------+--------+
|tid|user_ucnt|user_age_map|user_city_map|duration|
+---+---------+------------+-------------+--------+
| 5| 1| 1:1| | 2|
| 1| 3| 1:2|2:2| 北京:2| 22|
| 3| 3| 1:2|2:1| | 7|
| 2| 3| 2:4|北京:1|深圳:1| 17|
| 4| 2| 2:3| 北京:2| 22|
+---+---------+------------+-------------+--------+
-
自此文章画像相关标签计算完毕,比如文章id=1,
- 观看的用户有3个
- 用户年龄分布10-20岁有2个,20岁以上有1个,(这里可以根据分箱id映射回年龄段)
- 用户城市分布主要在北京,还有1个用户ip没有对上城市
- 文章总的用户观看时长为22个单位
-
其中一个文章的用户在线时长类似下面简图,从左到右计算有用户在线的时长即可