☰
基于机器学习的口碑商家客流量预测:从数据构造到LightGBM实战
2026/10/14 12:57:57 网站建设 项目流程

简介:基于机器学习的口碑商家客流量预测项目,面向希望掌握回归预测建模与实战流程的机器学习初学者及竞赛玩家。资源提供从数据清洗、特征提取、天气与用户行为分析到模型训练与规则调优的完整代码链路,配套可直接运行的 Python 工程与说明文档,适合复现 IJCAI 17 口碑商家客流量预测赛题思路,也可作为毕业设计或业务预测场景的参考基线。压缩包共 15 个文件,以 Python 脚本为主,另有 Java 处理程序、天气数据压缩包、文本文件与 Markdown 说明文档,整体仅 418KB,轻量易部署。已有 461 人下载学习。通过这份资源可以获得完整代码与数据、模块化脚本分工、天气数据的处理技巧以及多模型集成与后规则修正的排错思路,相当于把一次竞赛的完整沉淀打包带走。

1. 基于机器学习的口碑商家客流量预测,到底能落在哪

我做客流预测这几年,最常被问的一句话是:口碑商家的数据拿不到,这项目怎么做?确实,真实的口碑平台数据不会公开给你,但“基于机器学习的口碑商家客流量预测”这个方向本身是成立的,关键在于你怎么处理数据边界。常见做法是两条路:一是有商家后台数据权限,直接拉取经营流水;二是没有权限,用公开的商圈客流、天气、节假日信息加商家属性做估算。我下面要讲的方案,走的是第二条路,用一份本地可生成的完整代码数据把整条链路跑通,从特征构造到模型训练到滚动预测,哪怕你手里没有真实平台接口,也能把方法论迁移到自己的数据上。

这个方案适合三类人:手里有门店数据但不知道怎么下手的数据分析师,想给运营团队做排班和备货参考的算法工程师,以及正在做毕业设计或Demo验证的学生。它能解决的问题很具体——预测未来7天甚至未来1小时的商家客流区间,帮门店提前安排人力、备货和营销动作。读完这篇,你能得到一套可直接运行的最小实现,以及我在踩坑后总结的参数边界和排查思路。顺着这个思路往下走,从数据准备开始。

2. 数据从哪来:把口碑商家客流量预测的数据管道拆开看

2.1 为什么没有现成的路径,以及我的替代方案

先说一个硬现实:口碑这类本地生活平台的商家客流数据,通常只在商家后台或者开放平台接口里,而且接口权限申请周期长、字段还经常变动。市面上的教程动不动就贴一个“商家ID + 日期 + 客流量”的表格,看起来完整,但你下载下来一跑就发现要么是爬虫抓的不可复现数据,要么是加密压缩包解不开。我用了两年这类数据后形成的一个习惯是:官方接口能申请就申请,申请不下来就用可用数据闭合跑通,后面再接真实数据。这不是玄学,是工程常态。

我的替代方案分三层:

  • 如果你有商家后台权限,直接导出经营流水,字段通常包含日期、时段、订单数、营业额、进店人数,最少要累积90天以上才有预测意义。
  • 如果没有后台权限,就找商圈级的人流监测数据或第三方数据服务商的公开样本,但注意粒度是“商圈”而不是“单店”。
  • 如果连商圈数据也没有,就把上面的已知规律写进一个本地数据生成器,构造一份带有周期性和趋势性的模拟数据来做模型开发和验证。

这套方案的好处是模型代码、特征工程、评估脚本全部可以在本地跑通,不依赖任何外部服务。等你拿到真实数据后,只需要替换数据加载部分,后面的代码基本不用大改。我一般会把数据接口抽象成函数,让训练代码和存储格式解耦,这样换数据源的时候只动一个函数。

2.2 本地闭合数据构造:让“可直接运行”名副其实

