简介面向希望掌握Python数据挖掘与机器学习实战的数据分析初学者这份资料以电子商务网站用户行为分析及服务推荐为案例覆盖从数据预处理、探索性分析到协同过滤、矩阵分解等推荐模型构建的完整流程。压缩包仅含5个文件79KB其中2个Jupyter Notebook承载主要代码与讲解1个Excel数据表提供原始数据另含SQL脚本及说明文档轻量但结构完整。目前已有419人学习/下载。通过该代码与数据集读者可直观理解Pandas数据处理、可视化与推荐算法实现并能直接复用核心代码到自己的场景中适合入门级读者结合Notebook逐段练习快速建立从数据到模型的实战认知。1. 电商用户行为分析先把“服务推荐”这颗子弹喂给谁做推荐之前多数人第一反应是“把点击多的商品推给用户”但真正跑一遍数据你会被泼冷水用户行为日志里噪声占了大半点击多的商品未必是用户想买的浏览时长长的页面往往是详情页里那几张没加载出来的图。这个python数据挖掘机器学习实战代码数据集——电子商务网站用户行为分析及服务推荐项目核心不是教你调一个推荐模型而是让你完整走一遍“原始日志 → 用户画像 → 推荐候选 → 离线评估”的链路。适合刚学完 pandas 和 sklearn、想拿一份真实结构的数据练手的人也适合已经在业务里做过报表、想往算法方向转的工程师。数据挖掘在这里不玄乎就三件事把行为切成会话、把会话算成特征、把特征喂给协同过滤。这篇笔记按我自己的落地顺序写你照着跑一遍至少能回答“这个推荐到底推给谁、凭什么推给他”。2. 载入与清洗一份行为日志里能挖出的字段边界2.1 数据集结构四种行为之间的优先级关系电商行为日志最常见的是四列用户ID、商品ID、行为类型、时间戳。行为类型一般就四个值浏览、收藏、加购、购买。这里的第一个坑是“行为之间不是平行关系而是漏斗关系”。你在做清洗的时候不能把这四种行为当成四个独立的标签来处理它们是有先后逻辑的浏览→收藏→加购→购买。而在清洗阶段最要紧的是把“同一秒内重复点击同一个商品”的脏数据过滤掉——这类数据多半来自前端埋点重复上报。import pandas as pd df pd.read_csv(user_behavior.csv, header0) print(df.head()) print(df.info()) print(df.isnull().sum()) # 去掉同一用户同一商品同一行为在 3 秒内重复出现的记录 df df.sort_values([user_id, timestamp]) df[time_diff] df.groupby([user_id, item_id, behavior])[timestamp].diff() df df[~((df[time_diff] 3) (df[time_diff].notna()))].drop(columns[time_diff]) print(清洗后剩余记录数:, len(df))这个操作里groupby和diff是配合使用的先按用户、商品、行为分组然后计算每条记录和前一条记录的时间差时间差小于等于 3 秒的就删掉。参数 3 是怎么定的一般取埋点上报的典型间隔有的前端框架是 2 秒一次心跳有的拖拽事件会产生大量瞬时重复点击我习惯取 3 秒宁可多滤一点也不要让后面算特征时被同一事件的重复上报带偏。清洗后务必打印一下行数心里有个底后面特征计算都是基于这份干净的 DataFrame。2.2 时间窗口与会话切分给每一次点击贴上会话标签会话Session切分是做用户行为分析绕不开的一步。很多数据集没有直接给 session_id只给了时间戳这就需要你自己定义一个“多久没动作就断开会话”的阈值。电商场景一般用 30 分钟超过 30 分钟没有新行为就算一次新会话。别小看这一步会话切分直接影响后面的“会话内行为序列”特征——你把两次购物切成一个会话推荐出来的商品顺序就会显得莫名其妙。import pandas as pd df[timestamp] pd.to_datetime(df[timestamp], units) df df.sort_values([user_id, timestamp]).reset_index(dropTrue) # 按用户分组计算与上一条记录的时间差单位分钟 df[gap_min] df.groupby(user_id)[timestamp].diff().dt.total_seconds() / 60.0 # 超过 30 分钟未动作则新会话编号 1 df[session_id] (df[gap_min].isna() | (df[gap_min] 30)).astype(int) df[session_id] df.groupby(user_id)[session_id].cumsum() # 生成完整会话ID df[session_key] df[user_id].astype(str) _ df[session_id].astype(str) # 每个会话的长度和商品数用于后面特征筛选 session_stats df.groupby(session_key).agg( session_len(timestamp, count), item_cnt(item_id, nunique), total_time(timestamp, lambda x: (x.max() - x.min()).total_seconds()) ).reset_index() print(session_stats.describe())这里的核心逻辑在cumsum上先构造一个 0/1 标记1 代表“这是新会话”然后按用户做累计求和这样每一次新会话都会让编号加一得到的就是每个用户的会话序号。参数 30 分钟是电商通用默认值但如果你是做超短决策场景比如闪购可能要缩到 10 分钟如果是做酒店预订这类长决策场景可能放宽到 60 分钟。这个值不要拍脑袋可以跑几个候选值看会话平均长度和覆盖率的变化选一个让“绝大多数正常购买流程落在同一会话”的值。2.3 时间特征的离散化星期、时段和促销期的隐藏信号拿到干净的时间戳之后别急着做特征先把时间拆成几个维度星期几、一天中的哪个时段、是否周末。电商行为在周末和工作日差异巨大晚上 8 点到 11 点是下单高峰。另外很多数据集里藏着一个隐性变量——促销日如果你发现某几天的购买转化率是均值的三四倍那几天多半有活动。你可以在时间特征里加一列is_promo但要小心别让模型直接学到“促销日无脑推爆款”。df[weekday] df[timestamp].dt.dayofweek df[hour] df[timestamp].dt.hour df[is_weekend] (df[weekday] 5).astype(int) # 按小时聚合看下单高峰在哪几个时段 hourly_buy df[df[behavior] buy].groupby(hour)[item_id].count() print(hourly_buy.sort_values(ascendingFalse).head(10)) # 给每个行为打上时段标签 def period_label(h): if h in [10, 11, 12]: return midday elif h in [19, 20, 21, 22, 23]: return night elif h in [6, 7, 8, 9]: return morning else: return other df[period] df[hour].map(period_label)这段代码会帮你发现自己的数据里到底哪个时段是黄金时段而不是直接套别人文章里的结论。比如我跑过一份数据下单高峰竟然是上午 10 点到 12 点和很多人说的“晚上最高”不一样反而更接近白领摸鱼时段的场景。所以这里不要抄参数先打印出来看再决定。3. 从行为到特征RFM 指标与商品维度特征的计算细节3.1 RFM 特征三个数字描述一个用户怎么算才稳RFM 是用户行为分析里最经典的一套特征Recency最近一次购买距今多久、Frequency一段时间内购买次数、Monetary总消费金额。但这个项目里有个现实问题——很多公开数据集没有金额字段只有行为类型。这种情况怎么办我的做法是先把“购买”当作目标行为R 用最近一次购买 / 浏览 / 加购距统计窗口结束的时间差F 用购买次数M 用“加购次数×价格中位数”这种近似值。有价格字段就用真实价格没有就退而求其次用行为权重折算。import pandas as pd import numpy as np # 假设 df 有 item_price 字段没有就从 item 维度补一张价格表 # price_map df.drop_duplicates(item_id).set_index(item_id)[item_price] # 只保留购买行为或按行为加权 buy_df df[df[behavior] buy].copy() # 以数据集的最后时间为基准 latest_ts df[timestamp].max() rfm buy_df.groupby(user_id).agg( recency(timestamp, lambda x: (latest_ts - x.max()).days), frequency(timestamp, count), monetary(item_price, lambda x: (x * 1.0).sum()) ).reset_index() # 分位数打分1~5 分 for col in [recency, frequency, monetary]: rfm[col _score] pd.qcut(rfm[col], 5, labels[1, 2, 3, 4, 5]) rfm[rfm_total] rfm[recency_score].astype(int) rfm[frequency_score].astype(int) rfm[monetary_score].astype(int) print(rfm.head())这里我踩过的坑是pd.qcut会在数据分布极偏的时候报错——比如大部分用户只买过一次frequency 的 5 个分位切出来有几组是空的。解决办法是加duplicatesdrop参数或者直接用rank(pctTrue)做百分位映射。另外注意 recency 的分值和 frequency 方向是反的recency 越小越好frequency 越大越好所以如果你后面要把三个分数直接相加先把 recency_score 反转也就是5 - recency_score否则 RFM 总分越高反而代表用户越久没来。这个方向性错误我至少见过三次属于那种一眼看不出来、模型结果怎么调都不对的血泪经验。3.2 商品侧特征被推荐对象也得有画像用户侧特征做完了商品侧特征常常被忽略。协同过滤只用到“用户—商品”交互矩阵但如果你想做冷启动或者想给推荐结果加一层业务规则比如不推库存不足的商品商品特征就是那张底牌。最基础的商品特征是商品被浏览/加购/购买的总次数、购买转化率购买数/浏览数、商品上架天数。加购转化率尤其有用它比点击率诚实得多——点击可能是误触加购是明确意图。item_features df.groupby(item_id).agg( view_cnt(behavior, lambda x: (x view).sum()), cart_cnt(behavior, lambda x: (x cart).sum()), buy_cnt(behavior, lambda x: (x buy).sum()), ).reset_index() item_features[cart_to_view] item_features[cart_cnt] / (item_features[view_cnt] 1) item_features[buy_to_cart] item_features[buy_cnt] / (item_features[cart_cnt] 1) item_features[buy_rate] item_features[buy_cnt] / (item_features[view_cnt] 1)注意分母里都加了 1这是平滑处理防止除零。这种加 1 平滑在数据量小的时候特别重要不然一个只有 2 次浏览 1 次购买的商品buy_rate 直接干到 0.5比那些被浏览 10000 次购买 2000 次的爆款还高推荐出来就翻车了。另外一个要点是cart_cnt和buy_cnt的权重在不同行业差别很大服装类目加购到购买的转化很低用户喜欢先加购再比价生鲜类目加购后基本就会下单。你算完这五个特征之后先做个相关性矩阵看看哪些特征和“是否被购买”最相关再决定喂给模型时留哪几个。3.3 用户—商品交互矩阵别用 dense 矩阵除非你内存无限在 Python 里构建用户—商品矩阵新手最容易写的是pd.crosstab(user, item)得到的是一个稀疏 DataFrame后面转成 numpy 数组直接爆内存。正确做法是用 scipy 的稀疏矩阵或者 pandas 的稀疏格式。这一点在数据量过万时就会显现——1 万用户 × 1 万商品就是 1 亿单元格dense 矩阵占 800MB而真实非零元素可能只有 20 万个压缩后几 MB 就搞定。from scipy.sparse import coo_matrix, csr_matrix import numpy as np # 只保留购买行为构建矩阵 interact_df buy_df[[user_id, item_id]].drop_duplicates() user_ids interact_df[user_id].astype(category) item_ids interact_df[item_id].astype(category) row user_ids.cat.codes.values col item_ids.cat.codes.values data np.ones(len(interact_df)) # COO 格式适合构建 interact_sparse coo_matrix((data, (row, col)), shape(len(user_ids.categories), len(item_ids.categories))) # 转成 CSR适合后续矩阵乘法 interact_csr interact_sparse.tocsr() print(稀疏矩阵形状:, interact_csr.shape) print(非零元素数量:, interact_csr.nnz) print(稠密度: {:.4f}%.format(100 * interact_csr.nnz / (interact_csr.shape[0] * interact_csr.shape[1])))astype(category)是关键一步它把用户 ID 和商品 ID 从字符串或长整数映射成连续的 0~N-1 编码后面的cat.codes直接取编码值。注意这里有个坑如果交互是带权重的比如加购算 2、购买算 5data 数组可以替换成权重值不必都是 1。但用coo_matrix时如果有重复的 (user, item) 对它默认是叠加而不是去重所以前面一定要drop_duplicates()或者你刻意想用“行为次数”当权重那就别去重。4. 从协同过滤到服务推荐两条最稳的落地路线4.1 Item-based 协同过滤电商推荐里的主力方案电商场景里 ItemCF基于物品的协同过滤比 UserCF 用得广原因很简单用户的兴趣变化快但商品之间的关联相对稳定。“看了 A 商品的人还看了 B 商品”是电商平台最成熟的一种推荐策略。ItemCF 的核心是算商品相似度矩阵相似度计算常见的有余弦相似度、皮尔逊相关系数、Jaccard 相似度。余弦相似度在用户—物品矩阵上是默认选项而且可以直接用稀疏矩阵乘法算出来不用暴力两两循环。from sklearn.preprocessing import normalize # 基于购买矩阵计算商品相似度 # 思路将交互矩阵按列归一化每个商品为单位向量 item_norm normalize(interact_csr.T, norml2, axis1) # 每行是一个商品的特征向量 item_sim item_norm item_norm.T # 商品-商品余弦相似度 # 转成 DataFrame 方便查看 item_sim_df pd.DataFrame(item_sim.toarray()) print(item_sim_df.head())这段代码背后的数学逻辑一句话讲清先把每个商品的列向量归一化成单位长度两个单位向量的点积就是余弦相似度。矩阵乘法item_norm item_norm.T一次算完所有商品两两之间的相似度。参数norml2就是欧几里得范数。这里要注意如果你直接用interact_csr.T去 normalize得到的是按用户归一化那不是我们要的——我们要的是按商品归一化所以转置之后每行才是商品。这个方向搞反了后面的相似度矩阵会完全不可读而且数字还挺好看属于最阴的错。4.2 给目标用户生成推荐Top-N 的两种生成逻辑相似度矩阵算完推荐生成可以走两条路。第一是“买过商品的相似物品”聚合把用户购买过的所有商品在相似度矩阵里找各自最相似的 K 个物品加权求和后取 Top-N。第二是“未购买且相似度高”的直接筛。第二条路在代码上更直观但在实际场景里往往被第一条替代因为第一个方案天然包含了数量效应——用户买过 10 个商品每个商品推荐 10 个相似品候选池就有 100再按总分排序比只看一次交互可靠得多。import numpy as np def recommend_for_user(user_id, user_item_mat, item_sim_mat, user_ids_map, item_ids_map, top_k10): # 找到该用户在矩阵中的行索引 u_idx list(user_ids_map).index(user_id) user_vec user_item_mat[u_idx] # 用户买过的所有商品索引 bought_items user_vec.indices if len(bought_items) 0: return [] # 统计每个候选商品的得分 score np.zeros(item_sim_mat.shape[0]) for i in bought_items: sim_scores item_sim_mat[i] # 排除已经买过的商品同时用“是否购买”做加权 score sim_scores * user_vec[0, i] # 去掉已购商品 score[bought_items] -np.inf # 取 Top-N top_indices np.argsort(score)[::-1][:top_k] return [(item_ids_map[j], score[j]) for j in top_indices if score[j] ! -np.inf] # 用法示例给 user_id123 的用户推荐 10 件商品 reco recommend_for_user(123, interact_csr, item_sim, user_ids.categories, item_ids.categories, top_k10) print(reco)注意score sim_scores * user_vec[0, i]user_vec[0, i]是用户对该商品的交互权重。如果你矩阵里只存了 0/1这个权重就是 1如果你存了行为次数或者行为等级这里就会自动带上“用户有多喜欢这个商品”的信息。一个细节bought_items来自稀疏矩阵的indices属性这是 CSR 格式的特性COO 格式没有这个属性所以前面转 CSR 不只是为了省内存还为了这里取“非零元素的列索引”。4.3 User-based 协同过滤什么时候值得换这条线UserCF 核心是“和你相似的用户买了什么”。它比 ItemCF 更适合用户少、商品变化快的场景比如新闻资讯或短视频。电商里用户量动辄百万UserCF 要算百万用户的相似度矩阵代价比 ItemCF 高得多。但如果你的场景是“新用户已登录但没买过东西、只有浏览行为”此时 ItemCF 无从下手——用户没有购买交集UserCF 可以用浏览行为找到相似用户再推相似用户买的商品。这算是冷启动的兜底方案。from sklearn.metrics.pairwise import cosine_similarity # 用户相似度直接使用稀疏矩阵 user_sim cosine_similarity(interact_csr) # 用户-用户相似度矩阵一句代码就够。但cosine_similarity这里等价于先 normalize 再点积结果和 4.1 里手写的一致只是封装好了。实际项目里我不会用这个矩阵去做全量推荐太慢。通常的做法是先只算与目标用户相似度最高的 50 个用户再把这 50 个用户买过但你没买过的商品拉出来按相似度加权聚合取 Top-N。这样每一步的复杂度都可控。如果你发现 UserCF 结果差先别调参去查用户相似度是不是被“超级活跃用户”绑架了——那种什么都买的人会和所有人相似。解决办法是两个一是对交互次数做 log1p 压缩二是算相似度时不考虑购买超过 200 个商品的用户。5. 项目实战中的五个深坑现象、原因与对策5.1 现象准确率很高点击率却起不来我在跑推荐项目时经常遇到一种怪象离线评估的准确率有 0.25感觉还行但线上点击率只有同业的四分之一。后来排查发现问题不在模型而在训练数据的时间窗口——我把一个月的数据混在一起训练热门商品因为交互次数多被模型反复推荐而用户早就买过或者已经过了那个兴趣周期自然不愿意点。原因就是时间权重缺失。解决方法是给交互矩阵加时间衰减越近的交互权重越高比如按天衰减权重 1 / (1 log(1 天数差))。在构建矩阵的 data 数组里直接乘上这个衰减系数就行。5.2 现象ItemCF 在冷门商品上给出离谱推荐你算相似度矩阵时如果某个商品只有一两个人买过它和另一个冷门商品的共现次数可能恰好是 1余弦相似度算出 0.9 这种虚高值。原因就是共现次数太少统计上不可靠。解决方法是做“显著性折扣”共现次数低于 5 的相似度直接置零或者用“加权相似度”——把相似度乘以共现次数再除以 log(次数1) 之类的函数压制。更简单的办法是商品相似度矩阵只保留每个商品的最强 Top-20 相似关系其余全设为零这样能顺带压缩矩阵体积一箭双雕。5.3 现象训练集和线上行为分布不一致你用了前 80% 时间的数据训练后 20% 做验证离线指标很漂亮一上线就崩。原因大概率是 90% 的商品在训练期之后才上架模型根本没见过它们何谈推荐。电商里商品生命周期极短服装类目上新周期只有 14 天。解决方法是在建模前先做一个商品覆盖率的检查看看训练集里的商品在验证集里还剩下多少如果覆盖率低于 60%就要缩短训练窗口到最近 7 天或者换成交叉验证——用第 N 周训练、第 N1 周验证这种滑窗方式不要随机切分。随机切分在用户行为数据上本来就是错的你拿随机切分的结果去预估线上表现翻车是必然的。5.4 现象相似度矩阵占用内存直接爆掉我见过一个 30 万商品的项目有人直接pd.DataFrame(item_sim)把相似度矩阵转成稠密 DataFrame内存直接 30 万 × 30 万 × 8 字节 720GB 爆掉。原因就是没存成稀疏格式。相似度矩阵里面绝大多数值都是零或接近零稀疏存储后只有几 GB。解决方法是算完相似度之后马上做“阈值截断”——把小于 0.1 的值全置零再用scipy.sparse.csr_matrix存。这一步不仅能省内存还能让后面推荐生成时的矩阵乘法快很多因为零元素越多稀疏矩阵乘法越省时间。5.5 现象推荐结果全是爆款如果推荐列表反复出现那些高销量商品看起来没毛病但实际对用户没有意义——用户打开首页看到自己随手点过的爆款又出现在“猜你喜欢”里不会产生点击欲望。原因是 ItemCF 天然偏向高频物品它们和太多商品有共现。常见做法是在最终生成推荐列表时加一个“流行度惩罚”给每个商品乘一个衰减系数比如score score / (log(1 全局热度) ** alpha)alpha 一般取 0.5~1越大越保守。这个技巧不是把爆款全干掉而是给腰部商品一个露脸机会在很多场景下能把点击率拉回来两三个百分点。6. 离线评估与上线前验证用一个回看实验检验推荐质量离线评估不能用 accuracy推荐任务是排序问题用命中率Hit Rate和覆盖率更诚实。命中率的意思是用户真实购买过的商品是否出现在你给他推荐的 Top-10 里。做法很简单——把用户按时间切半前半段用于生成推荐后半段看用户买了什么再比对推荐列表。如果命中率有 5%在这个场景里就已经不算差很多电商的实际命中率在 1%~3% 之间徘徊。def compute_hit_rate(reco_dict, heldout_df, top_k10): hit 0 total 0 for user, group in heldout_df.groupby(user_id): if user not in reco_dict: continue bought_items set(group[item_id].values) reco_items set([item for item, score in reco_dict[user]][:top_k]) if bought_items reco_items: hit 1 total 1 return hit / max(total, 1) # 时间切分按时间戳分训练/验证 split_ts df[timestamp].max() - pd.Timedelta(days7) train_df df[df[timestamp] split_ts] test_df df[df[timestamp] split_ts] # 这里用 train_df 构建矩阵、算相似度、生成推荐 # 再把 recommend_for_user 的输出整理成 {user_id: [(item_id, score), ...]} 的字典 reco_dict {} for u in test_df[user_id].unique()[:1000]: # 先评估前1000个用户控制耗时 reco_dict[u] recommend_for_user(u, train_mat, item_sim, user_map, item_map, top_k10) hit_rate compute_hit_rate(reco_dict, test_df, top_k10) print(Hit Rate10:, hit_rate)这个评估设计里有两个细节值得说。第一只评估验证集里有行为的用户别把验证集里没出现的用户算进去否则分母虚高。第二top_k10这个参数要和业务对齐——你的推荐位是 6 个还是 10 个评估就用几。我见过有人推荐位只有 4 个评估却用 Top-10 的命中率线上自然对不上。覆盖率也要算一下公式是“推荐过的商品数量 / 全部商品数量”。覆盖率低于 5% 说明推荐只在少数爆款之间打转长期来看用户体验会烂掉。还有一个指标叫“新颖度”用推荐商品的平均热度倒数来衡量计算方法就是推荐列表里的商品它们的全局交互次数的平均值的倒数。这个指标不用上线也能算用来监控我们第 5.5 里说的“全是爆款”的问题。做这个项目最大的教训是什么我早期总是纠结于用什么高级模型矩阵分解、FM、GNN 轮着上结果折腾一星期指标还不如 ItemCF 加时间衰减。推荐系统的起步阶段数据清洗和特征工程带来的收益远大于模型升级。如果你手头只有一个电脑、一份 CSV别急着上深度学习先把行为事件切好、把用户画像算对、把相似度矩阵的稀疏化做好这套流程本身就值回票价。希望帮到你。本文还有配套的精品资源点击获取