【推荐系统14】数据分析:以阿里天池新闻推荐为例学习
目录
为什么需要数据分析
读取数据
数据预处理
数据浏览
训练集用户点击日志文件merge文章具体属性
快速查看合并后的用户点击日志(trn_click)信息:
查看用户点击文件数据分布特征
查看训练集的用户数量
计算训练集中每个用户最少点击了多少篇文章
画直方图大体看基本的属性分布
不同环境的点击次数
设备组点击次数
测试集用户点击日志
查看测试集用户点击文件数据分布特征
测试集中的用户数量
计算测试集中每个用户最少点击了多少篇文章
新闻文章信息数据表
大致看文章id和字数
查看文章的字数
查看文章主题分类 ID 的分布特征
新闻文章embedding向量
查看向量维度
用户重复点击
统计每个用户对每篇文章的点击次数
编辑
查看点击次数大某个数的文章id和用户id
统计用户对文章的点击次数有哪些不同的取值
计算有多少对(用户 - 文章)点击次数是 1 次、2 次...
用户点击环境变化分析
用户点击次数分析
用户点击的文章总数的分布情况
点击数量在前50的用户
看看排在后面的用户点击次数
新闻点击次数分析
新闻点击次数总体分布情况
点击次数在前100的新闻
点击次数少的新闻
新闻共现频次:两篇新闻连续出现的频次
用户点击完 A 文章后紧接着点击 B 文章的频率
新闻文章信息
不同类型的新闻出现的次数
新闻字数的描述性统计
用户点击的新闻类型的偏好
用户查看文章的长度的分布
用户点击新闻的时间分析
点击事件的早晚排列
看用户点击行为之间的时间差
基于 Word2Vec 的用户点击序列中 文章相似度分析
总结
为什么需要数据分析
数据分析的价值主要在于熟悉了解整个数据集的基本情况包括每个文件里有哪些数据,具体的文件中的每个字段表示什么实际含义,以及数据集中特征之间的相关性,在推荐场景下主要就是分析用户本身的基本属性,文章基本属性,以及用户和文章交互的一些分布,这些都有利于后面的召回策略的选择,以及特征工程。
读取数据
# path = './data/' # 自定义的路径
path = './data/'
# train
trn_click = pd.read_csv(path + 'train_click_log.csv')
tst_click = pd.read_csv(path + 'testA_click_log.csv')item_df = pd.read_csv(path + 'articles.csv')
item_df = item_df.rename(columns={'article_id': 'click_article_id'}) #重命名,方便后续match
item_emb_df = pd.read_csv(path+'articles_emb.csv')
数据预处理
# 数据预处理
# 对每个用户的点击时间戳进行排序
trn_click['rank'] = trn_click.groupby(['user_id'])['click_timestamp'].rank(ascending=False).astype(int)
tst_click['rank'] = trn_click.groupby(['user_id'])['click_timestamp'].rank(ascending=False).astype(int)
# 计算用户点击文章的次数,并添加新的一列count
trn_click['click_cnts'] = trn_click.groupby('user_id')['click_article_id'].transform('count')
tst_click['click_cnts'] = trn_click.groupby('user_id')['click_article_id'].transform('count')
数据浏览
训练集用户点击日志文件merge文章具体属性
# 用户点击日志文件
trn_click = trn_click.merge(item_df, how='left', on=['click_article_id'])
trn_click.head()

快速查看合并后的用户点击日志(trn_click)信息:
trn_click.info()

trn_click.info() 的作用是快速诊断数据集的基本结构:包括样本量(行数)、特征数(列数)、每列的非缺失值情况、数据类型以及内存占用。
查看用户点击文件数据分布特征
trn_click.describe() 是 pandas 中用于生成数值型数据的统计描述报告的方法,主要作用是快速获取 DataFrame 中所有数值列的关键统计量,帮助你初步了解数据的分布特征。

查看训练集的用户数量

计算训练集中每个用户最少点击了多少篇文章