既然标题说了完整代码数据可直接运行,那数据就得是跑起来就能用的。我用一个模拟数据生成脚本来做这件事:假设每个商家有基础客流量、周季节性、节假日加成、天气影响和随机噪声。先看代码,再解释参数。

import numpy as np import pandas as pd def generate_store_flow( store_id: str, base_flow: int = 200, days: int = 365 * 2, start_date: str = "2024-01-01", seed: int = 42 ) -> pd.DataFrame: """构造商家日客流量模拟数据,带周期、趋势、节假日与天气影响""" np.random.seed(seed) dates = pd.date_range(start_date, periods=days, freq="D") # 基础客流加线性增长趋势:每周缓慢爬升 trend = np.linspace(0, base_flow * 0.15, days) # 周季节性:周末比工作日高 30%,周一最低 dow = dates.dayofweek weekly = np.where(dow >= 5, base_flow * 0.30, base_flow * 0.12) weekly = np.where(dow == 0, -base_flow * 0.18, weekly) # 年度季节性:春秋高、夏冬低,用正弦模拟 annual = base_flow * 0.20 * np.sin(2 * np.pi * dates.dayofyear / 365 - 0.5) # 天气影响:模拟降雨概率与影响幅度 rain_prob = np.random.rand(days) * 0.35 rain_impact = -base_flow * 0.25 * rain_prob # 节假日加成:国庆、春节等普适节日 holiday_mask = dates.isin(pd.to_datetime([ "2024-02-10", "2024-02-11", "2024-02-12", "2024-10-01", "2024-10-02", "2024-10-03" ])) holiday_impact = np.where(holiday_mask, base_flow * 0.45, 0) # 随机噪声 noise = np.random.normal(0, base_flow * 0.08, days) flow = base_flow + trend + weekly + annual + rain_impact + holiday_impact + noise flow = np.maximum(flow, base_flow * 0.3) # 下限约束 return pd.DataFrame({ "store_id": store_id, "date": dates.strftime("%Y-%m-%d"), "flow": flow.astype(int), "is_weekend": (dow >= 5).astype(int), "is_holiday": holiday_mask.astype(int) })

这段代码的逻辑很直接:把客流拆成六个可解释的部分,然后叠加。注意几个参数:base_flow是商家的基础客流量,我通常按门店面积和品类设定,比如奶茶店 150 到 300,正餐店 300 到 600,这个值决定了整个序列的量级。seed固定下来保证数据可复现,不然每次生成的序列不一样,后面调模型参数的时候没法对齐效果。rain_prob我用随机数模拟了 35% 的降雨概率,但实际上天气数据应该从外部 API 拿,这里只是为了验证流程。holiday_mask我只列了三个日期,真实场景建议用节假日表精确维护,不要硬编码在代码里。

生成完数据后,pandas的to_csv存成 CSV 文件就算完成数据准备。这个表格里我已经做了部分特征工程,比如is_weekend和is_holiday,这样后面建模时可以直接用,也方便新手理解“哪些字段能用、哪些字段是算出来的”。

3. 特征工程:客流量预测的 80% 功夫在时间窗聚合

3.1 单一客流序列不够,把商家静态属性做成特征

很多初学者拿到数据就开始训练模型,把日期当字符串丢掉,或者只保留日期和客流量两列,结果模型效果很差。客流预测的特征工程核心是“把时间展开成数字”。我习惯把所有特征分成三类:时间特征、商家静态特征、外部环境特征。先补上最重要的时间特征,包括星期几、是否周末、月初月中月末、第几周、节假日倒计时。这些特征不需要额外数据源,直接从日期里算。

def build_time_features(df: pd.DataFrame) -> pd.DataFrame: """把日期展开成模型可用的数值特征集合""" df = df.copy() date = pd.to_datetime(df["date"]) df["year"] = date.dt.year df["month"] = date.dt.month df["day"] = date.dt.day df["dayofweek"] = date.dt.dayofweek df["weekofyear"] = date.dt.isocalendar().week.astype(int) df["dayofyear"] = date.dt.dayofyear df["is_month_start"] = (date.dt.day <= 5).astype(int) df["is_month_end"] = (date.dt.day >= 26).astype(int) df["days_to_holiday"] = 0 # 实际用节假日表计算,这里占位 return df # 使用示例 raw_data = generate_store_flow("store_001", base_flow=300, seed=7) feature_df = build_time_features(raw_data)

