☰
从用户行为日志到推荐服务:数据挖掘与机器学习实战
2026/9/28 5:42:51 网站建设 项目流程

简介:这是一份面向数据挖掘与机器学习初学者的电商实战资料,围绕电子商务网站用户行为分析及服务推荐展开,帮助学习者把Python数据处理、可视化与建模能力应用到真实业务场景。压缩包共5个文件,以两个Jupyter Notebook代码为主体,辅以Excel数据表、SQL查询脚本与使用说明,整体仅79KB,结构紧凑,非常适合快速下载研读。代码覆盖NumPy数值计算、Pandas清洗聚合、数据可视化与探索性分析,并实现协同过滤、矩阵分解(SVD)等经典推荐算法,同时包含模型评估与参数调优思路;自带数据库查询文件与Excel原始数据,可配合代码动手实践,理解从数据清洗到推荐结果输出的完整流程。学习者既能获得可复用的代码框架与数据集,也能掌握电商场景下推荐系统的基本实现方法。已有419人学习,适合具备Python基础、希望快速入门数据挖掘实战的读者。

1. 数据挖掘和机器学习实战项目:从用户行为日志到推荐服务

拿到一个电子商务网站的用户行为日志,怎么把它变成推荐列表,这是数据挖掘和机器学习实战里最经典的一段流程。这份资料不是干讲算法的教程,而是直接把一份带数据的电商行为分析项目拆给你:里面有 7law.sql 这种数据库导出的原始数据,也有 .ipynb 的 Jupyter Notebook 脚本和配套代码目录。它的价值在于把数据清洗、探索性分析、推荐模型、评估指标四件事串成了一条完整链路,适合刚学完 Python 基础、想用 Pandas 和 sklearn 跑通真实数据集的读者,也适合手里有类似行为日志、想参考同行特征处理方式的一线数据分析师。看完这份项目,你能回答一个问题:一个用户从浏览到购买,中间到底经历了什么,系统又是依据哪些行为把他想要的商品排到前面的。

2. 环境准备和数据加载:先把 SQL 和 Excel 变成可分析的 DataFrame

2.1 解压后的文件清单:先认清四类资产

压缩包里的内容,表面看是 data、7law.sql、readme.txt、Untitled.ipynb、123.xls、code 这些文件和目录。我通常把它们分成四类:readme.txt 是说明文档,第一步先读它;7law.sql 是数据库导出的结构加数据,属于行为明细数据;123.xls 是另一份表格数据,一般是商品信息或补充行为数据;Untitled.ipynb 和 code 目录是代码资产。真正开始写代码之前,把每一类的格式和用途确认清楚,能避免后面一半时间在猜字段含义。

先用文本方式打开 readme.txt,再在命令行里看一下 7law.sql 的头部,确认它是不是 MySQL dump 文件,建的是哪几张表。这两步不用写 Python,但能让你对接下来的数据规模有数。常见的情况是,行为表有用户 id、商品 id、行为类型、时间戳四个核心字段,外加一张商品表存类目和品牌信息。导入前把表结构和字段类型都打印一遍,后面 Pandas 读取就不用来回试错。

代码环境建议用 conda 或 venv 单独建一个项目环境,别直接装进系统级的 Python。项目里既然带 ipynb,说明作者就是在 Jupyter 里跑的,你也应该先装好 Jupyter:

pip install pandas numpy matplotlib seaborn scikit-learn pymysql xlrd jupyter jupyter notebook

参数说明:pymysql 是用来连 MySQL 读 7law.sql 的驱动;xlrd 是读老 .xls 格式的引擎,新版 pandas 不内置,必须手动补。装完以后我把 Untitled.ipynb 重命名成自己的项目名——这种默认文件名第一次打开能跑,第二次你自己都分不清存的是哪个版本。

2.2 用 Pandas 加载 7law.sql 和 123.xls:两种数据源两种读法

7law.sql 是 MySQL 导出文件,推荐流程是:先用命令行把 sql 导入本地数据库,再用 pymysql 连上去读。不要试图用 Python 直接解析 sql 文件,投资回报率太低。导入命令一般是:

mysql -u root -p < 7law.sql