画直方图大体看基本的属性分布
plt.figure(figsize=(15, 20))
i = 1 # 子图序号,从1开始
for col in ['click_article_id', 'click_timestamp', 'click_environment', 'click_deviceGroup', 'click_os', 'click_country', 'click_region', 'click_referrer_type', 'rank', 'click_cnts']:# 修正1:子图序号用i,否则所有图会重叠在第一个子图plt.subplot(5, 2, i) i += 1 # 序号递增# 修正2:value_counts后重置索引,显式指定两列的名称# 原索引列(col的取值)命名为'value',计数列命名为'count'v = trn_click[col].value_counts().reset_index(name='count').rename(columns={col: 'value'})[:10]# 修正3:用新列名绘图,x轴是'value',y轴是'count'fig = sns.barplot(x='value', y='count', data=v)# x轴标签旋转,避免重叠for item in fig.get_xticklabels():item.set_rotation(90)plt.title(f'{col} 分布') # 标题显示列名plt.tight_layout() # 自动调整子图间距
plt.show()
- 每个子图对应一个特征列(如
click_environment的分布)。 - 子图中展示该列出现次数前 10 的值,以及它们的具体出现次数。

不同环境的点击次数
trn_click['click_environment'].value_counts()

设备组点击次数

可见设备1占大部分(61%),设备3占36%。
测试集用户点击日志
tst_click = tst_click.merge(item_df, how = 'left', on = ['click_article_id'])
tst_click.head()

训练集的用户ID由0 ~ 199999,而测试集A的用户ID由200000 ~ 249999。
查看测试集用户点击文件数据分布特征
tst_click.describe()

测试集中的用户数量

计算测试集中每个用户最少点击了多少篇文章

新闻文章信息数据表
大致看文章id和字数
item_df.head().append(item_df.tail())
pd.concat([item_df.head(), item_df.tail()])这行代码的作用是 将数据集 `item_df` 的前几行和后几行合并在一起展示,方便快速浏览数据集的整体结构和内容分布。

查看文章的字数
item_df['words_count'].value_counts()

查看文章主题分类 ID 的分布特征
每个区间内包含的分类 ID 数量,用柱子高度表示该区间的频率。
print(item_df['category_id'].nunique()) # 461个文章主题
item_df['category_id'].hist()
新闻文章embedding向量
item_emb_df.head()

查看向量维度
item_emb_df.shape

364047行251列
用户重复点击
统计每个用户对每篇文章的点击次数
user_click_merge = pd.concat([trn_click, tst_click], ignore_index=True)
user_click_count = user_click_merge.groupby(['user_id', 'click_article_id'])['click_timestamp'].agg({'count'}).reset_index()
user_click_count[:10]
将训练集点击数据(trn_click)和测试集点击数据(tst_click)合并成一个完整的用户点击数据集(user_click_merge)
按 “用户 ID” 和 “文章 ID” 分组,统计每个用户对每篇文章的点击次数。
展示统计结果(user_click_count)的前 10 行数据。
查看点击次数大某个数的文章id和用户id

统计用户对文章的点击次数有哪些不同的取值

计算有多少对(用户 - 文章)点击次数是 1 次、2 次...
user_click_count.loc[:,'count'].value_counts()

用户点击环境变化分析
def plot_envs(df, cols, r, c):plt.figure(figsize=(10, 5))i = 1for col in cols:plt.subplot(r, c, i)i += 1v = df[col].value_counts().reset_index(name='count') v.rename(columns={'index': col}, inplace=True) sns.barplot(x=col, y='count', data=v)plt.xticks(rotation=90)plt.title(col)plt.tight_layout()plt.show()
# 分析用户点击环境变化是否明显,这里随机采样10个用户分析这些用户的点击环境分布
sample_user_ids = np.random.choice(tst_click['user_id'].unique(), 10, replace=False)
sample_users = user_click_merge[user_click_merge['user_id'].isin(sample_user_ids)]
cols = ['click_environment','click_deviceGroup', 'click_os', 'click_country', 'click_region','click_referrer_type']
for _, user_df in sample_users.groupby('user_id'):plot_envs(user_df, cols, 2, 3)

可以看出绝大多数数的用户的点击环境是比较固定的。
用户点击次数分析
用户点击的文章总数的分布情况
user_click_item_count = sorted(user_click_merge.groupby('user_id')['click_article_id'].count(), reverse=True)
plt.plot(user_click_item_count)