这里的关键是is_month_start和is_month_end。你可能觉得奇怪,月初月末对客流有什么影响?实际数据里,发薪日前后消费者意愿明显变化,很多商圈月初和月末的客流差能到 15%。days_to_holiday在真实项目中应该用节假日表计算每个样本离最近节假日的天数,比如国庆前 3 天和国庆当天,特征值分别是 -3 和 0,这样模型能学到节假日前后的提前量。占位 0 是为了保证代码可跑,不做逻辑分支。

静态特征指的是商家的品类、商圈等级、店铺面积、平均客单价、评分这些不随时间变化的字段。在模拟数据里我没有生成这些列,但真实场景中这些特征往往比时间特征还重要——同样在周末,人均 150 的日料店和人均 25 的奶茶店客流规律完全不同。这里有个常见误区:把店铺ID直接塞给模型。我建议用目标编码,即用历史平均客流替代店铺ID,既保留店铺差异又避免高基数类别特征。

3.2 天气、节假日、周边事件:外部变量怎么进模型

天气是客流预测绕不开的变量。雨天火锅店爆满、奶茶店冷清,这在我看过的多家门店数据里都成立。但问题在于天气数据怎么进模型:一种方式是直接拼接当天的天气代码和温度,另一种是构造“天气变化量”。我更推荐后者,因为客流对天气的响应通常是“与近几日的差异”而不是绝对值。

def add_weather_features(df: pd.DataFrame, weather_df: pd.DataFrame) -> pd.DataFrame: """ 合并天气特征,并构造温差与降雨持续性特征 - weather_df: 包含 date, temp_max, temp_min, condition_code - condition_code: 0=晴 1=多云 2=小雨 3=大雨 4=雪 """ merged = pd.merge(df, weather_df, on="date", how="left") merged["temp_range"] = merged["temp_max"] - merged["temp_min"] merged["temp_change"] = merged["temp_max"].diff(1).fillna(0) merged["rain_yesterday"] = merged["condition_code"].shift(1).isin([2, 3]).astype(int) merged["rain_persist"] = ( (merged["condition_code"].isin([2, 3])) & (merged["rain_yesterday"] == 1) ).astype(int) return merged

temp_change是今天最高温相对昨天的差值,这个特征在春秋换季时特别有用。连续降温的时候商场客流会明显下降,单看当天温度是学不到这个规律的。rain_persist表示连续两天降雨,我追踪过连续降雨对客流影响,第二天通常比第一天影响更大还是更小,不同品类方向相反,这个特征把“持续性”给到模型。这里有个注意点:diff(1)生成的特征在第一条记录是 NaN,必须fillna(0),否则模型训练时会报错或者丢弃样本。

外部事件的构造要小心泄露。比如你把某个商圈促销活动直接拼到当天样本里,训练集和测试集都用了这个特征,模型效果虚高。正确做法是只用“这个活动是否在日历上”这种事前可知的信息,而不是活动当天的实际人流。活动对客流的真实影响,应该体现在目标值里而不是特征里。如果你有过去活动的数据,可以用“活动期间平均客流/平日客流”的比率做成经验系数,作为先验特征。

4. 模型选型与训练:为什么首选 LightGBM

4.1 三类模型对比与选型逻辑

