☰
电商用户行为分析与服务推荐:Python数据挖掘实战
2026/9/28 11:44:23 网站建设 项目流程

简介:面向希望掌握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", header=0) 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"], unit="s") df = df.sort_values(["user_id", "timestamp"]).reset_index(drop=True) # 按用户分组,计算与上一条记录的时间差(单位:分钟) 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(ascending=False).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 个分位切出来有几组是空的。解决办法是加duplicates="drop"参数,或者直接用rank(pct=True)做百分位映射。另外注意 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、购买算 5),data 数组可以替换成权重值,不必都是 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, norm="l2", axis=1) # 每行是一个商品的特征向量 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一次算完所有商品两两之间的相似度。参数norm="l2"就是欧几里得范数。这里要注意,如果你直接用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_k=10): # 找到该用户在矩阵中的行索引 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_id=123 的用户推荐 10 件商品 reco = recommend_for_user(123, interact_csr, item_sim, user_ids.categories, item_ids.categories, top_k=10) 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 周训练、第 N+1 周验证这种滑窗方式,不要随机切分。随机切分在用户行为数据上本来就是错的,你拿随机切分的结果去预估线上表现,翻车是必然的。

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_k=10): 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(days=7) 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_k=10) hit_rate = compute_hit_rate(reco_dict, test_df, top_k=10) print("Hit Rate@10:", hit_rate)

这个评估设计里有两个细节值得说。第一,只评估验证集里有行为的用户,别把验证集里没出现的用户算进去,否则分母虚高。第二,top_k=10这个参数要和业务对齐——你的推荐位是 6 个还是 10 个,评估就用几。我见过有人推荐位只有 4 个,评估却用 Top-10 的命中率,线上自然对不上。

覆盖率也要算一下,公式是“推荐过的商品数量 / 全部商品数量”。覆盖率低于 5% 说明推荐只在少数爆款之间打转,长期来看用户体验会烂掉。还有一个指标叫“新颖度”,用推荐商品的平均热度倒数来衡量,计算方法就是推荐列表里的商品它们的全局交互次数的平均值的倒数。这个指标不用上线也能算,用来监控我们第 5.5 里说的“全是爆款”的问题。

做这个项目最大的教训是什么?我早期总是纠结于用什么高级模型,矩阵分解、FM、GNN 轮着上,结果折腾一星期,指标还不如 ItemCF 加时间衰减。推荐系统的起步阶段,数据清洗和特征工程带来的收益远大于模型升级。如果你手头只有一个电脑、一份 CSV,别急着上深度学习,先把行为事件切好、把用户画像算对、把相似度矩阵的稀疏化做好,这套流程本身就值回票价。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询