导入成功后,用下面的代码读行为表。如果你的 sql 里表名不叫 user_behavior,先去数据库里 show tables 看一眼再替换:

import pandas as pd import pymysql conn = pymysql.connect( host='localhost', # 本机 MySQL port=3306, # MySQL 默认端口 user='root', password='你的密码', database='ecommerce', # 7law.sql 导入后所在的库名 charset='utf8mb4' # 关键:指定 utf8mb4,避免中文乱码 ) df = pd.read_sql('SELECT * FROM user_behavior', conn) conn.close() print(df.shape) print(df.dtypes) print(df.head())

这段代码的作用是把整张行为表读进 DataFrame。charset 参数我每次都写 utf8mb4,因为老 MySQL 库默认可能是 latin1,一旦出现中文编码问题,后面重新导数据成本很高。read_sql 支持直接写 SQL 语句,所以你可以先在 SQL 里做过滤,比如只查最近 30 天,减少传输量。

123.xls 那份表格数据,读取方式完全不同。老格式 Excel 必须指定 xlrd 引擎,否则大概率报错:

df_items = pd.read_excel('123.xls', sheet_name=0, engine='xlrd') print(df_items.head()) print(df_items.columns.tolist())

参数说明:sheet_name=0 表示读第一个 sheet,也可以传 sheet 名;engine='xlrd' 强制用 xlrd 解析 .xls 老格式,新版 pandas 默认用 openpyxl 处理 .xlsx,但对 .xls 兼容性反而不如 xlrd 稳定。如果读进来中文列名乱码,多半是 Excel 文件本身的编码问题,第五章有排查方法。行为表和商品表读进来以后,下一步用 item_id 做关联,行为分析才有业务含义。

2.3 数据清洗:时间戳、重复行为和缺失值一起处理

电商行为数据最常见的问题有三类:重复记录、时间戳格式不统一、用户或商品字段缺失。先把这三件事处理掉,后面 EDA 和建模才不会反复返工。注意清洗不是把异常值删光,而是要记录清洗前后行数变化,心里有数。

# 1) 对同一用户、同一商品、同一行为、同一时间点去重 df = df.drop_duplicates( subset=['user_id', 'item_id', 'behavior_type', 'timestamp'] ) # 2) Unix 时间戳转成可读日期 df['datetime'] = pd.to_datetime(df['timestamp'], unit='s') df['date'] = df['datetime'].dt.date df['hour'] = df['datetime'].dt.hour # 3) 缺失值检查 print(df.isnull().sum()) df = df.dropna(subset=['user_id', 'item_id']) # 4) 基本异常值过滤 df = df[df['user_id'] > 0] df = df[df['item_id'] > 0]

说明:unit='s' 表示 timestamp 字段是以秒为单位的 Unix 时间戳,常见 10 位数字;如果原始值类似 1609459200000 这种 13 位数字,要把参数改成 unit='ms'。这一步没对齐,日期会整体偏移,是所有后续分析的第一个坑。dropna 只删掉 user_id 和 item_id 为空的记录,因为这两个字段没值就没办法做用户或商品维度的聚合。

清洗之后建议把结果落一份副本:

df.to_parquet('behavior_cleaned.parquet', index=False)

不推荐存 CSV,中文和数据类型容易二次出错;parquet 保留类型、压缩率高,后面反复读也不会撑爆内存。

3. EDA 和特征工程:把行为日志变成推荐模型的输入

3.1 用户活跃度和转化漏斗:三张图看清业务

在碰模型之前,先回答三个业务问题:用户每天活跃在哪些时段?从浏览到购买,每一步转化率是多少?哪些商品类目贡献了大部分购买?这三个问题决定了后面特征工程怎么做,也决定了推荐列表的排序目标。

import matplotlib.pyplot as plt import seaborn as sns # 图 1:按小时统计行为量 hourly = df.groupby('hour').size().reset_index(name='count') plt.figure(figsize=(8, 4)) sns.lineplot(data=hourly, x='hour', y='count') plt.title('按小时行为量分布') plt.show() # 图 2:行为转化漏斗 behavior_counts = df['behavior_type'].value_counts().reindex(['pv', 'fav', 'cart', 'buy']) plt.figure(figsize=(6, 4)) sns.barplot(x=behavior_counts.index, y=behavior_counts.values) plt.title('浏览-收藏-加购-购买漏斗') plt.show() # 图 3:Top 类目购买人数 top_cats = df_items.merge(df, on='item_id') \ .query('behavior_type == "buy"') \ .groupby('category')['user_id'].nunique() \ .sort_values(ascending=False).head(10) top_cats.plot(kind='bar') plt.title('Top 10 类目购买人数') plt.show()

逻辑说明:第一张图帮你判断流量高峰时段,特征工程里可以加「是否晚间活跃」这类特征;第二张图是漏斗,电商里 pv 到 buy 的转化一般在 1%~3%,如果明显偏高或偏低,要去核对数据口径;第三张图看类目集中度,如果购买集中在前 10 个类目,推荐排序时可以给这些类目更高权重。

参数说明:value_counts().reindex 是为了保证图上顺序是 pv、fav、cart、buy,而不是按出现次数乱排。groupby 之后用 nunique 统计购买人数,比 count 更能排除同一用户反复购买对类目活跃度的影响。

3.2 用户-商品行为矩阵:协同过滤的基础输入长什么样

推荐模型的输入不是长表格,而是矩阵。用户做行、商品做列、行为加权值做单元格。大多数推荐算法都吃这个结构,只是内部实现不同。先把行为映射成数值才是关键一步:

# 行为加权:浏览 1 分,收藏 1 分,加购 1 分,购买 2 分 behavior_score = {'pv': 1, 'fav': 1, 'cart': 1, 'buy': 2} df['score'] = df['behavior_type'].map(behavior_score) # user 行、item 列的稀疏矩阵 user_item = df.pivot_table( index='user_id', columns='item_id', values='score', aggfunc='sum', # 同一用户对同一商品多次行为,score 累加 fill_value=0 ) print(user_item.shape) # 稀疏度:非零元素占比 nonzero_ratio = (user_item > 0).sum().sum() / (user_item.shape[0] * user_item.shape[1]) print(f'稀疏度: {nonzero_ratio:.6f}')

逻辑说明:aggfunc='sum' 的含义是,如果同一个用户对同一个商品点击过 3 次又买了,那么这个格子得分就是 3×1+2=5。如果换成 max,则更多反映有没有发生过强行为事件,而不是次数。稀疏度这一行长什么样,直接决定后面用 ItemCF 还是 SVD,稀疏度低于万分之几是正常现象,不用慌。

参数说明:pivot_table 返回的 DataFrame 行数等于用户数,列数等于商品数。几万个用户乘几万个商品就是几亿个格子,这个矩阵在内存里会比较占地方,所以我一般先按「行为次数不低于 2 次」过滤一遍用户,把极少活跃的噪声用户去掉,矩阵能小一圈。

3.3 特征工程:把行为日志变成可输入模型的 Feature

从行为表构造用户画像特征,可以喂给逻辑回归、随机森林这类模型做购买预测,也可以直接拿来做人货匹配规则。这里的核心是聚合窗口的选择:用全量历史聚合容易产生数据泄漏,后面第四章评估一节会详细说明为什么。

user_feat = df.groupby('user_id').agg( total_actions=('behavior_type', 'count'), buys=('behavior_type', lambda x: (x == 'buy').sum()), carts=('behavior_type', lambda x: (x == 'cart').sum()), favorites=('behavior_type', lambda x: (x == 'fav').sum()), unique_items=('item_id', 'nunique'), last_active_hour=('hour', 'max'), active_days=('date', 'nunique') ).reset_index() user_feat['purchase_rate'] = user_feat['buys'] / user_feat['total_actions'] user_feat['cart_to_buy_rate'] = user_feat['buys'] / (user_feat['carts'] + 1e-6) user_feat.head()

这段的产出是用户维度特征表,每行一个用户,每列一个特征。purchase_rate 和 cart_to_buy_rate 都是比率类特征,加 1e-6 是防止除零报错。活跃天数 active_days 用来区分一次性用户和回头客,在很多商业推荐里,活跃用户数本身就应该作为人群分层的依据。特征构造好之后,再配合第四章的标签列,就能训练一个「是否购买」的分类模型,这也是摘要里提到的逻辑回归常见切入点。

4. 推荐模型与评估:ItemCF、SVD 和离线指标一次跑通

4.1 ItemCF 协同过滤:候选集怎么来

ItemCF 的基本假设是:用户对和自己之前买过的东西相似的商品感兴趣。相似不看商品属性,而看行为——两个商品经常被同一批用户交互,就认为是相似的。下面这段是完整可跑的 ItemCF 实现:

import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 转成 item-user 矩阵:行=商品,列=用户 item_user = user_item.T # shape (n_items, n_users) # 余弦相似度矩阵 item_sim = cosine_similarity(item_user, item_user) np.fill_diagonal(item_sim, 0) # 自己和自己不算相似 item_sim_df = pd.DataFrame(item_sim, index=item_user.index, columns=item_user.index)

逻辑说明:cosine_similarity 逐对计算商品向量,计算公式是点击交集除以两个向量的模长。两个商品相似度高,是因为它们的共同用户多、向量夹角小。np.fill_diagonal 把对角线上的 1 置 0,避免推荐出自己的 id。这里有一个容易被忽略的问题:如果某个商品只有一两个用户交互过,它的向量和谁都算不出相似度,这部分商品天然会被 ItemCF 忽略,第四章最后会讲怎么兜底。

接着给目标用户生成推荐列表:

def recommend_by_itemcf(user_id, top_n=10): interacted = user_item.loc[user_id] acted_items = interacted[interacted > 0].index.tolist() score = np.zeros(len(item_sim_df)) for item in acted_items: # 取出该商品与其他商品的相似度向量 score += item_sim_df[item].values # 已经交互过的商品不重复推荐 score[user_item.columns.get_indexer(acted_items)] = 0 top_idx = np.argsort(-score)[:top_n] return user_item.columns[top_idx].tolist()

逻辑说明:score 向量长度等于商品数,遍历用户交互过的商品,把这些商品在相似度矩阵里的行向量累加,得到的 score 表示候选商品与用户历史行为的总体相似度。复杂度是「交互商品数 × 商品总数」这个量级,几万商品可以忍受;如果是百万级商品,这段循环就要改成矩阵向量乘法,否则跑起来够你喝一壶的。给已经交互过的商品强制置 0,是推荐系统必做的防重复动作。

4.2 SVD 矩阵分解:给稀疏矩阵降维找隐藏向量

第二类方案是矩阵分解。SVD 把 user_item 矩阵拆成三个矩阵的乘积,再取前 k 个奇异值,就得到了用户向量和商品向量。原始矩阵里没观测到的格子,在降维后的向量空间中能获得预测值,这就是它比 ItemCF 更擅长发现「远亲」商品的原因。

from sklearn.decomposition import TruncatedSVD svd = TruncatedSVD(n_components=30, random_state=42) item_factors = svd.fit_transform(item_user) # 商品向量,shape (n_items, 30) # 预测行为分数用 inverse_transform 做近似回代 reconstructed = svd.inverse_transform(item_factors) # 对某个用户排序出 TopN user_id = 10086 user_row = user_item.loc[user_id].values pred = reconstructed.dot(user_row) # (n_items,) 预测分 pred[user_item.loc[user_id].values > 0] = 0 # 屏蔽已有交互 top_n = user_item.columns[np.argsort(-pred)[:10]] print(top_n)

n_components 是降维维度,经验取值 20 到 50。太小,向量表达不了多个兴趣主题;太大,降维失去意义,内存和训练时间也会涨。TruncatedSVD 和 PCA 的区别在于,PCA 要先减均值,而 TruncatedSVD 直接对稀疏矩阵做部分奇异值分解,所以更适合 user_item 这种大部分为 0 的矩阵。预测分数是通过重构矩阵点乘用户行为向量得到的,哪个商品得分高,实质是它的隐向量和用户历史行为的隐向量更匹配。

ItemCF 和 SVD 的取舍,我一般看两个点:如果业务方要求推荐理由可解释,说「因为你看过 A,所以推荐 A 的相似款」,就选 ItemCF;如果数据非常稀疏,而且更看重召回精度,就选 SVD。SVD 有个代价是结果不可解释,给业务宣讲时会被问「为什么推荐这个」,你要提前准备措辞。

4.3 模型评估:精确率、召回率、覆盖率一次算清

推荐系统的离线评估和分类模型有区别,不能只看准确率。购买行为在总体行为里占比极低,如果模型永远推荐热卖品,准确率也能很好看,但用户根本不买账。所以评估要同时看精确率、召回率和覆盖率:

# 按时间切分:最后一天做测试,之前做训练 test_date = df['date'].max() train_df = df[df['date'] < test_date] test_df = df[df['date'] == test_date] # 测试集里真正购买的商品作 ground truth test_buy = test_df[test_df['behavior_type'] == 'buy'] test_user_actual = test_buy.groupby('user_id')['item_id'].apply(set).to_dict() # 用 train_df 重算 user_item,再对测试用户推荐 top_u = list(test_user_actual.keys())[:200] precisions = [] recalls = [] for u in top_u: rec_list = recommend_by_itemcf(u, 10) act_set = test_user_actual[u] if not act_set: continue hit = len(set(rec_list) & act_set) precisions.append(hit / 10) recalls.append(hit / len(act_set)) print('平均精确率 Precision@10:', sum(precisions) / len(precisions)) print('平均召回率 Recall@10:', sum(recalls) / len(recalls))

参数说明:top_n=10 表示推荐列表长度,精确率是推荐列表里真正命中的比例,召回率是用户实际购买里有几个被推荐到。这两个指标一个看准不准,一个看全不全。注意这里必须按时间切分而不是随机切分,随机切分会把未来信息泄漏到训练集里的模型学到「答案」,线下指标虚高,上线立刻打回原形。

覆盖率也要算:

all_rec = [] for u in top_u: all_rec.extend(recommend_by_itemcf(u, 10)) coverage = len(set(all_rec)) / user_item.shape[1] print('商品覆盖率:', coverage)

覆盖率表示推荐列表覆盖了多少商品。如果覆盖率只有个位数百分比,说明推荐结果全压在少数爆款上,长尾商品完全没有曝光机会。电商场景覆盖率太低的系统,短期指标好看,长期会让流量越来越极端,品牌方的腰部商品没人理的。项目里如果还有逻辑回归做购买预测的代码,记得配套算一下 ROC 曲线,二分类问题只看准确率在这个场景里基本没有参考价值。

5. 常见问题与避坑:乱码、内存溢出与稀疏矩阵三个坑

5.1 Excel 和 SQL 读进来全是乱码

现象:用 read_excel 读 123.xls,中文列名变成乱码;用 read_sql 从 MySQL 读行为表,中文变成 ???。

原因:连接 MySQL 时没有指定 charset,或者导入 7law.sql 时表默认字符集不是 utf8mb4;老版 .xls 文件内部编码常常是 GBK,pandas 没做自动识别。

解决:MySQL 连接参数强制写 charset='utf8mb4';导入 sql 后先 show create table 检查表定义,发现有 latin1 就重新建表导一次。xls 文件用 engine='xlrd' 打开,如果还乱码,先读取原始字节用 chardet 检测编码再处理:

import chardet with open('123.xls', 'rb') as f: raw = f.read(1000) print(chardet.detect(raw))

这个检测结果告诉你文件实际编码,再在后续读取或转码时对症下药。别在同一个乱码上反复重跑,每次浪费时间半小时。

5.2 全表读入内存就爆

现象:notebook 执行 read_sql('SELECT * FROM user_behavior', conn),内核直接崩了,或者等很久才出结果。

原因:行为数据动辄几百万行,全字段全行加载;pandas 自动推断 dtype 会把 user_id 读成 int64,白白多用一倍内存。

解决:先在 SQL 里做列裁剪和时间过滤,再在 read_sql 时指定 dtypes:

df = pd.read_sql( 'SELECT user_id, item_id, behavior_type, timestamp FROM user_behavior', conn, dtype={'user_id': 'int32', 'item_id': 'int32'} )

int32 能撑到 21 亿,对用户 id 和商品 id 来说完全够用。聚合结果存 parquet,清洗阶段的中间结果不要反复读原表。

5.3 Unix 时间戳单位搞错,日期全乱

现象:datetime 转出来是 1970-01-01 附近的时间,或者发现日期整体差了一天。

原因:把毫秒时间戳当秒用了,或者反过来。10 位数字是秒,对应 unit='s';13 位数字一般是毫秒,对应 unit='ms'。

解决:拿到 timestamp 先看位数,别靠猜:

sample_ts = df['timestamp'].iloc[0] if sample_ts > 1e12: df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms') else: df['datetime'] = pd.to_datetime(df['timestamp'], unit='s')

这段逻辑放在数据清洗开头,能省掉后面所有时间相关分析的返工。这个坑我栽过一次后再也没跳过,属于典型的血泪经验。

5.4 稀疏矩阵算出来的相似度全是 0

现象:item_sim 矩阵里大部分值趋近于 0,cosine_similarity 输出一堆警告,推荐列表全是热门商品。

原因:用户、商品粒度太细,两个商品同时被同一用户交互的样本太少;全量行为矩阵太稀疏,相似度信号被噪声淹没。

解决:先过滤掉交互次数少于阈值的商品再算相似度:

item_cnt = df.groupby('item_id').size() valid_items = item_cnt[item_cnt >= 5].index df_filt = df[df['item_id'].isin(valid_items)]

阈值可以从 3 试到 10,看推荐结果覆盖面变化。如果过滤完还是稀疏,把 ItemCF 换成 SVD,SVD 对冷门物品更友好,因为向量空间是压缩过的,不会出现「分母为零」的尴尬。

5.5 只盯准确率,推荐结果却被用户无视

现象:离线报告里准确率 90% 以上,上了线点击率却很难看。

原因:行为数据极端不平衡,购买行为只占极小比例;准确率被多数类主导,模型学到的其实是「什么都不推也是对的」这个结论。

解决:离线评估把精确率、召回率、覆盖率放一起看,再加一道多样性检查,看推荐列表里是否只包含少数热门商品:

from collections import Counter rec_items = [item for u in top_u for item in recommend_by_itemcf(u, 10)] size_dist = Counter(rec_items).most_common(10) print('出现最多的 10 个商品占比:', sum(v for k, v in size_dist) / len(rec_items))

如果前 10 个商品占掉了一半推荐位,说明多样性出问题了,纯度太高的推荐会让人审美疲劳。加一个规则兜底,每个用户的推荐列表里至少保留两三个非热门商品,通常能改善体验。

6. 进阶验证:时间切分、冷启动兜底与推荐多样性

6.1 时间切分必须强制

做推荐模型的离线评估,时间切分不是可选项,是必须项。随机切分的问题在于,同一用户的行为被拆到训练集和测试集,模型在训练时已经见过测试集里的商品交互,评估自然虚高。正确做法是按日期排序,前 80% 时间窗口做训练,后 20% 做测试。如果项目里行为记录跨越了多个自然周,我倾向直接用最后一周做测试集,这样更能模拟线上真实场景,也能顺带看出推荐结果有没有明显的时效性偏差。

6.2 冷启动兜底策略

新用户没有历史行为,ItemCF 和 SVD 都算不出分数。这是推荐系统绕不开的冷启动问题。项目里常见做法是:对新用户直接推全局热榜,等他有了一次浏览行为再切换成个性化推荐。热榜可以按「购买人数 × 行为加权」来算,比如买过的人权重高、加购的人次之、点击的人再次之。另一条路是基于商品属性的召回,新商品进来时没有行为数据,就用它的类目、品牌去匹配老商品,这个效果通常比纯热榜好,但前提是商品维度表得干净,类目字段不能有大量空值。

6.3 我的验证习惯

我把离线评估跑完一圈之后,还会做一遍覆盖率和多样性检查,然后手动抽查十个测试用户的推荐列表,看看推荐理由是否说得通。Model 打分是一回事,推荐列表看起来是不是符合常识是另一回事——有些模型指标挺好,推荐出来的商品却是用户根本不感兴趣的品类。从那以后,我每次拿到电商行为数据集,都会强制走一遍时间切分 + 覆盖率 + 人工抽检这三个步骤,顺序固定的,不做完不算模型跑通。数据挖掘和机器学习实战最容易翻车的地方不在模型,而在数据口径和数据泄漏,希望你跑这个项目的时候,能少踩我当时踩过的那些坑。希望帮到你。

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

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

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

立即咨询