点击数量在前50的用户
plt.plot(user_click_item_count[:50])

点击次数排前50的用户的点击次数都在100次以上。思路:我们可以定义点击次数大于等于100次的用户为活跃用户,这是一种简单的处理思路, 判断用户活跃度,更加全面的是再结合上点击时间。
看看排在后面的用户点击次数

点击次数小于等于两次的用户非常的多,这些用户可以认为是非活跃用户
新闻点击次数分析
新闻点击次数总体分布情况
item_click_count = sorted(user_click_merge.groupby('click_article_id')['user_id'].count(), reverse=True)
plt.plot(item_click_count)
X 轴(横轴):文章的排名序号(按点击量从高到低排序)
Y 轴(纵轴):对应排名文章的总点击次数

点击次数在前100的新闻

可以看出点击次数最多的前100篇新闻,点击次数大于1000次
点击次数少的新闻

可以发现很多新闻只被点击过一两次。思路:可以定义这些新闻是冷门新闻
新闻共现频次:两篇新闻连续出现的频次
用户点击完 A 文章后紧接着点击 B 文章的频率
tmp = user_click_merge.sort_values('click_timestamp')
tmp['next_item'] = tmp.groupby('user_id')['click_article_id'].transform(lambda x:x.shift(-1))
union_item = tmp.groupby(['click_article_id','next_item'])['click_timestamp'].agg({'count'}).reset_index().sort_values('count', ascending=False)
union_item[['count']].describe()

由统计数据可以看出,平均共现次数3.18,最高为2202。
说明用户看的新闻,相关性是比较强的。
画图看(y轴表示跳转次数):

新闻文章信息
不同类型的新闻出现的次数

新闻字数的描述性统计
user_click_merge['words_count'].describe()

画图:words_count 记录了每篇被点击文章的字数
plt.plot(user_click_merge['words_count'].values)

用户点击的新闻类型的偏好
此特征可以用于度量用户的兴趣是否广泛。

从上图中可以看出有一小部分用户阅读类型是极其广泛的,大部分人都处在20个新闻类型以下。
数值统计:
user_click_merge.groupby('user_id')['category_id'].nunique().reset_index().describe()

用户查看文章的长度的分布
通过统计不同用户点击新闻的平均字数,这个可以反映用户是对长文更感兴趣还是对短文更感兴趣。
plt.plot(sorted(user_click_merge.groupby('user_id')['words_count'].mean(), reverse=True))

从上图中可以发现有一小部分人看的文章平均词数非常高,也有一小部分人看的平均文章次数非常低。
大多数人偏好于阅读字数在200-400字之间的新闻
可以再加上[1000:45000]挑选区间:
plt.plot(sorted(user_click_merge.groupby('user_id')['words_count'].mean(), reverse=True)[1000:45000])
详细统计:
user_click_merge.groupby('user_id')['words_count'].mean().reset_index().describe()

用户点击新闻的时间分析
点击事件的早晚排列
将所有用户的点击记录按 “点击时间” 从早到晚重新排列:
from sklearn.preprocessing import MinMaxScaler
mm = MinMaxScaler()
user_click_merge['click_timestamp'] = mm.fit_transform(user_click_merge[['click_timestamp']])
user_click_merge['created_at_ts'] = mm.fit_transform(user_click_merge[['created_at_ts']])user_click_merge = user_click_merge.sort_values('click_timestamp')
user_click_merge.head()