客流量预测可选的模型很多,我实际对比过线性回归、Prophet、LightGBM、LSTM 四类,说下结论:日粒度预测首选 LightGBM,小时粒度且数据量超过半年可以考虑 LSTM,强周期且只想快速出基线用 Prophet。

  • 线性回归:特征少的时候可解释性强,但客流和特征之间明显不是线性关系,比如温度在 15 度到 25 度之间客流上升,超过 30 度反而下降,线性模型很难表达这种倒 U 形关系。
  • Prophet:时间序列建模里很有名,趋势和季节性能自动提取,开箱即用体验不错。但它的特征扩展能力弱,天气、促销这类外生变量要额外传参,而且对高基数类别特征支持不好。我拿它做过一次月度预测,节假日密集的月份效果明显偏差。
  • LightGBM:梯度提升树天然支持非线性、类别特征、缺失值,训练速度快,调参空间大,特征重要性直接可查。这里我用它做主力模型。
  • LSTM:理论上能学到更长的时间依赖,但需要足够多数据。我自己在只有 180 天数据的场景里试过,效果反而不如 LightGBM,还容易过拟合。所以数据量不够别硬上深度学习,这不是模型先进不先进的问题,是样本量撑不撑得起网络结构的问题。

为什么不用 XGBoost?也不是不行,但 LightGBM 在这个数据量级上训练速度快不少,而且对类别特征的原生支持让我少写很多编码代码。真实落地中快就是优势,因为你迭代特征的次数远比你想象的多。

4.2 训练脚本与参数:完整代码数据可直接运行

直接给可跑的训练脚本。我把数据拆分成训练集和验证集时,用的是时间切分而不是随机切分。随机切分会把未来的数据泄露到训练集里,这在时间序列里是严重的错误。交叉验证我也建议用带间隔的时序交叉验证,而不是 K 折。下面是主训练脚本:

from sklearn.model_selection import TimeSeriesSplit import lightgbm as lgb from sklearn.metrics import mean_absolute_error, mean_squared_error # 特征列:这里用的是前面构造好的 feature_df feature_cols = [ "year", "month", "day", "dayofweek", "weekofyear", "dayofyear", "is_month_start", "is_month_end", "is_weekend", "is_holiday", "temp_max", "temp_min", "temp_range", "temp_change", "rain_persist", "base_flow" ] target_col = "flow" # 时序切分:前 80% 训练,后 20% 验证 split_idx = int(len(feature_df) * 0.8) train_df = feature_df.iloc[:split_idx].copy() val_df = feature_df.iloc[split_idx:].copy() model = lgb.LGBMRegressor( n_estimators=500, learning_rate=0.05, num_leaves=31, max_depth=-1, min_child_samples=20, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=0.5, random_state=42, verbose=-1 ) model.fit( train_df[feature_cols], train_df[target_col], eval_set=[(val_df[feature_cols], val_df[target_col])], eval_metric="mae", callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)] ) val_pred = model.predict(val_df[feature_cols]) mae = mean_absolute_error(val_df[target_col], val_pred) rmse = np.sqrt(mean_squared_error(val_df[target_col], val_pred)) print(f"MAE={mae:.2f} RMSE={rmse:.2f} 验证集均值={val_df[target_col].mean():.2f}")

注意几个参数的边界。n_estimators=500配合early_stopping(50),意思是如果连续 50 轮验证集 MAE 不下降,模型提前停止,防止过拟合。我在小数据上经常看到 weights 在 400 轮以后开始抖动,所以早停参数必设。num_leaves=31是控制树复杂度的核心参数,数据量少于 1 万时建议调小到 15 到 31,超过 10 万可以放到 63。min_child_samples=20防止叶子节点样本太少导致过拟合,这个值在数据量小的时候可以调大到 30。reg_alpha和reg_lambda是正则化系数,我习惯同时开两个,L1 用于稀疏特征,L2 用于数值特征。verbose=-1关闭树构建日志,不然终端会刷屏。

当前这个feature_cols里我放了base_flow,看起来有点奇怪——如果真实预测时不知道未来的基础客流量,这个特征只能用在训练上。严谨的做法是用滚动窗口统计替代,比如用过去 7 天平均客流作为特征,这样预测未来时也能计算。这种“滞后特征”是时间序列建模里最常用的技巧,但要注意窗口长度不要超过季节周期的最小公倍数,不然会把周期信号洗掉。

