机器学习客流预测实战:时间序列特征工程与SVM调参避坑指南
发布时间:2026/11/2 15:45:40 锦皓数字建站

简介面向天池IJCAI17口碑商家客流量预测赛题的完整代码与数据包适合参加数据挖掘竞赛、做机器学习课程设计或练习特征工程与模型融合的读者使用。压缩包共15个文件约418KB以11个Python脚本为主体配有Java实现、Markdown说明、txt与gz格式的天气辅助数据覆盖数据预处理、用户支付与浏览行为分析、时间窗口统计、天气好坏判定、特征抽取、模型训练融合及后处理规则调整等环节。已有463人学习下载说明该方案在同类赛题复现中具有一定参考价值。读者可据此了解从原始行为数据到客流量预测的完整流程借鉴Lasso、stacking等建模思路和脚本组织方式并直接运行代码进行结果复现与二次修改适合作为竞赛方案参考和机器学习项目练习素材。1. 从一份“跑不起来”的预测作业说起带过几届做机器学习课设的学生之后我发现一个挺反直觉的现象真正卡住进度的往往不是模型选型而是数据管道和特征构造。有位同学拿着“基于机器学习的口碑商家客流量预测”这个题目找到我代码里 SVM 写得很标准sklearn.svm.SVR调得也不差可程序一跑就报错要么是日期列没法转成数值要么是训练集和测试集按随机方式切分之后 R² 高得离谱、换个时间窗口就崩盘。客流量预测属于典型的时间序列回归问题商家口碑评分、历史客流、节假日、天气这些字段之间的耦合关系决定了它不能按普通表格回归的套路照搬。这份完整代码数据方案要解决的就是把“读数据—做特征—训模型—出预测—能复现”这条链路一次性走通适合正在交课程设计、做毕业设计或者要给门店做一套客流排班参考的读者。2. 需求拆解与数据管道从原始表到可训练样本的四步流水线2.1 先明确预测目标和评价口径动手写代码之前得先把“预测什么”钉死。客流量预测常见的口径有两种一种是预测某商家在未来某一天的到店人数属于按天粒度的回归另一种是预测未来某时段比如午市、晚市的客流区间。这份方案按按天粒度回归来做目标变量取每日客流计数评价指标主用 RMSE 和 MAE辅以 R² 看整体拟合程度。为什么不用准确率因为客流是连续数值分类指标在这里没有意义这一点在答辩时经常被追问提前想清楚能省不少事。数据管道的设计目标很明确输入一张原始业务表输出一份列名规范、时间有序、无缺失的建模样本。通常的做法是拆成四步——字段清洗、时间对齐、缺失处理、划分数据集。四步分开写成函数不要糊在一个main里后面调参和复现都要靠这个结构撑着。2.2 字段清洗与时间对齐的实现原始表里最容易翻车的是日期字段。它可能是2023/6/1、2023-06-01、甚至带时分秒的字符串直接pd.to_datetime有时会静默解析成错误格式而不报错。血泪经验是加errorscoerce再检查空值数量异常日期会变成 NaT方便定位。import pandas as pd import numpy as np def clean_and_align(df: pd.DataFrame, date_col: str date) - pd.DataFrame: 清洗日期字段并按天对齐返回按时间升序排列的 DataFrame df df.copy() # 强制解析非法日期转为 NaT避免静默错位 df[date_col] pd.to_datetime(df[date_col], errorscoerce) bad_rows df[date_col].isna().sum() if bad_rows 0: print(f发现 {bad_rows} 行日期无法解析已剔除) df df.dropna(subset[date_col]) # 统一到天粒度去掉时分秒对聚合的干扰 df[date_col] df[date_col].dt.normalize() # 同一天多条的按均值聚合避免重复日期导致索引冲突 num_cols df.select_dtypes(include[np.number]).columns.tolist() df df.groupby(date_col, as_indexFalse)[num_cols].mean() return df.sort_values(date_col).reset_index(dropTrue)这段代码的关键点有三个errorscoerce负责把脏日期暴露出来而不是悄悄吞掉dt.normalize()把时间戳压到天避免同一天出现多个时间点groupby聚合解决重复日期这一步在真实业务表里几乎必做因为同一商家同一天可能有多条记录。参数上date_col做成可配置是因为不同来源的表列名不一致硬编码会在换数据时直接崩。2.3 缺失值处理与异常值截断客流数据缺失有两种来源一种是商家当天没营业另一种是采集系统漏了。这两种情况处理方式完全不同混在一起填 0 会让模型学到错误规律。我的处理习惯是先用营业标记列筛掉闭店日剩下的缺失按前后 7 天滚动中位数填补比均值更抗异常。def fill_missing(df: pd.DataFrame, target: str, window: int 7) - pd.DataFrame: 对目标列做时间感知的缺失填补并截断极端异常值 df df.copy() # 用前后窗口的中位数填补比全局均值更贴近局部趋势 rolling_med df[target].rolling(windowwindow, min_periods1, centerTrue).median() df[target] df[target].fillna(rolling_med) # 用 1% 和 99% 分位截断防止极端值带偏 SVR 的间隔 low, high df[target].quantile([0.01, 0.99]) df[target] df[target].clip(lowerlow, upperhigh) return dfcenterTrue让窗口以当前点为中心避免只用历史值造成填补结果滞后。分位截断这一步对 SVM 尤其重要因为 SVR 对离群点敏感一个极端高峰值就能把回归面拽偏后面第 5 章还会专门讲这个坑。2.4 数据集划分为什么不能随机切时间序列预测最常见的错误就是train_test_split(shuffleTrue)。随机打乱之后模型在训练时见过“未来”的样本测试集指标会好看得离谱上线就原形毕露。正确做法是按时间顺序切分前 80% 作训练后 20% 作测试如果要做交叉验证用TimeSeriesSplit而不是 KFold。这份代码里我一般会把切分逻辑单独抽出来保证训练集的时间最大值严格小于测试集的时间最小值答辩时这一条能直接讲出设计考量。3. 特征工程把日期、口碑和滞后信息变成模型能吃的数值3.1 时间特征的构造与周期编码客流量最直接的驱动因素就是时间。星期几、是否周末、是否节假日、月份这些字段原始表里通常没有得自己造。星期几不能直接填 1 到 7因为模型会误以为周日和周一差距很大正确做法是正弦余弦编码把周期性映射到单位圆上。def add_time_features(df: pd.DataFrame, date_col: str date) - pd.DataFrame: df df.copy() df[weekday] df[date_col].dt.weekday # 0周一 df[is_weekend] (df[weekday] 5).astype(int) df[month] df[date_col].dt.month df[day_of_month] df[date_col].dt.day # 周期编码让周一和周日在一维上相邻而不是首尾割裂 df[weekday_sin] np.sin(2 * np.pi * df[weekday] / 7) df[weekday_cos] np.cos(2 * np.pi * df[weekday] / 7) df[month_sin] np.sin(2 * np.pi * df[month] / 12) df[month_cos] np.cos(2 * np.pi * df[month] / 12) return df原始weekday列保留没关系树模型能自己分裂但 SVR 这类基于距离的模型对周期编码更友好。两套特征都留着后面模型对比时可以看哪套在验证集上更稳。节假日字段如果表里没有用简化的“是否法定假日”二值列代替即可不必引入外部日历库代码可移植性更重要。3.2 口碑评分的分箱与交互特征题目里“口碑商家”四个字不是装饰口碑评分对客流有滞后影响评分高的店客流上升但可能隔几天才体现。通常的做法是把评分做两件事一是分箱把连续评分切成“低/中/高”三档降低噪声二是造交互项比如“评分 × 是否周末”捕捉周末高评分店更吸客的现象。def add_reputation_features(df: pd.DataFrame, score_col: str score) - pd.DataFrame: df df.copy() # 分箱边界按业务经验固定避免用测试集分位数造成信息泄漏 bins [0, 3.5, 4.2, 5.0] labels [0, 1, 2] df[score_bin] pd.cut(df[score_col], binsbins, labelslabels).astype(int) # 交互特征高口碑在周末是否带来额外客流 df[score_weekend] df[score_col] * df[is_weekend] return df分箱边界写成固定值而不是qcut是因为分位数依赖数据分布训练集和测试集分位数不同会导致同一评分被分到不同箱属于隐蔽的数据泄漏这个坑在第 5 章会展开。3.3 滞后特征与滚动统计让模型看到历史趋势客流量有强自相关性昨天的客流对今天有解释力。滞后特征通常取 1 天、7 天、14 天三个跨度分别对应短期惯性、周周期和双周趋势。滚动统计取 7 天滑动均值和标准差前者刻画基线水平后者刻画波动程度。def add_lag_features(df: pd.DataFrame, target: str) - pd.DataFrame: df df.copy() # shift(1) 确保预测当天时只使用前一天及更早的信息 for lag in [1, 7, 14]: df[flag_{lag}] df[target].shift(lag) # 滚动窗口基于 shift(1) 后的序列避免把当天值算进去 df[roll_mean_7] df[target].shift(1).rolling(7).mean() df[roll_std_7] df[target].shift(1).rolling(7).std() # 生成后必然产生前若干行空值直接丢弃 return df.dropna().reset_index(dropTrue)这段逻辑里shift(1)是生命线。如果写成rolling(7).mean()而不先 shift窗口里就包含了当天的目标值模型等于在偷看答案训练指标漂亮但线上毫无意义。滞后阶数不是越多越好14 天之后边际收益骤降还会拉长有效样本的起始时间数据量小的商家可能直接不够训练。3.4 特征组装与标准化顺序所有特征函数按顺序串起来形成一条可复现的管道。标准化放在特征组装之后、模型训练之前而且必须用训练集的均值方差去变换测试集不能各自标准化。from sklearn.preprocessing import StandardScaler def build_features(raw: pd.DataFrame, target: str traffic) - tuple: df clean_and_align(raw) df fill_missing(df, target) df add_time_features(df) df add_reputation_features(df) df add_lag_features(df, target) feature_cols [c for c in df.columns if c not in (date, target)] # 按时间切分绝不 shuffle split int(len(df) * 0.8) train, test df.iloc[:split], df.iloc[split:] scaler StandardScaler().fit(train[feature_cols]) # 只在训练集上拟合 X_train scaler.transform(train[feature_cols]) X_test scaler.transform(test[feature_cols]) return X_train, train[target].values, X_test, test[target].values, feature_cols参数0.8是训练占比数据量少于一年时可以调到 0.7 留更多测试样本。feature_cols排除日期列和目标列防止把字符串或标签直接喂进模型。这套组装函数返回标准化后的数组和特征名后面的模型对比和重要性分析都基于它改一处全链路生效。4. 模型对比与 SVM 调参为什么最终选了 RBF 核4.1 基线模型对比先跑通再调优不要一上来就怼 SVM。我的习惯是先铺三个基线线性回归看线性可解释性决策树看非线性分裂能力随机森林看集成效果。三个基线跑完心里对数据的线性程度和噪声水平就有数了再决定 SVM 的核函数和参数搜索范围。from sklearn.linear_model import LinearRegression from sklearn.tree import DecisionTreeRegressor from sklearn.ensemble import RandomForestRegressor from sklearn.svm import SVR from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score import numpy as np def evaluate(model, X_tr, y_tr, X_te, y_te, name: str): model.fit(X_tr, y_tr) pred model.predict(X_te) rmse np.sqrt(mean_squared_error(y_te, pred)) mae mean_absolute_error(y_te, pred) r2 r2_score(y_te, pred) print(f{name:16s} RMSE{rmse:8.2f} MAE{mae:7.2f} R2{r2:6.3f}) return {name: name, rmse: rmse, mae: mae, r2: r2}这段评估函数统一了指标口径四个模型用同一份切分数据跑结果才可比。RMSE 对大误差更敏感MAE 更贴近日常排班决策的实际偏差两个都报出来比只报 R² 有说服力。基线对比表建议直接放进答辩 PPT一眼能看出选型不是拍脑袋。4.2 RBF 核 SVM 的参数含义与搜索策略SVR 有三个关键参数C控制对误差的容忍度越大越容易过拟合epsilon是不敏感带宽度预测偏差在带内不计损失gamma是 RBF 核的宽度参数越大影响范围越小、越容易记住训练点。这三个参数互相耦合网格搜索范围要有层次先粗搜C和gamma的数量级再在最优附近细化。from sklearn.model_selection import GridSearchCV, TimeSeriesSplit def tune_svr(X_tr, y_tr): # 时间序列交叉验证避免未来信息渗入验证折 tscv TimeSeriesSplit(n_splits5) param_grid { C: [0.1, 1, 10, 100], gamma: [scale, 0.01, 0.1, 1], epsilon: [0.01, 0.1, 0.5], } grid GridSearchCV( SVR(kernelrbf), param_grid, cvtscv, scoringneg_root_mean_squared_error, n_jobs-1, ) grid.fit(X_tr, y_tr) print(最优参数:, grid.best_params_) return grid.best_estimator_gammascale是 sklearn 的默认启发式等于 1/(特征数×方差)先把它放进候选里当参照再和固定值比较。scoring用负 RMSE 是因为 GridSearchCV 默认最大化得分取负号后等价于最小化误差。TimeSeriesSplit的每一折都保证验证集在训练集之后这个细节直接决定了调参结果可不可信。4.3 与随机森林的取舍精度、解释性和训练成本在中小规模客流数据上RBF 核 SVR 和随机森林的精度往往咬得很紧差距常在 5% 以内。取舍点不在精度在另外两件事一是解释性随机森林能给特征重要性SVR 给不了如果答辩要求讲清“哪个因素最影响客流”树模型更省事二是训练成本SVR 的复杂度随样本量增长较快样本过万时网格搜索会明显变慢。我的做法是两个都训用验证集指标选主力模型再用另一个做交叉印证结论写进报告里比单模型更稳。如果最终选 SVR特征重要性可以用置换重要性补上虽然不是原生输出但足够支撑分析。5. 避坑排查五类最容易在验收时翻车的问题5.1 随机划分导致指标虚高现象是测试集 R² 逼近 0.95换一批数据或者按时间重切之后掉到 0.5 以下。原因就是划分时打乱了时间顺序滞后特征把未来信息带进了训练集。解决办法是全程按时间切分交叉验证统一用TimeSeriesSplit并在报告里写明切分方式。这个坑几乎每届都有人踩改起来只需一行代码但发现得晚就要重跑全部实验。5.2 标准化在切分之前做现象是模型离线指标很好线上预测系统性偏大或偏小。原因是先对全量数据做了fit_transform测试集的均值和方差信息渗进了训练过程。正确顺序永远是先切分、再在训练集上fit、最后transform测试集。检查方法很简单看代码里StandardScaler的fit调用是不是只出现一次且入参是训练集。5.3 滞后特征把当天值算进去现象是训练 loss 极低但预测下一天时误差突然放大。原因是滚动窗口没做shift(1)窗口末尾包含了当天目标值模型在训练阶段相当于看了答案。排查办法是造一个最小样例手动核对某一行的roll_mean_7是否等于它前 7 天的均值。这个错误隐蔽性强因为代码不报错、指标还好看只能靠逻辑核对。5.4 SVR 对未截断的极端值过度敏感现象是整体 MAE 尚可但个别高峰日预测值被明显压低残差分布严重右偏。原因是 SVR 的 epsilon 不敏感带对离群点容忍有限一个极端峰值会把回归面整体拉偏。解决办法是在特征管道里做分位截断第 2 章那段clip或者在训练前对目标做对数变换预测后再指数还原。两种方式我都用过数据波动大时对数变换更稳。5.5 用测试集分位数做分箱边界现象是离线评估没问题换时间段后同一评分被分到不同箱模型行为漂移。原因是分箱用了qcut或者在全量数据上算分位数测试集分布参与了特征构造。解决办法是把分箱边界写成业务经验固定值并在文档里注明依据来源。凡是“依赖数据分布”的特征变换都要问一句这个分布是训练集独有的还是全量共享的6. 验收与答辩用一份可复现清单把方案讲圆验收这件事评委和导师看的不是你模型多花哨而是整条链路能不能被别人复现。我一般会准备一份四行的核对清单答辩前逐项过一遍数据切分是否严格按时间、标准化是否只在训练集拟合、滞后特征是否全部 shift、随机种子是否固定。这四条对应第 5 章的前三个坑加上复现性任何一条不过指标都要打问号。现场演示建议按“输入—处理—输出”三段走不要一上来就贴模型代码。先展示原始表长什么样、脏在哪再展示清洗后的样本和特征列最后给出测试集上的预测对比图横轴时间、纵轴客流、两条线分别是真实值和预测值。评委最容易追问的三个问题提前准备好答案——为什么用 SVM 而不是树模型答精度接近时看解释性和数据规模、RMSE 的具体数值对应业务上多大偏差答换算成日均多少人、模型多久需要重训答客流模式变化或误差连续超标时触发。指标体系用一张小表收口更清楚指标数值业务含义RMSE按实测填写大误差惩罚后的整体偏差MAE按实测填写日均预测偏差人数R²按实测填写对客流波动的解释比例表格里的数值必须来自你自己跑出来的结果不要照抄任何示例数字答辩时被问到计算口径答不上来是很扣分的事。另外把random_state、切分比例、分箱边界这些参数集中写进一个配置文件或脚本头部别人拿到代码改一处就能复跑这比在报告里写十页原理都有说服力。从那以后我每次交付预测类项目都强制把“切分顺序”和“特征生成时点”写进 README 第一页哪怕需求方不看自己复盘时也能一眼定位问题出在哪一层。这套习惯帮我省下的返工时间远比多调几轮参来得值。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。