看用户点击行为之间的时间差
def mean_diff_time_func(df, col):df = pd.DataFrame(df) # 确保输入是DataFrame格式df['time_shift'] = df[col].shift(-1).fillna(0) # 下移一行取后一次时间,最后一行补0df['diff_time'] = abs(df[col] - df['time_shift']) # 修正笔误:time_shift1 → time_shiftreturn df['diff_time'].mean() # 返回该用户的平均时间差# 按用户分组,计算每个用户的点击时间差平均值(注意用列表选取列)
mean_diff_click_time = user_click_merge.groupby('user_id')[['click_timestamp', 'created_at_ts']].apply(lambda x: mean_diff_time_func(x, 'click_timestamp')
)# 按平均时间差从大到小排序并绘图
plt.plot(sorted(mean_diff_click_time.values, reverse=True))
- 按用户分组,计算每个用户连续两次点击行为的时间差的平均值(
click_timestamp是归一化后的点击时间戳)。 - 将所有用户的平均时间差按从大到小排序,并用折线图展示分布(X 轴为用户序号,Y 轴为平均点击时间差)。
基于 Word2Vec 的用户点击序列中 文章相似度分析
from gensim.models import Word2Vec
import logging, pickle
import numpy as np # 注意添加这行导入(后面用到np.random.choice)def train_item_word2vec(click_df, embed_size=16, save_name='item_w2v_emb.pkl', split_char=' '):click_df = click_df.sort_values('click_timestamp')click_df['click_article_id'] = click_df['click_article_id'].astype(str)# 转换成句子形式docs = click_df.groupby(['user_id'])['click_article_id'].apply(lambda x: list(x)).reset_index()docs = docs['click_article_id'].values.tolist()# 日志配置logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.INFO)# 关键修正:size → vector_sizew2v = Word2Vec(docs, vector_size=embed_size, # 用vector_size替代size,指定向量维度sg=1, window=5, seed=2020, workers=24, min_count=1, epochs=10 # 注意:旧版本的iter参数在新版本中改为epochs)# 保存文章向量字典item_w2v_emb_dict = {k: w2v.wv[k] for k in click_df['click_article_id'].unique()} # 加unique()避免重复键return item_w2v_emb_dict# 训练模型
item_w2v_emb_dict = train_item_word2vec(user_click_merge)# 随机选择15个用户,查看其点击文章的前后相似性
sub_user_ids = np.random.choice(user_click_merge.user_id.unique(), size=15, replace=False)
sub_user_info = user_click_merge[user_click_merge['user_id'].isin(sub_user_ids)]
sub_user_info.head()
def get_item_sim_list(df):sim_list = []item_list = df['click_article_id'].valuesfor i in range(0, len(item_list)-1):emb1 = item_w2v_emb_dict[str(item_list[i])] # 需要注意的是word2vec训练时候使用的是str类型的数据emb2 = item_w2v_emb_dict[str(item_list[i+1])]sim_list.append(np.dot(emb1,emb2)/(np.linalg.norm(emb1)*(np.linalg.norm(emb2))))sim_list.append(0)return sim_list
for _, user_df in sub_user_info.groupby('user_id'):item_sim_list = get_item_sim_list(user_df)plt.plot(item_sim_list)
- 用户的点击序列作为 “语料”,通过 Word2Vec 将文章转化为向量;
- 向量的相似度可反映文章的关联程度(用户行为上的关联);
- 抽取样本用户数据是为了验证模型效果(比如看用户连续点击的文章是否向量相似)


- X 轴:用户点击的文章序号(如第 1 篇、第 2 篇、第 3 篇...)。
- Y 轴:相邻两篇文章的余弦相似度(值越高,向量越相似)。
按用户分组,对之前随机抽取的 15 个用户,逐个计算其点击序列中相邻文章的相似度,并绘制折线图。
总结
通过数据分析的过程, 我们目前可以得到以下几点重要的信息, 这个对于我们进行后面的特征制作和分析非常有帮助:
- 训练集和测试集的用户id没有重复,也就是测试集里面的用户模型是没有见过的
- 训练集中用户最少的点击文章数是2, 而测试集里面用户最少的点击文章数是1
- 用户对于文章存在重复点击的情况, 但这个都存在于训练集里面
- 同一用户的点击环境存在不唯一的情况,后面做这部分特征的时候可以采用统计特征
- 用户点击文章的次数有很大的区分度,后面可以根据这个制作衡量用户活跃度的特征
- 文章被用户点击的次数也有很大的区分度,后面可以根据这个制作衡量文章热度的特征
- 用户看的新闻,相关性是比较强的,所以往往我们判断用户是否对某篇文章感兴趣的时候, 在很大程度上会和他历史点击过的文章有关
- 用户点击的文章字数有比较大的区别, 这个可以反映用户对于文章字数的区别
- 用户点击过的文章主题也有很大的区别, 这个可以反映用户的主题偏好 10.不同用户点击文章的时间差也会有所区别, 这个可以反映用户对于文章时效性的偏好

