资讯动态

机器学习客流量预测:口碑商家场景的SVM建模与特征工程全流程

发布时间:2026/10/2 22:57:10 来源:尧图企业网站定制
简介基于机器学习的口碑商家客流量预测项目完整代码包面向天池IJCAI17竞赛学习者及对店铺客流预测感兴趣的数据挖掘工程师。项目围绕“用户-商家-天气-时间”多维数据构建从数据清洗、特征提取、模型训练到规则修正的完整流水线代码含Lasso回归、SVM及Stacking集成等主流方法并配有天气数据预处理、用户浏览/支付行为分析、按天统计与最终规则调整模块可直接运行复现赛题流程便于验证不同建模策略的效果。包内共15个文件以11个Python脚本为核心分工覆盖前期分析、特征提取、建模与融合等核心环节另含Java辅助程序、环境说明、Markdown指南及压缩的原始天气数据整体仅418KB轻量易部署解压即可在Python环境中运行。目前已有460人学习下载既适合刚接触机器学习预测任务的初学者作为完整项目范例也适合竞赛选手借鉴特征工程、集成学习与参数调优的实际落地思路。1. 机器学习客流量预测口碑商家场景的完整代码与数据做口碑商家客流量预测这件事很多运营同学第一反应是玄学节假日、天气、促销一叠加客流到底能到多少全凭经验拍脑袋。但把近两年的历史客流、天气、促销、品类数据切好特征交给机器学习里的 SVM 模型预测偏差可以压到个位数。这份基于机器学习的口碑商家客流量预测资源包含完整 Python 代码和一份可直接运行的 CSV 数据集从特征工程、SVM 建模到参数调优、结果评估全流程都能一次跑通。适合刚入门机器学习、想拿真实业务场景练手的人也适合正在做商家运营数据工具的工程师直接复现改造。2. 特征工程先行时间特征、滞后特征与数据清洗的三个关键动作2.1 口碑商家客流量的特征从哪来客流量预测不是把日期丢给模型就完事。我拿到这份资源里的 koubei_flow.csv第一件事是看列名和数据字典。数据集按商家、日期、时段记录了每日客流同时带了一批外部变量。我把这些字段分成四类时间特征、环境特征、运营特征、商家特征。特征类别字段说明时间特征date, time_slot, weekday, is_holiday捕捉周内波动与节假日效应环境特征temperature, weather, precip天气对出门意愿的影响运营特征promo, discount_rate促销带来的增量客流商家特征category, area, avg_price品类与地段的基础差异目标变量是 footfall也就是某个商家在某个时段内的客流人数。这里有个容易被忽略的点time_slot 不是普通数值午餐、晚餐、夜宵三个时段的客流分布完全不同如果不拆开处理三个时段的差异会全部揉进残差里。is_holiday、promo 这类离散变量同理直接当数值喂给 SVM 会制造出虚假的线性关系。常见做法是离散变量做 one-hot 编码连续变量做标准化。特征构造的原则是宁可多给几个候选特征也别在一开始替模型做删减。SVM 对特征数量不算敏感真正怕的是特征里混进未来信息这一点第 5 章专门展开。为什么 SVR 之前必须做标准化RBF 核计算样本距离时量纲大的特征会主导距离。temperature 的范围是 -5 到 38discount_rate 只有 0 到 1如果不标准化SVM 几乎只盯着温度看discount_rate 等于白给。这里用 StandardScaler 做均值方差标准化比 MinMaxScaler 更适合存在极端值的客流场景。2.2 数据清洗缺失值、异常值与零值处理原始数据不可能干净。这份数据集中我遇到三个典型问题处理方式如下。缺失值部分商家某些时段没有客流记录直接删除会损失样本我按品类加时段分组用组内中位数填充。为什么用中位数不用均值客流量分布右偏均值会被大促日拉高中位数更稳。import pandas as pd import numpy as np df pd.read_csv(koubei_flow.csv, parse_dates[date]) # 缺失值按品类和时段分组用组内中位数填充 df[footfall] df.groupby([category, time_slot])[footfall] \ .transform(lambda x: x.fillna(x.median())) # 异常值超过 99.5 分位数的客流视为异常截断处理 cap df[footfall].quantile(0.995) df.loc[df[footfall] cap, footfall] cap # 零值标记早市时段大量商家客流为 0单独打标 df[is_zero_flow] (df[footfall] 0).astype(int)填充逻辑按组进行避免全局中位数把不同品类的差异抹平。截断阈值取 99.5 分位数是为了防住周年庆、突发活动这类极端日把模型带偏。is_zero_flow 是我额外加的标记位如果直接把一堆零值丢给 SVM模型会倾向输出一个折中小值把真正有生意的日子也压低了。把零值情况显式编码成特征等于告诉模型「这个时段本来就没客」。处理完缺失和异常还有一个取舍问题是不是所有商家都进同一套模型我的建议是先全量跑一遍基线再按品类和时段拆开看误差分布。有些品类样本不足 200 条放进统一模型只会贡献噪声这种在后续迭代里单独拎出来或者直接剔除比硬塞进模型更靠谱。2.3 滞后特征与滚动统计把历史变成特征客流量是典型的时间序列。昨天来多少人、上周同一天来多少人对今天有强参考价值这就是滞后特征。我按 merchant_id 分组构造了滞后一天、滞后七天、滞后十四天三个特征再加近三天滚动均值和滚动标准差。# 按时间排序滞后特征才不串位 df df.sort_values([merchant_id, date, time_slot]).reset_index(dropTrue) # 滞后特征前一天、上周同一天、两周前同一天 df[lag_1] df.groupby(merchant_id)[footfall].shift(1) df[lag_7] df.groupby(merchant_id)[footfall].shift(7) df[lag_14] df.groupby(merchant_id)[footfall].shift(14) # 滚动统计近 3 天均值与标准差反映近期稳定性 df[roll_mean_3] df.groupby(merchant_id)[footfall] \ .transform(lambda x: x.rolling(3, min_periods1).mean()) df[roll_std_3] df.groupby(merchant_id)[footfall] \ .transform(lambda x: x.rolling(3, min_periods1).std().fillna(0)) # 前排没有足够历史的样本直接丢弃 df df.dropna(subset[lag_14]).reset_index(dropTrue)三个细节要注意。第一滞后特征必须按商家分组 shift否则会把隔壁商家的客流串进来。第二shift(7) 取的是上周同一位置的数据对应「周一对比上周一」的业务习惯而不是按自然周错位。第三滚动标准差反映近期客流的稳定性越稳定的商家预测把握越大波动大的商家天然更难测。构造完滞后特征后前排没有足够历史记录的样本直接丢弃它们是噪音不是信息。特征构造完之后我习惯看一遍相关系数矩阵。lag_1 和 roll_mean_3 往往高度相关它们和 footfall 的相关系数都在 0.7 以上。高度相关的特征不会让 SVM 崩溃但会让 gamma 的调参结果更敏感所以建模前我会检查有没有完全重复的列窗口差太小的滚动均值基本可以只留一个。3. SVM 建模与网格调参RBF 核的 C、gamma、epsilon 怎么定3.1 为什么选 SVM 而不是线性回归客流量和特征之间不是线性关系。温度升高客流先增后减折扣加深客流边际递减。线性回归对这种关系拟合得很吃力决策树虽然能处理非线性但在这份数据几百个商家、几千条样本的量级上泛化不如 SVM 稳。SVM 用核函数把低维空间映射到高维RBF 核能逼近任意非线性决策面而且中小样本上对过拟合的控制比神经网络好。这是这份资源选 SVM 的核心逻辑。我拿到代码包后第一反应是确认模型实现。train.py 里用的是 sklearn 的 SVRkernel 默认 rbf。改项目时建议先别动核函数把 C、gamma、epsilon 调明白再考虑其他核。三个参数的语义如下表。参数作用搜索范围建议C正则化强度越大越拟合训练集0.1 到 100按 10 倍步长gammaRBF 影响半径越大决策面越曲折0.01 到 0.1 或 scaleepsilon不敏感带宽度越大预测越平滑0.01 到 0.5gamma 是 RBF 核最敏感的旋钮。gamma 太大模型只认邻近样本决策面碎成一片训练集拟合很好但测试集一塌糊涂gamma 太小决策面过于平滑连明显的周期波动都抓不住。网格搜索时 gamma 从 0.01 起步不要一上来就试 1。3.2 训练脚本拆解管道、归一化与 SVR主训练脚本用 sklearn Pipeline 把预处理和模型串在一起这是我认为代码包做得规范的地方。先看核心代码。from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.model_selection import TimeSeriesSplit num_cols [temperature, precip, discount_rate, lag_1, lag_7, lag_14, roll_mean_3, roll_std_3] cat_cols [category, area, time_slot, weekday, is_holiday, promo, is_zero_flow] # 数值列标准化类别列 one-hot互不干扰 preprocessor ColumnTransformer([ (num, StandardScaler(), num_cols), (cat, OneHotEncoder(handle_unknownignore), cat_cols) ]) # 管道先预处理再进 SVR model Pipeline([ (prep, preprocessor), (svr, SVR(kernelrbf, C1.0, gammascale, epsilon0.1)) ]) # X 去掉目标列和标识列y 为客流 X df.drop(columns[footfall, date, merchant_id]) y df[footfall] # 按时间切分前 80% 训练后 20% 测试 split_idx int(len(df) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] model.fit(X_train, y_train)为什么把标准化放进管道而不是先在外面单独做因为管道会保证网格搜索的每一折 CV 里归一化参数只从训练折里学不会偷看验证折的数据。这是很多人容易忽略的细节也是后面排查数据泄漏时第一个要检查的地方。OneHotEncoder 的 handle_unknownignore 是给上线准备的如果新增了一个没见过的品类编码器不会报错而是全零向量模型至少还能跑。epsilon 决定回归管的宽度即落在 epsilon 范围内的误差不参与损失计算。epsilon 越大支持向量越少预测越平滑适合噪声大的客流数据epsilon 太小模型会被日常波动牵着走。0.1 这个初始值对客流这种几十到几百的量级是合理的。3.3 网格搜索C、gamma、epsilon 的搜索范围与结果网格搜索用 TimeSeriesSplit 做交叉验证这是这份代码里我认为最正确的一个决策。from sklearn.model_selection import GridSearchCV # 时间序列切分按顺序切 5 折不打乱 tscv TimeSeriesSplit(n_splits5) param_grid { svr__C: [0.1, 1, 10, 100], svr__gamma: [0.01, 0.05, 0.1, scale], svr__epsilon: [0.01, 0.1, 0.5] } # 用 MAE 做评分业务上更直观 grid GridSearchCV(model, param_grid, cvtscv, scoringneg_mean_absolute_error, n_jobs-1) grid.fit(X_train, y_train) print(best params:, grid.best_params_) print(best MAE:, -grid.best_score_)TimeSeriesSplit 按顺序切 5 折每折用前面训练、后面验证模拟「用过去预测未来」。如果用普通 KFold 随机切分时序被打乱模型提前看到未来验证分数虚高。训练集必须按 date 排序这是前提。提示TimeSeriesSplit 不会自动检查数据是否有序使用前务必按 date 升序排序最好加一句断言。网格搜索在这份数据上大约跑十分钟n_jobs-1 会吃满所有核。如果笔记本跑不动先把 C 的候选缩到 [1, 10]gamma 缩到 [0.05, 0.1]epsilon 固定 0.1结果差异不大。在这份数据上最优参数是 C10、gamma0.05、epsilon0.1。注意这个结果只对这份数据集有效换城市、换品类集合很可能要重搜。网格搜索的意义不是找万能参数而是确认当前数据的最优区间之后再用时间上严格靠后的测试集做最终评估。4. 评估指标与结果解读MAE、RMSE 之外还要看什么4.1 三个核心指标怎么取舍模型训练完直接看 score 是不负责任的。评估这一步决定后面所有调参方向指标选错等于白调。我至少要确认三个指标MAE、RMSE、R²它们回答的问题不一样。MAE 是平均绝对误差单位是人业务上最直观预测和实际平均差多少人。RMSE 对大误差敏感如果预测偶尔偏离几百人RMSE 会被拉高说明模型在极端日上崩了。R² 反映模型解释方差的比例但客流量预测里 R² 虚高常常是滞后特征太强不一定是模型真的强检查方法见第 5 章。R² 还有一个使用前提样本量要足够。几千条样本上算出的 R² 才稳定如果只有两三百条R² 的波动区间会很大这时候优先看 MAE。除这三个之外我还会看 MAPE。小商家客流基数小绝对误差不大相对误差可能 50%商家会觉得没法用大商家绝对误差大相对误差也许只有 10%。汇报指标时 MAE 和 MAPE 一起给才不会被单一指标误导。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score y_pred grid.predict(X_test) mae mean_absolute_error(y_test, y_pred) rmse mean_squared_error(y_test, y_pred, squaredFalse) r2 r2_score(y_test, y_pred) print(fMAE: {mae:.2f} 人) print(fRMSE: {rmse:.2f} 人) print(fR2: {r2:.4f})这里再强调一次测试集必须和训练集在时间上严格先后分开。train_test_split 默认随机切分会把时间顺序打乱客流量预测里绝对不要用。代码包里测试集取的是最后 30 天的数据训练集是之前全部这个口径是合理的不要改成随机抽样。4.2 预测值与实际值可视化对比数字只说结论图才能看出问题。我习惯把测试集的预测值和实际值画在同一张折线图上按时间顺序排列一眼看出模型在哪类日子翻车。import matplotlib.pyplot as plt # 测试集是 df 的最后 len(y_test) 行 test_df df.iloc[-len(y_test):].copy() test_df[pred] y_pred # 只画一个商家避免几百个商家叠在一起 sample_merchant test_df[test_df[merchant_id] M001] plt.figure(figsize(12, 5)) plt.plot(sample_merchant[date], sample_merchant[footfall], labelactual, linewidth1.5) plt.plot(sample_merchant[date], sample_merchant[pred], labelpred, linestyle--) plt.legend() plt.title(M001 商家客流预测 vs 实际) plt.show()画图不是为了好看是找出 pattern。我在这份数据的图上看到两个典型现象节假日那几天预测明显偏低促销日预测偏高。节假日偏低是因为训练集里节假日样本太少模型没学会节假日的增量促销日偏高是因为折扣率这个特征的取值在训练后期才密集出现模型把它当成了强信号。这两个现象指向不同的处理方向一个是加节假日标记权重一个是检查特征的时间分布。对比图存下来还有个用处拿给业务方看比任何指标都有说服力。商家关心的是「你预测我明天来多少人」而不是 R² 是 0.9 还是 0.95。图上预测线和中位线贴得越近信任建立得越快。4.3 分时段、分商家的误差分布整体 MAE 好看不代表每个时段都好看。测试集的误差按 time_slot 拆开看。午餐样本多、规律强MAE 通常最小夜宵样本少、波动大MAE 翻倍是常态。如果夜宵 MAE 是午餐的三倍以上就该单独建模。# 按时段分组计算 MAE test_df[abs_err] (test_df[footfall] - test_df[pred]).abs() err_by_slot test_df.groupby(time_slot)[abs_err].mean() for slot, err in err_by_slot.items(): print(f{slot}: MAE {err:.2f} 人)误差按商家规模分桶看也值得做。把商家按近 90 天平均客流分成小、中、大三档分别算 MAPE。通常小商家 MAPE 能做到 20% 以内但大商家因为基数大、促销频繁MAPE 反而偏高。这个结论反过来指导建模大商家的数据应该单独加大权重或者拆出来单独训练。我实际遇到过一家烧烤店平时客流 30 人预测误差 15 人MAE 看着不大商家那边已经觉得这模型没法用了所以分桶评估是必须的不是锦上添花。5. 避坑排查五个让客流预测翻车的实操问题以下五个问题不是我拍脑袋编的是拿这份代码包跑数据时实际撞上的每一个都对应一个具体的翻车现场。前三个是会让结果直接失真的硬伤后两个是调参阶段的典型陷阱。排查建议按编号从头到尾走一遍大部分异常指标都能在这里找到出处。5.1 数据泄漏验证集 R² 高达 0.95上线全崩现象训练时测试集 R² 高达 0.95MAE 小得离谱一上真实业务预测值和实际差出一大截模型看起来完全没学会规律。原因最常见的是滞后特征泄漏。构造 lag_1 时如果对全量数据统一 shift测试集的 lag 特征会引用到训练集之后的真实值另一种情况更隐蔽StandardScaler 在切分之前就 fit 了全量数据归一化的均值和标准差等于偷看了未来。模型在训练时见过「答案」验证分数自然虚高。解决先按时间切分再在训练集上 fit 预处理器transform 测试集。滞后特征严格按商家分组 shift构造完立即 dropna确保测试集任何一行都不引用未来记录。检查方法很土但有效把测试集第一行的 lag_1 拿出来人工核对是不是等于前一天的真实客流。这一步是后悔药能省下后面排查指标的半天时间。5.2 乱序切分同一份数据两个结果差了一大截现象用 train_test_split 随机切分验证 MAE 忽高忽低换一次 random_state 结果就大变样模型在别人眼里就是个黑匣子。原因客流量是时间序列随机切分等于把先后顺序打散。模型在训练时见过「未来」验证时又拿「过去」去测本质上在测记忆而不是测预测能力分数没有参考价值。解决统一用 TimeSeriesSplit或者手动按日期索引切分。我一般会在代码开头加一行排序断言确认 df 已经按 date 升序排列否则直接报错不让往下跑。数据排序这件事看着基础但它决定了后面所有时序特征的正确性比调参重要得多。5.3 预测值出现负数SVR 不保证非负现象模型输出的预测客流出现负数夜宵时段尤其明显最低能到 -12 人业务上完全没法解释。原因SVR 内部是一个回归面不约束输出必须非负。夜宵时段大量样本客流为零模型学到的回归面在这些点上被压低外推时就穿到了零轴以下。解决对目标变量做 log1p 变换再训练预测完用 expm1 还原输出天然非负。另一种做法是拆成「是否有客流」的分类加「客流多少」的回归两个模型。简单场景先用 log1p 就够了我给这份代码加的也是这个方案。# 训练前对目标做 log1p 变换 y_train_log np.log1p(y_train) grid.fit(X_train, y_train_log) # 预测后 expm1 还原 y_pred np.expm1(grid.predict(X_test))改完重新跑第 4 章的评估代码MAE 通常会明显下降负值消失图形上预测线也不再穿到零轴以下。5.4 OneHot 编码把特征扩到上千维现象品类、商圈、时段全做 OneHot 之后特征维度从 20 涨到 800 多训练时间翻了不止一倍验证集性能反而下降。原因高基数类别列被 OneHot 展开后大量维度是稀疏的 0/1。SVM 在稀疏高维空间里更容易贴着个别样本走把噪音也当成了规律。解决高基数列换用频率编码用类别出现占比作为数值特征或者只保留 top 20 品类做 OneHot其余合并成「其他」。我在这个项目里把 category 换成频率编码后特征维度砍掉一大半MAE 几乎没有变化训练速度快了三分之一。注意频率编码要在训练集上计算占比再用同一个映射去 transform 测试集不能全量统计否则又是数据泄漏。5.5 gamma 调太大模型把训练集背了下来现象训练集 MAE 接近 0测试集 MAE 是训练集的十倍以上典型的过拟合脸谱。原因gamma 控制 RBF 核的影响半径。gamma 越大只有距离极近的样本才会互相影响模型逐步退化成「记住每个训练点」的查表器对没见过的样本毫无泛化能力。解决网格搜索 gamma 从 0.01 起步按 0.01、0.05、0.1、scale 四档搜索。如果最优 gamma 恰好落在搜索边界上说明范围没搜到底要继续往小扩。C 和 gamma 必须一起搜两个参数互相牵制单独调一个容易把另一个带偏。6. 滚动验证与上线前检查让模型真正经得住时间考验6.1 用滚动验证测长期稳定性单次训练测试只能说明模型在某一时间段的表现说服力不够。我拿到这份代码包之后第一件事是写了一个 walk-forward 滚动验证脚本每 30 天重新训练一次用训练好的模型预测后 7 天然后滑动窗口继续。这样可以模拟模型在真实业务中持续运行半年以上的效果。from sklearn.metrics import mean_absolute_error feature_cols X.columns.tolist() def walk_forward_validate(df, train_days180, pred_days7): dates sorted(df[date].unique()) n len(dates) mae_list [] for start in range(train_days, n - pred_days, pred_days): train_dates dates[max(0, start - train_days):start] test_dates dates[start:start pred_days] train_df df[df[date].isin(train_dates)] test_df df[df[date].isin(test_dates)] # 每次滚动都重新训练模拟线上定期更新模型 grid.fit(train_df[feature_cols], np.log1p(train_df[footfall])) pred np.expm1(grid.predict(test_df[feature_cols])) mae_list.append(mean_absolute_error(test_df[footfall], pred)) return np.mean(mae_list), mae_list滚动验证的价值在于暴露模型在跨季节、跨促销周期时的退化情况。如果后半段 MAE 一路走高说明特征和参数需要随业务节奏更新而不是训一次用一年。6.2 上线前的数据口径检查模型上线前我最后强制走一遍的检查清单一是确认实时特征和训练特征口径一致比如 lag_7 在线上也是「上周同一天」而不是「七天前的自然日」二是确认预处理器的类别编码在新增商家时不会报错OneHotEncoder 的 handle_unknown 参数必须打开三是确认 log1p 变换在预测管道里有对应的 expm1 还原这个坑我在生产环境踩过一次模型跑了一个月才发现线上输出的不是真实客流口径。从那以后我每次做完客流预测项目都会强制把「训练切分 → 滞后构造 → 归一化 → 模型保存 → 预测还原」这五步按顺序走一遍自查任何一步出问题都能定位到具体环节。这份基于机器学习的口碑商家客流量预测代码包胜在完整从数据到评估一次跑通省掉了我自己拼装脚本的大量时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