pyspark-04 画像分析

以CSDN为场景,基于用户观看文章行为数据,利用pyspark分别对用户和文章进行标签画像。

主要涉及pyspark以下应用:

  1. 基础的数据清洗
  2. 统计指标计算
  3. 聚合分布
  4. 自定义udf
  5. 表关联

涉及数据:

  1. 用户基础信息表
  2. 文章基础信息表
  3. 用户登录ip流水表
  4. 用户行为表

输出画像:

  1. 用户画像数据

    • 观看文章次数
    • 观看文章id list
    • 观看文章类型分布
    • 最爱看的文章类型
    • 用户最近登录ip和登录时间
  2. 文章画像数据

    • 多少用户观看
    • 用户年龄段分布
    • 用户城市分布
    • 用户观看总时长

数据示例

用户基础信息: [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,
    1. 观看文章3次,
    2. 对应文章id分别是[1,1,3],
    3. 观看文章类型0有2次,类型2有1次,
    4. 最喜欢文章类型是1
    5. 最近登录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个单位
  • 其中一个文章的用户在线时长类似下面简图,从左到右计算有用户在线的时长即可在这里插入图片描述


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值