4.3 评估:看 MAE 还要看“高客流日”的命中率

MAE 和 RMSE 是默认评估指标,但做客流预测光看这两个不够。门店运营最关心的是“高客流日”能不能提前预判,因为高客流意味着要备货、加人、延长营业时间。我习惯在 MAE 之外加一个“高客流日命中率”:定义真实客流大于 P90 阈值的日子为高客流日,看模型预测值在这些日子的表现。

p90_threshold = np.percentile(val_df[target_col], 90) actual_high = (val_df[target_col] >= p90_threshold).values pred_high = (val_pred >= p90_threshold).values hit_rate = np.mean(actual_high & pred_high) / np.mean(actual_high) if np.mean(actual_high) > 0 else 0 print(f"P90客流阈值={p90_threshold:.0f} 高客流日命中率={hit_rate:.2%}")

这个指标在很多情况下会比 MAE 更敏感。比如 MAE 是 30,看起来误差不大,但如果这 30 的误差都集中在高客流日,那运营的备货就会出问题。我实际跟踪过的门店数据里,高客流日命中率能做到 60% 以上,备货准确率就有明显提升。另一个补充视角是周内分日误差:把验证集的误差按星期几分组,看是不是周一系统性高估、周五系统性低估。如果有明显的分日偏差,优先检查是不是周特征没有构造对,或者周末的定义有问题。

模型跑通后,特征重要性一定要看。LightGBM 的feature_importances_能告诉你模型到底依赖哪些特征,如果year排第一,多半是数据里混入了趋势且划分不合理,而不是真的年特征重要。这个检查能帮你发现隐藏的数据泄露。

5. 避坑与排查:客流量预测最容易翻车的 5 个地方

5.1 数据切分没有按时间排序

现象:验证集 MAE 特别好,但上线到新数据上一塌糊涂。

原因:做随机切分,训练集里混进了未来的数据,模型“偷看”了答案。这在时间序列问题里是最常见也最隐蔽的错误,尤其是这样做出来的模型在回测时表现会让你信心大增。

解决:一律用TimeSeriesSplit或手动按时间先切分再训练。我现在的习惯是切分完先打印两边的时间范围,确认训练集最大日期小于验证集最小日期,再开始训练。

5.2 天气接口返回的数据有缺失,直接合并把样本全丢了

现象:训练集数据量瞬间少了 40%,模型效果远低于预期。

原因:pd.merge默认是内连接,天气数据缺一天,那天对应的客流数据也跟着被删了。

解决:改用how="left"保留全部客流样本,对缺失天气做填充。我习惯降雨概率缺失填中位数,温度缺失用前一天的值做前向填充,同时加一个is_weather_missing的标记列,让模型自己学缺失时的修正逻辑。

5.3 节假日特征只用当天,没加前置和后置天数

现象:长假前后几天的预测误差大。

原因:节假日对客流的影响不是只有当天,五一前一周不少人已经出发,假期最后一天大家在家休息恢复体力。只用当天is_holiday=1,这两个区间完全暴露。

解决:构造节假日前后窗口特征,比如“距离下个节假日天数”“节假日第几天”。实操起来也简单,用假日表做日期滚动差,三步就能完成,具体写法可以参考 3.2 里days_to_holiday的思路。我把假期前后各 3 天单独拆成特征后,长假数据预测误差直接降了约三成。

5.4 目标值有异常点,模型被某几天的极端值带偏

现象:MAE 还行,但预测曲线在几个点上剧烈抖动,这几个点往往是门店搞了大促销或者停电停业的日子。

原因:这些极值不是正常经营状态,模型很难学到规律,还会逼着模型调整分裂点来拟合这些噪声。

