简介:围绕Swiggy Hackathon 2018订单需求预测赛题构建的机器学习项目包,面向具备Python与回归建模基础、希望学习外卖订单量预测完整流程的开发者与参赛者,重点解决基于历史订单数据预测未来需求的问题。压缩包共75个文件,约4.63MB,包含Python脚本、CSV订单数据、JSON预测结果与前端配置、JavaScript及可视化页面素材,并附PDF说明与Markdown文档,覆盖数据处理、KNN与随机森林回归、预测生成及结果展示等环节。已有414人学习下载,说明该赛题方案在需求预测学习中具有一定参考度。读者可从中获得较完整的赛题实现思路:数据准备与特征构造脚本、两种回归模型的训练与评估代码、批量预测输出,以及基于地理编码和Web前端的可视化展示素材,便于对照复现实验、理解模型选型与结果呈现,并据此拓展到更复杂的订单需求预测场景。
1. 订单需求预测:误差的大头往往不在模型选型上
订单需求预测这件事,最反直觉的一点是:把 KNN 换成随机森林、把随机森林换成梯度提升,误差通常只掉几个百分点;而把特征的时间对齐方向改一改,误差能掉三成。用历史订单数据训练一个机器学习模型去预测未来某个时间片的订单量,模型本身占的工作量其实很小,剩下的大头全在「什么样的特征才算没有偷看未来」这件事上。这也是为什么值得把机制拆开看:预测不准的时候,多数人第一反应是换模型,真正的根因常常藏在时间切分和特征构造里。一份基于历史订单数据的订单需求预测机器学习模型,恰好是个足够小的样本,能把 KNN 回归和随机森林回归这两条路线的取舍一次看清楚。
2. 特征工程:把订单流水变成可学习的时间片样本
原始数据几乎永远是一行一条订单,包含订单标识、下单时间、配送区域、金额、餐品类别这些字段。而预测目标不是「这一单要多久送到」,是「下一个时间片、这个区域会有多少单」。中间隔着一层聚合和重构,这层做错了,后面换什么模型都是白搭。
2.1 从一条订单到一个时间片的聚合口径
聚合粒度不是拍脑袋定的,它由预测结果的消费方倒推。外卖场景里,预测结果一般给排班和备餐用,提前量通常在 15 分钟到 1 小时之间:粒度太细,单个时间片内样本稀疏,噪声压过信号;粒度太粗,预测出来也没法指导具体调度。半小时或一小时是常见的起点,具体取值要看你需要提前多久做出决策。
聚合的另一个维度是空间。直接用经纬度做键不行,因为坐标是连续的,几乎不会有两个订单落在完全相同的点上。常见做法是映射成网格,或者取 geohash 的前缀。前缀长度的选择本质上是在控制区域大小,前缀太短会把整个商圈混成一个点,太长又会让每个区域样本不足。
import pandas as pd def build_time_slices(orders, freq="30min", geohash_prefix=5): df = orders.copy() df["order_time"] = pd.to_datetime(df["order_time"]) # 时间片对齐:floor 到固定边界,否则每条订单各占一个时间戳,无法聚合 df["slice"] = df["order_time"].dt.floor(freq) # 区域键:用前缀而不是完整 geohash,保证区域数量可控、样本不被打散 df["grid"] = df["geohash"].str[:geohash_prefix] agg = (df.groupby(["slice", "grid"]) .agg(order_cnt=("order_id", "nunique"), # unique 防止重复上报的订单被算两次 avg_amount=("amount", "mean")) .reset_index()) # 补零:没有订单的时间片是「需求为 0」,不是「这条记录不存在」 full_idx = pd.MultiIndex.from_product( [pd.date_range(agg["slice"].min(), agg["slice"].max(), freq=freq), sorted(agg["grid"].unique())], names=["slice", "grid"]) return agg.set_index(["slice", "grid"]).reindex(full_idx, fill_value=0).reset_index()这段代码里最容易被跳过、后果也最严重的是补零那一步。如果只保留有订单的时间片,模型看到的样本全部是「有需求」的时刻,它会天然学出一个偏高的基线,而现实中大量时间片的需求就是零。补零之后样本量会涨好几倍,训练变慢,但这是必要的代价。order_id用nunique而不是count,是因为订单流水里重复记录的情况比想象中常见,尤其是在多次导出拼接之后。
2.2 滞后特征与滑动窗口:让模型看到自己过去的样子
聚合出来的表本身信息量很低:一行只有时间片、区域和当片订单数。模型要预测未来,就必须能从「过去」里读出模式,所以要把历史值搬到当前行来,这就是滞后特征。lag_1是上一个时间片,lag_48是 24 小时前(半小时粒度下),lag_336是一周前同一时段。后面这两个是需求预测里信息量最大的,因为它们分别对应日周期和周周期。
滑动窗口统计补的是另一类信息:近期趋势和波动幅度。均值告诉你「最近大概什么水平」,标准差告诉你「最近稳不稳」。一个区域如果最近几个时间片方差很大,那预测值的不确定性也大,这两类信息对后面的模型都很关键。
def add_lag_features(df, lags=(1, 2, 48, 336), windows=(6, 48)): df = df.sort_values(["grid", "slice"]).copy() g = df.groupby("grid", group_keys=False) for lag in lags: # 按区域分组 shift,避免相邻区域的数据互相串味 df[f"lag_{lag}"] = g["order_cnt"].shift(lag) for w in windows: # 关键:先 shift(1) 再 rolling。不 shift 的话,当前时刻的真实值会混进特征 rolled = g["order_cnt"].shift(1).rolling(w, min_periods=w // 2) df[f"roll_mean_{w}"] = rolled.mean().reset_index(level=0, drop=True) df[f"roll_std_{w}"] = rolled.std().reset_index(level=0, drop=True) return dfshift(1)这一步是整段代码里唯一的「不能错」的地方。rolling默认是包含当前行的,如果不先移一位,roll_mean_6里就包含了当前时间片的真实订单量,模型会学得极好,上线后立刻崩。这种错误在离线评估里几乎不可能被发现,因为离线指标只会好看。
min_periods=w // 2是为了在序列开头不产生大段空值,代价是前几个窗口的统计量不够稳。可以接受,因为这部分样本本来就少。
2.3 时间特征与周期编码:为什么 23 点和 0 点在编码上相邻
时间片天然带有小时、星期几、是否节假日这些属性。最直接的做法是 one-hot,但小时这个变量有个特殊之处:23 点和 0 点在语义上紧挨着,one-hot 之后它们成了距离最远的两列。对树模型影响不大,因为树可以靠多次切分绕过去;对 KNN 是致命的,因为它的全部判断都建立在距离上。
把周期变量映射到单位圆上是标准解法:小时映射成sin(2πh/24)和cos(2πh/24)两维,23 点和 0 点在圆上就是相邻的。星期几同理,周期取 7。
import numpy as np def cyclical(series, period): """把周期变量映射到单位圆,保留首尾相邻的几何关系""" angle = 2 * np.pi * series / period return np.sin(angle), np.cos(angle) df["hour_sin"], df["hour_cos"] = cyclical(df["slice"].dt.hour, 24) df["dow_sin"], df["dow_cos"] = cyclical(df["slice"].dt.dayofweek, 7)要注意的是,这种编码方式会让特征维度增加,而增加的这两维本身是强相关的(sin² + cos² = 1)。对于 KNN,这意味着它们在距离计算里被算了两次;对于随机森林,意味着特征重要性会被这两个共线特征稀释。所以周期编码不是无脑加,要看下游模型。这一点在后面两章的对比里会反复出现。
3. KNN 回归:没有训练阶段,代价全压在距离度量上
KNN 在需求预测里出人意料地好用,因为它的假设非常贴合业务直觉:历史上最像当前这个时刻的若干个时间片,它们的订单量平均一下,就是我的预测。这个「像」由特征空间里的距离定义。
3.1 KNN 到底在什么空间里找邻居
KNN 没有显式的训练过程,它把整个训练集当作索引存下来,预测时计算待预测样本到所有训练样本的距离,取最近的 k 个,然后平均。所谓「学习」,全部发生在特征构造阶段——你给什么特征,它就在什么空间里找邻居。
这意味着两件事。第一,KNN 的预测成本随训练集大小线性增长,样本量到十万级以上时,线上实时预测就很吃力,通常要退化成离线批量预测或者用近似最近邻索引。第二,它的表现对特征的表达方式极度敏感,同一个任务换一套特征编码,效果可能差一倍。
from sklearn.neighbors import KNeighborsRegressor from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline knn = Pipeline([ ("scaler", StandardScaler()), # 距离度量对量纲敏感,缩放不是可选项 ("model", KNeighborsRegressor( n_neighbors=15, weights="distance", # 越近的邻居权重越大,缓解 k 取值过硬的问题 metric="manhattan", # 高维下 L1 通常比 L2 稳,不会被单维拉爆 n_jobs=-1, )), ])weights="distance"的效果是:距离为 d 的邻居权重是 1/d,k 个邻居加权平均。这比等权平均更合理,因为「最像的那几个」本来就该有更大话语权。副作用是如果某个邻居距离接近 0,权重会爆炸,实际使用时通常要在分母上加一个极小的常数,或者设一个距离下限。
metric="manhattan"的选择值得多说一句。L2 距离对大差异的维度惩罚更重(平方关系),在特征维度高、且某些维度噪声大的时候,单个异常维度会主导整个距离。L1 是线性的,对离群维度更宽容。外卖订单数据里,avg_amount这类字段的异常值不少,用 L1 更稳。
3.2 特征缩放:一个量级能毁掉整个距离度量
标准化在 KNN 里不是调优项,是前提。设想order_cnt的取值范围是 0 到 200,而hour_cos在 -1 到 1 之间。不做缩放的话,两个样本之间哪怕只差 5 单,在距离上的贡献也远大于「一个是午高峰、一个是凌晨」这种本质差异。模型实际上退化成了「只按订单量找邻居」,周期特征完全失效。
# 反例:不做缩放时,各维度对距离的贡献完全失衡 # 样本 A: order_cnt=100, hour_cos=1.0 # 样本 B: order_cnt=105, hour_cos=-1.0 # L2 距离 ≈ sqrt(25 + 4) ≈ 5.4,其中订单量维度贡献了 25,周期维度只贡献了 4标准化之后,每个维度的方差被拉到 1,贡献才大致均衡。但这里有个隐藏问题:标准化假设各维度同等重要,而实际上lag_1的预测能力远强于avg_amount。等权标准化等于人为地把弱特征抬到了和强特征一样的地位。KNN 里没有内置的机制给你学特征权重,所以通常要么手工给不同特征组乘系数,要么用带权重的距离度量。
另一个相关的问题是滞后特征之间的共线。lag_1、lag_2、roll_mean_6这几个量高度相关,在距离计算里相当于把「近期订单水平」这个维度重复加了三次。结果是距离空间被这一族特征主导,其他维度被淹没。常见的缓解办法是只保留少数几个有代表性的窗口,或者先对滞后特征做一次降维再喂给 KNN。
3.3 k 值与维度灾难:偏差和方差的另一张脸
k 小的时候,预测只参考最近的几个样本,方差大:训练集里某个时间片恰好有一场活动导致订单暴涨,附近的预测点全被带偏。k 大的时候,更多样本参与平均,方差小,但偏差上来了:凌晨的样本会被平均进午高峰的预测里。
调 k 本身没什么玄学,在按时间切分的验证集上扫一遍就行。真正需要警惕的是维度灾难,它会让「调 k」这件事失去意义。
import numpy as np from sklearn.neighbors import NearestNeighbors def distance_contrast(X, sample=200, seed=0): """最近邻距离 / 最远邻距离。越接近 1,说明距离度量越退化""" rng = np.random.default_rng(seed) idx = rng.choice(len(X), size=min(sample, len(X)), replace=False) nn = NearestNeighbors(n_neighbors=len(X)).fit(X) dist, _ = nn.kneighbors(X[idx]) return float((dist[:, 1] / dist[:, -1]).mean())这个比值在低维时通常很小,说明样本之间远近分明;随着维度上升,它会趋近 1,意思是「所有样本离你差不多远」。此时最近邻已经不再「最近」,KNN 的预测结果趋近于全体样本的均值。这就是为什么在 KNN 上盲目堆特征会翻车:每多一个弱相关维度,距离的区分度就被稀释一点。
实践上的判断标准很简单:特征数控制在十几个以内,且每个特征都要能说清「为什么它会让两个时间片的需求更接近」。说不清的维度就该砍掉。
4. 随机森林回归:用 Bagging 换掉方差这笔账
KNN 的问题在于它把全部希望寄托在距离度量上,随机森林换了一条路:它不关心样本之间的远近,只关心「用哪个特征、在哪个阈值上切一刀,能让两边的订单量差异最大」。
4.1 为什么单棵树会过拟合,而一片森林不会
单棵决策树的生长过程是贪心的:每次选一个特征和阈值,让切分后两侧的残差平方和下降最多。深度不加限制时,它能把训练集切到每个叶子只剩一个样本,训练误差接近 0。但订单量这个目标本身噪声很大,同一个区域同一个时段,这周 87 单下周 92 单,这里面有相当一部分是不可预测的随机波动。树把这些波动也记住了,就是典型的过拟合。
Bagging 的思路是:训练 B 棵树,每棵用有放回抽样得到的不同子集,最后把 B 个预测平均。在树之间近似独立的假设下,平均后的方差降到原来的 1/B,而偏差基本不变,因为每棵树的期望输出是一样的。数学上更准确的表达是:
Var(平均) = ρ·σ² + (1-ρ)·σ²/Bρ 是树之间的相关系数,σ² 是单棵树的方差。B 增大时第二项趋于 0,但第一项不动。所以光靠增加树的数量有上限,真正决定性能的是能不能把 ρ 压下去。这就是随机森林在 Bagging 之外多做一步「每次分裂只在随机特征子集里选」的原因——它强行让不同的树看到不同的特征,从而降低相关性。
from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor( n_estimators=500, max_features=0.5, # 每次分裂只看一半特征,压低树间相关性 rho min_samples_leaf=5, # 叶子太小等于记住噪声,时间序列任务里这个值要放大 bootstrap=True, oob_score=True, # 用袋外样本估一个泛化误差,但别太当真 n_jobs=-1, random_state=42, )min_samples_leaf是这份配置里最需要针对业务调整的参数。默认值 1 意味着允许每个叶子只有一个样本,而单条订单记录噪声极大,叶子越纯越是在拟合噪声。设成 5 到 20 之间的值,相当于对每个叶子做了最小样本量的平滑,通常能带来明显的验证集收益。
max_features取 0.5 是偏保守的。取值越小,树之间越不相关,但单棵树的表达能力也越弱。在特征数量不多(十几个)的情况下,0.5 到 0.8 是个合理区间;特征多了可以往下压。
4.2 OOB 与时间序列的错配:一个免费的验证集,但不能全信
oob_score=True的意思是:对每一棵树,用那些没有被抽到的样本(袋外样本)来计算误差,最后对所有树取平均。在样本独立同分布的假设下,这个估计近似无偏,等于白送一个验证集。
但订单数据不是独立同分布的。相邻时间片的 lag 特征直接把前后样本绑在了一起:一棵树的袋外样本时间点,很可能就是某个袋内样本的下一个时间片。这时候用袋内样本训练出来的树去预测袋外样本,误差会被严重低估。
我一般把这个指标当成「粗筛工具」:比较不同max_features或min_samples_leaf的相对高低可以,但它和真实线上误差的绝对值基本没有可比性。最终的模型选择,必须落在按时间切分出来的 holdout 上。
import numpy as np # 按时间切分,后面 20% 作为验证集,绝不随机打乱 cut = int(len(df) * 0.8) train, valid = df.iloc[:cut], df.iloc[cut:] X_train, y_train = train[FEATURES], train["order_cnt"] X_valid, y_valid = valid[FEATURES], valid["order_cnt"] rf.fit(X_train, y_train) pred = rf.predict(X_valid) rmse = float(np.sqrt(((pred - y_valid) ** 2).mean()))按时间切分这件事看着简单,但它是整篇文章里最重要的一行实践。随机切分会让模型在同一天的相邻时间片上同时「训练」和「验证」,lag 特征把未来信息直接递到了模型手里,离线指标能漂亮到 0.3 以内,上线直接打回原形。
4.3 特征重要性与可解释性:impurity 的偏心和 permutation 的陷阱
随机森林自带feature_importances_,但它基于不纯度下降,有两个已知偏差:偏好取值多的连续特征,以及偏好高基数特征。在订单特征里,avg_amount这种连续量天然占便宜,而「是否节假日」这种二值特征经常被低估。
更可靠的是置换重要性:把某一列打乱,看验证误差涨多少,涨得越多说明这个特征越重要。但对时间序列,直接 shuffle 会破坏时间结构,打乱之后模型面对的是「不可能出现的时间组合」,得到的重要性会系统性地偏向 lag 特征。
from sklearn.inspection import permutation_importance result = permutation_importance( rf, X_valid, y_valid, n_repeats=10, scoring="neg_root_mean_squared_error", random_state=42, ) importance = dict(zip(X_valid.columns, result.importances_mean))更稳的做法有两种:一是按grid整组打乱,保持每个区域内部的时间顺序;二是遮挡法,把某一列整体置为训练集均值,让模型在「这个特征没信息」的状态下预测。两种方法都比直接 shuffle 更贴近真实场景。不过要提醒一句,这两种做法算出来的重要性更保守,别指望它给出漂亮的排序,它的价值在于找「哪些特征其实是噪声」,而不是证明「哪些特征最重要」。
5. 避坑与排查:需求预测任务里最常见的五个翻车现场
这类任务的坑有个共同特征:离线指标越好,越可能有问题。下面五条基本覆盖了从数据处理到上线的大部分事故。
5.1 数据切分与特征构造阶段的三个坑
坑一:随机 KFold 造成时间泄漏。现象是线下 RMSE 在 0.3 上下,上线后直接翻到接近 1。原因是随机切分让模型在训练时见过某个时间片的下一个时间片,而lag_1恰恰就是那个值。解决方式很明确:所有验证都必须按时间切分,训练集在前、验证集在后,中间留出一段 gap(比如一个预测窗口的长度),防止边界处仍有重叠。
坑二:补零后滞后特征跨区域串味。现象是某些区域开头几行的lag_1有值,甚至带着完全不相干的量级。原因是groupby之后没有按grid分组shift,或者reindex之后的 MultiIndex 顺序和groupby的顺序不一致,导致某些行的滞后值来自另一个区域。解决:先sort_values(["grid", "slice"])再分组,reindex之后逐区域检查前几行是不是 NaN。这个检查可以写成一个断言,跑一次就永久生效。
坑三:目标编码在时间维度上的泄漏。现象是训练集拟合得极好,验证集崩掉。原因是用整段时间的均值做类别编码,编码值里包含了未来信息。在订单数据里,按区域算历史平均订单量是常见操作,但如果用的是全量数据,就等于把答案提前告诉了模型。解决:只用当前时间点之前的数据做扩张窗口均值,窗口至少留出一个预测周期的 gap。
5.2 模型训练与线上效果的两个坑
坑四:KNN 的 scaler 过期。现象是模型上线一段时间后,预测整体偏移,但代码没改过。原因是StandardScaler是在训练集上 fit 的,而线上数据的分布会随时间漂移(新商圈开张、季节性变化),过时的缩放参数会让距离度量整体失准。解决:把 scaler 和邻居库绑定成一条 pipeline,按固定周期整体重训,而不是只往里追加新样本。
坑五:树模型对高峰时段系统性低估。现象是平峰时段预测挺准,午晚高峰普遍偏低 10% 到 20%。原因有两层:一是树模型的分裂是分段常数,叶子节点的输出是平均值,而订单量是右偏分布,平均值天然低于右偏分布的高分位;二是 RMSE 虽然对大误差敏感,但它同时也会把极端高的样本往中间拉。解决:对目标做log1p变换,评估用 RMSLE,预测时再expm1还原。如果业务上「少排骑手」比「多排骑手」更贵,那还应该用非对称损失或者分位数回归,直接预测一个偏高的分位数。
6. 评估与取舍:指标选择本身就是一次业务建模
6.1 为什么这个任务该看 RMSLE 而不是 RMSE
订单量是个右偏、量级跨度极大的目标:凌晨某些区域 1 单,午高峰能到 200 单。RMSE 在平方项上放大误差,会被大值样本主导,模型会为了压低高峰的绝对误差而牺牲低峰时段的相对精度,结果就是低峰时段的预测完全不可用。
RMSLE 在对数空间上计算,等价于看相对误差,各量级样本的贡献被拉平。它还有个隐含效果:对低估的惩罚比对高估更重。直接从公式就能看出来,当实际值小时,log(1+pred) - log(1+actual)的相对变化更剧烈。
import numpy as np def rmsle(y_true, y_pred): # 必须先裁剪掉负值,否则 log 无定义;树模型和 KNN 都可能输出负数 y_pred = np.clip(y_pred, 0, None) return float(np.sqrt(np.mean((np.log1p(y_pred) - np.log1p(y_true)) ** 2)))收紧负值是必须的。KNN 在weights="distance"下不会产生负值,但随机森林在极端外推时偶尔会给出负数,直接送进 log 会变成 NaN 并污染整个评估结果。
6.2 KNN 和随机森林各自适合什么,怎么选
把两条路线放在同一张表里比一下,取舍就很清楚了。
| 维度 | KNN 回归 | 随机森林回归 |
|---|---|---|
| 训练成本 | 几乎为零,只存索引 | 高,500 棵树在小数据集上也要分钟级 |
| 预测成本 | 随样本量线性增长 | 固定,O(树数 × 深度) |
| 增量更新 | 直接追加邻居即可 | 需要重训或增量树 |
| 特征工程敏感度 | 极高,依赖缩放和距离度量 | 中等,对量纲不敏感 |
| 外推能力 | 无,预测值不会超出历史范围 | 同样受限,树是分段常数 |
| 可解释性 | 弱,只能回溯邻居 | 较强,有特征重要性 |
一个很实际的组合方式是:把 KNN 的预测值当成随机森林的一个输入特征。KNN 擅长捕捉局部相似性(「上周同一时段、类似天气、类似区域的订单量」),随机森林擅长在全局上组合多维度信息。两者叠加,在验证集上通常比任何单一模型稳,尤其是新开区域这种历史样本少的场景,KNN 的局部邻居能补上全局模型的冷启动缺口。
from sklearn.model_selection import KFold import numpy as np # 用 K 折给训练集生成 out-of-fold 的 KNN 预测,避免这列特征本身泄漏 oof_knn = np.zeros(len(X_train)) kf = KFold(n_splits=5, shuffle=False) # 注意:fold 之间仍要保持时间顺序 for tr_idx, va_idx in kf.split(X_train): knn.fit(X_train.iloc[tr_idx], y_train.iloc[tr_idx]) oof_knn[va_idx] = knn.predict(X_train.iloc[va_idx]) X_train_aug = X_train.assign(knn_pred=oof_knn) rf.fit(X_train_aug, y_train)shuffle=False是关键。如果这里用随机 KFold 生成 out-of-fold 预测,KNN 的邻居里仍然会有未来样本,这列特征就带上了泄漏,而它又是随机森林最重要的输入之一,整条链路会一起失效。
6.3 落到取舍:选模型其实是在选时间的表示方式
回头看这几章其实在讲同一件事:需求预测的精度上限由特征决定,模型只是在既定特征空间里找一个尽量好的解。KNN 把全部赌注押在距离度量上,所以它对缩放、编码、共线极其敏感,稍微一个量级失衡就整体失效;随机森林对量纲不敏感,但它对叶子大小、特征相关性这些结构参数敏感,而且它对右偏目标的系统性低估需要专门处理。
我一般会用三个判据来做最终决定。第一,看分时段误差而不是总体指标:把验证集按高峰、平峰、凌晨切开,如果某个模型在总体 RMSLE 上领先但凌晨段崩了,那它在排班场景里就是不可用的。第二,看冷启动表现:新开区域没有历史 lag,KNN 靠邻居还能给出一个粗估,随机森林只能回退到全局均值。第三,看更新频率:如果业务要求小时级更新,KNN 的增量特性更有优势;如果是一天重训一次,随机森林的稳定性更划算。
验证方法上,我会固定一份按时间切分的 holdout,任何改动——加特征、调参、换模型——都必须在这份 holdout 上过一遍,并且要求改进幅度超过噪声水平才算数。这份 holdout 一旦确定就不再动,用它来防止自己在验证集上反复调参调到过拟合。
从那以后我每做一次时间序列的特征改造,都强制先跑一遍时间切分的 holdout,再去看总体指标,这个习惯救回来的上线事故不止一次。希望帮到你。
这份资源把时间片聚合、滞后与滑动窗口构造、KNN 和随机森林两套回归实现、以及按时间切分的评估脚本放在了一起,可以拿去对着改自己的特征口径,也可以直接把两套模型跑在同一份数据上做对比。
本文还有配套的精品资源,点击获取