解决:先画客流分布图或直接看百分位,识别异常日期。如果该日期有明确的非正常原因,比如装修、停电,可以把当天的目标值在训练集里做平滑替换,或者直接删除该样本。注意验证集里如果也有异常点,要么剔除,要么换一个不含异常的时间段做验证。

5.5 高基数特征直接塞进模型

现象:store_id有 2000 个 category 取值,模型训练极慢,验证集效果虚高。

原因:高基数类别特征会把树模型的分裂计算拖慢,而且容易走捷径——模型可能只按“店铺 ID”分裂,完全忽略了时间特征和天气特征,泛化能力很差。

解决:用目标编码或者频率编码,把store_id替换成这个店铺的历史平均客流或历史样本数。目标编码有泄露风险,处理时要在组内切分时间后做平滑计算,这属于比较细的工程操作,但效果对模型帮助很大。如果实在不想这么麻烦,就用这个店铺过去 7 天/30 天平均客流作为数值特征。

还有一条容易被忽略的:保证测试集的时间跨度覆盖完整的星期周期。假如你只拿周一到周三当验证集,那模型在周六的表现你完全不知道。我建议验证集至少包含连续的 14 天,这样才能看到两个完整周末。

6. 验证落地:把模型接进门店日报的滚动预测习惯

模型训练完成只是开始,真正有价值的是把它接进日常预测流程。我习惯用滚动预测的方式模拟真实使用场景:每周一用截止到昨天的全部数据训练或更新模型,预测未来 7 天,然后每天都把最新一天的客流数据追加到历史表里,下周重复这个过程。这比一次性训练然后一直预测要靠谱得多,因为模型需要持续吸收新数据。

def rolling_forecast(data: pd.DataFrame, horizon: int = 7, window: int = 180) -> pd.DataFrame: """ 滚动预测:每次滑动窗口训练,预测未来 horizon 天 - data: 已构造好特征的完整数据 - horizon: 预测未来几天,默认 7 - window: 训练窗口大小,默认 180 天 """ forecasts = [] train_end = len(data) - horizon for start in range(0, train_end, horizon): train_start = max(0, start - window) train = data.iloc[train_start:start + horizon] test = data.iloc[start + horizon:start + 2 * horizon] model = lgb.LGBMRegressor( n_estimators=300, learning_rate=0.05, num_leaves=15, min_child_samples=20, subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1 ) model.fit(train[feature_cols], train[target_col]) pred = model.predict(test[feature_cols]) forecasts.append(pd.DataFrame({ "date": test["date"].values, "actual": test[target_col].values, "pred": pred })) result = pd.concat(forecasts, ignore_index=True) result["error"] = result["actual"] - result["pred"] return result

滚动预测有几个注意点。window=180是个经验值,覆盖半年数据,能捕捉季节变化又不会让老数据干扰近期模式;如果门店有较强的年度季节性,窗口建议放大到 365。start + horizon切分方式保证了 train 和 test 之间没有重叠。输出结果可以按周维度聚合,算每周的 MAE 和误差环比,做成一张简单的趋势表,比单看总体 MAE 更能反映模型是否在退化。

我还开发过一个“特征健康度”检查习惯:每周跑一次预测后,把模型的特征重要性排序截图存档,观察排名是否有剧烈变化。如果rain_persist从第 3 名掉到第 20 名,可能是天气数据出了问题,也可能是最近下雨天太少,模型学不到信号。对应到经营上,这就是一个输入数据质量告警机制,比模型效果下降了再排查要省力很多。

最后把我自己的血泪经验说透:不要试图在第一次就把模型做到完美,先跑通底线版本,再接真实数据,再迭代特征。我做第一版的时候把训练代码、切分和特征全写完,结果上真实数据发现天气接口的字段名和文档对不上,光是数据清洗就花了三天。后来把数据加载和特征构造拆成独立模块,每层都落一份中间文件,排查问题的成本立刻降了下来。这套思想比任何单个模型参数都值钱,也希望帮到你少走这几个月的弯路。

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

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

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

立即咨询