简介这份压缩包围绕“用机器学习分析双色球”展开面向希望把数据处理、特征工程与模型训练串成完整实战流程的初学者和进阶学习者也可作为彩票数据探索与随机性认知的案例材料。包内共29个文件以16个Python脚本为主体辅以SQL建表与入库脚本、JSON数据文件、Markdown说明、PNG图表及Pipfile依赖配置压缩包约181KB覆盖数据采集、清洗转换、训练日志和可视化等环节。已有385人学习下载说明该主题具有一定参考热度。读者可从中了解历史开奖数据的组织方式、号码编码与特征构造思路、TensorFlow与PyTorch等框架的入门练习以及模型训练评估和结果解释的基本路径同时能体会彩票结果的随机性与不可预测性把它当作机器学习项目练手和流程拆解的参考而非中奖保证。1. 先泼盆冷水机器学习分析双色球能做的和不能做的有个做后端的朋友拿两万多期历史开奖记录喂了个模型回测里蓝球命中率刷到 11%兴奋了三天改成严格滚动预测后掉回 6%跟闭眼瞎猜一模一样。这种落差在双色球数据分析里太常见了。这个 zip 里的项目并不承诺帮你中奖它给的是一条能完整跑通的机器学习流水线原始数据解析、特征工程、红球多标签建模、蓝球二分类与概率校准、滚动回测、结果落盘。适合三类人想把 pandas、sklearn、LightGBM 从头到尾串一遍的练手者想搞懂时间序列回测到底该怎么切窗口的人以及拿它当数据分析教学样例的人。想靠它算出下一期号码的趁早调头前面只有玄学。2. 工程结构拆解一个 zip 里到底装了什么解压之后不是一堆散落的脚本而是一个可以反复重跑的小工程。我拆包后做的第一件事是把目录树打印出来对着看职责边界。这类项目最容易失控的地方是“数据清洗”和“特征构造”糊在一个文件里改一行特征就顺带把解析逻辑弄崩。这个包在结构上把两者分开了值得保留。2.1 目录树与模块职责lottery-ml/ ├── configs/ │ ├── default.yaml # 主配置数据路径、特征窗口、模型参数 │ └── fast.yaml # 小样本快速验证用跑通链路先跑这个 ├── data/ │ ├── raw/draws.csv # 原始开奖记录一行一期 │ ├── interim/ # 清洗后的中间态可随时删掉重生成 │ └── processed/ # 特征矩阵 标签训练直接读这里 ├── src/ │ ├── data/build_dataset.py # 解析、校验、拆列 │ ├── features/feature.py # 遗漏值、冷热号、形态统计 │ ├── models/red_model.py # 红球 33 个二分类器 │ ├── models/blue_model.py # 蓝球二分类 校准 │ ├── backtest/rolling.py # 滚动窗口回测 │ ├── train.py # 训练入口 │ └── predict.py # 预测入口 ├── notebooks/ # 探索性分析随便改不影响主链路 ├── outputs/ # 模型文件、指标 json、预测 csv └── requirements.txt模块职责你会改它的频率src/data/原始 csv 到结构化宽表换数据源时才动src/features/特征矩阵构造最高天天改src/models/训练与推理中换模型时动src/backtest/评估协议低但改错最致命configs/参数外置高但只改数值从职责切分能看出作者的设计意图data层只保证“一行一期、字段干净”features层只保证“每一行特征时间戳早于标签”models层拿到的是纯数值矩阵。这样你换模型不影响数据管道改特征也不影响回测协议。2.2 数据层把历史开奖记录落成一张宽表原始数据一般是“期号、日期、红球一串、蓝球一个”的形态红球用空格或逗号分隔中间夹杂着补录的脏行。构建脚本干的就是把这种半结构化文本压成r1...r6 blue的定长宽表顺带做合法性校验。import pandas as pd def parse_draw(raw: pd.DataFrame) - pd.DataFrame: 把一行一条的原始开奖记录拆成一行一期、61 列的结构化宽表 # 红球先统一分隔符再切兼容空格、逗号、顿号三种写法 reds raw[red].astype(str).str.replace(r[,、], , regexTrue).str.split() rec pd.DataFrame({ issue: raw[issue].astype(str).str.strip(), date: pd.to_datetime(raw[date], errorscoerce), reds: reds.apply(lambda x: sorted({int(v) for v in x if v.isdigit()})), blue: pd.to_numeric(raw[blue], errorscoerce), }) # 三类脏数据直接丢红球数量不对、有重复被去重后不足 6 个、日期解析失败 rec rec[rec[reds].apply(len) 6] rec rec.dropna(subset[date, blue]) # 范围校验防止把别的彩种数据混进来 rec rec[rec[reds].apply(lambda x: x[0] 1 and x[-1] 33)] rec rec[(rec[blue] 1) (rec[blue] 16)] # 期号去重后按时间升序后面的滚动回测强依赖这个顺序 rec rec.drop_duplicates(subsetissue).sort_values(date).reset_index(dropTrue) # 展开成 r1..r6升序排列后位置本身没有语义这点后面建模会用到 rec[[fr{i} for i in range(1, 7)]] pd.DataFrame(rec[reds].tolist(), indexrec.index) return rec.drop(columnsreds)parse_draw里最关键的不是切分而是最后那句sort_values(date)。后面的特征工程用shift和rolling表达“过去 N 期”一旦行序乱了整个时间轴就错位模型看起来能训指标全是假的。另一个细节是红球排序双色球开奖时红球是按开奖顺序出的但业务上组合无序排序之后r1..r6只表示大小关系不表示位置这直接决定了 3.2 节为什么不能用六个独立的分类头。跑完这一步data/interim/draws.parquet就是全项目唯一的“事实表”之后所有特征都从它派生。我一般会在这里加一个断言把期号数量和头尾日期打印出来跟原始文件对一眼再往下走。2.3 配置与依赖config.yaml 是唯一需要动的地方参数不写死在代码里是这个包比大多数练手项目讲究的一点。所有窗口长度、模型超参、回测折数都收在 yaml 里调参不用改 Python也就不会手滑改坏逻辑。data: raw_path: data/raw/draws.csv start_issue: 2013001 # 早期数据字段缺失多一般从这之后开始 min_rows: 500 # 样本太少直接拒绝训练防止跑出无意义指标 features: lags: [1, 3, 5, 10, 30] # 滞后特征的回看阶数 hot_window: 30 # 热号统计窗口 cold_window: 100 # 冷号统计窗口 roll_windows: [10, 50] # 滚动均值/方差窗口 model: type: lightgbm params: num_leaves: 31 learning_rate: 0.03 n_estimators: 600 min_child_samples: 40 cv: scheme: rolling # 只支持 rollingrandom 会泄漏未来 n_splits: 12 gap: 1 # 训练集末尾与验证集之间空一期缓冲特征窗口渗透gap这个参数很多人第一眼看不懂。特征里用了 100 期冷号窗口意味着第 i 行的特征实际“看到”了第 i-100 到 i-1 期。如果训练集截至第 i-1 期、验证集从第 i 期开始边界上那几行特征和验证标签存在微弱重叠gap: 1就是给这个渗透留一格缓冲。数值不大但做时序评估时该留。min_rows是我加的习惯样本少于阈值时直接抛异常而不是训出一个 loss 好看、泛化为零的模型。血泪经验宁可报错也别出一个假指标。3. 核心代码逐段讲从特征矩阵到滚动回测的主链路数据洗干净只是开始真正决定这个项目有没有分析价值的是三件事特征构得对不对、标签定义得清不清、评估协议守不守规矩。下面按调用顺序拆。3.1 特征构造遗漏值、冷热号、形态统计彩票类数据最大的坑是特征看起来很多、信息量却极低。这个包采用了三类相对克制的特征形态统计和值、跨度、奇偶比、逐号遗漏值某个号码距上次开出过了几期、窗口热度最近 N 期出现次数。三类都不是“预测未来”而是对历史分布的压缩描述。import numpy as np import pandas as pd def build_features(df: pd.DataFrame, cfg: dict) - pd.DataFrame: 输入按时间升序的开奖宽表输出与 df 行对齐、且只用历史信息的特征表 reds df[[fr{i} for i in range(1, 7)]].to_numpy() n len(df) out df[[issue, date]].copy() # 1) 形态类整注号码的统计摘要 out[sum_val] reds.sum(axis1) out[span] reds.max(axis1) - reds.min(axis1) out[odd_cnt] (reds % 2 1).sum(axis1) out[big_cnt] (reds 16).sum(axis1) # 大小比按 1-16 / 17-33 切 out[zone1] (reds 11).sum(axis1) # 三区分布 out[zone3] (reds 23).sum(axis1) # 2) 逐号遗漏值miss[i][j] 表示第 i 期开奖前号码 j1 已经多久没出 miss np.zeros((n, 33), dtypenp.float32) last_seen np.full(33, -1, dtypenp.int32) for i in range(n): miss[i] np.where(last_seen 0, i 1, i - last_seen) for num in reds[i]: last_seen[num - 1] i # 整体下移一行保证第 i 行看到的是「本期开奖之前」的遗漏 miss np.vstack([np.full((1, 33), np.nan, dtypenp.float32), miss[:-1]]) miss pd.DataFrame(miss).ffill().fillna(0).add_prefix(miss_) # 3) 窗口热度最近 hot_window 期每个号码出现了几次同样 shift 一位 onehot np.zeros((n, 33), dtypenp.float32) onehot[np.arange(n)[:, None], reds - 1] 1 hot (pd.DataFrame(onehot) .rolling(cfg[features][hot_window], min_periods1).sum() .shift(1).fillna(0).add_prefix(hot_)) return pd.concat([out.reset_index(dropTrue), miss, hot], axis1)两个shift是整段代码的命门。遗漏值那段的np.vstack把整块矩阵下移一行热度用.shift(1)效果一样第 i 行的特征只能包含第 i 期之前的信息标签是第 i 期的开奖结果。少了这两下模型会拿到“当期号码出现了几次”这种答案提示训练 loss 直接掉到 0.1 以下然后实盘一塌糊涂——这是这个项目最容易翻车的地方也是我做时序特征时唯一会强制写断言的地方。参数上hot_window调小10~30抓到的是近期波动调大100~300会趋近全局均匀分布因为每个号码的长期频率都在 6/33 ≈ 18% 附近。roll_windows里的滚动均值和方差我一般只保留两个窗口就够特征维度堆到几百列对这类数据只增加过拟合风险。顺带说一句树模型不需要对这几类特征做标准化遗漏值几十、和值上百对分裂点的选择没有影响。真要上线性模型或者神经网络得先做分位数变换不然量纲会把梯度带偏。3.2 红球33 个独立二分类器而不是 6 个分类头红球是 33 选 6 且不分顺序。有两种常见建模方式一是当成 33 类多标签问题每个号码一个 0/1 标签二是当成“第 k 个球”的 6 个 33 类分类问题。第二种是典型误用——红球排序之后r1..r6只是大小名次第 3 个球并不比第 5 个球有更多语义硬拆成六个头等于给模型灌错误的结构先验。这个包选了第一种。from lightgbm import LGBMClassifier import numpy as np def train_red_models(X: np.ndarray, Y: np.ndarray, cfg: dict) - dict: Y: (n_samples, 33) 的 0/1 矩阵第 j 列表示号码 j1 是否开出 params dict(cfg[model][params]) models {} for j in range(33): y Y[:, j] pos_rate y.mean() # 正样本太少时退化为常数先验训出来的树没有统计意义 if y.sum() 30: models[j] (prior, float(pos_rate)) continue clf LGBMClassifier(objectivebinary, random_state42, **params) clf.fit(X, y) models[j] (lgbm, clf) return models def predict_red_proba(models: dict, X: np.ndarray) - np.ndarray: 输出 (n_samples, 33) 的边际概率矩阵注意它不是一注号码 proba np.zeros((len(X), 33), dtypenp.float32) for j, (kind, obj) in models.items(): if kind prior: proba[:, j] obj else: proba[:, j] obj.predict_proba(X)[:, 1] # 每期强制归一化到 6 个球保持与真实分布同量级方便回测对比 proba * 6.0 / proba.sum(axis1, keepdimsTrue) return proba每个号码的正例率大约是 6/33 ≈ 18.2%不算极端不平衡所以没上scale_pos_weight直接binary就行。33 个模型串行训练600 棵树 × 33单机跑完几分钟成本可以接受想快就把n_estimators降到 200 先看趋势。predict_red_proba末尾那行归一化很有必要但容易被误解它只是把 33 个边际概率缩放到总和为 6量级上对得上“一期出 6 个球”不代表这 33 个数能直接拼成一注有效组合。真正取一注还得做无放回抽样或者按约束求解这部分放到第 6 章讲。很多人看到概率矩阵就直接argsort取前 6结果每期输出一模一样以为是模型没训好其实是取法错了。3.3 蓝球二分类加概率校准蓝球是 16 选 1单标签多分类用predict_proba拿 16 个类别的概率最直接。但要注意项目里默认走的是“16 个独立二分类”还是“单模型 16 分类”取决于数据量样本上万时直接multiclass更稳几千条时独立二分类反而收敛得快。无论哪种概率都得校准否则输出的 0.4 和真实频率 0.4 对不上后面的组合抽样就是自欺欺人。from sklearn.calibration import CalibratedClassifierCV from sklearn.metrics import brier_score_loss, log_loss import numpy as np def train_blue(X_tr, y_tr, X_va, y_va, base_clf): isotonic 校准需要足够样本样本少时用 sigmoid method isotonic if len(X_tr) 3000 else sigmoid clf CalibratedClassifierCV(base_clf, methodmethod, cv3) clf.fit(X_tr, y_tr) p clf.predict_proba(X_va)[:, 1] base np.full_like(p, y_tr.mean()) # 全猜训练集先验的基线 print(flogloss model{log_loss(y_va, p):.4f} fbase{log_loss(y_va, base):.4f}) print(fbrier model{brier_score_loss(y_va, p):.4f} fbase{brier_score_loss(y_va, base):.4f}) return clf基线一定要打出来。16 类均匀分布的对数损失是 ln16 ≈ 2.77全猜某个固定号码的基线大约也是这个量级。如果模型 logloss 是 2.75看着“比基线好”实际差距在噪声范围内没有任何实用意义。我见过太多人只贴一个accuracy: 0.07就宣布模型有效而 1/16 6.25%7% 和 6.25% 的差别在几千条验证样本上根本不是显著差异。Brier 分数比准确率更能反映概率质量值越小越好均匀猜的 Brier 约 0.0586对二分类而言这两个数放在一起看就能判断模型到底学到了东西还是只学到了先验。3.4 回测滚动窗口和必须对齐的基线回测协议是整个项目里最不能省的部分。随机切分train_test_split(shuffleTrue)在这类数据上会把未来样本混进训练集指标虚高得离谱。这个包只支持滚动窗口每一折训练集都在验证集之前。import pandas as pd import numpy as np def rolling_backtest(df: pd.DataFrame, cfg: dict, fit_predict) - pd.DataFrame: fit_predict(train_df, test_df) 内部自行构造特征外部不传任何全量统计量 n_splits cfg[model][cv][n_splits] gap cfg[model][cv][gap] step len(df) // (n_splits 1) rows [] for k in range(n_splits): train_end step * (k 1) test_start train_end gap test_end min(test_start step, len(df)) if test_start len(df): break tr df.iloc[:train_end] te df.iloc[test_start:test_end] preds fit_predict(tr, te) # 返回每期的概率矩阵与实际号码 rows.append(evaluate(preds, te)) return pd.DataFrame(rows).agg([mean, std]).T def evaluate(preds: dict, te: pd.DataFrame) - dict: 核心指标红球平均命中个数与超几何分布期望 6*6/33≈1.09 对比 hits [] for proba, actual in zip(preds[red_proba], te[[fr{i} for i in range(1, 7)]].to_numpy()): top6 np.argsort(-proba)[:6] 1 hits.append(len(set(top6) set(actual))) return {red_hit_mean: float(np.mean(hits)), n: len(hits)}evaluate里的 1.09 这个基线值得记住随机选 6 个红球命中个数的数学期望是 6 × 6/33 ≈ 1.09。模型如果做到 1.15只能说明它在“选热门号”这件小事上略微有效——因为热门号的出现频率确实长期稳定在 18% 附近而真实分布并不均匀。做到 1.3 以上才值得怀疑是不是泄漏了而不是庆祝。fit_predict的签名设计是刻意为之训练集和测试集分开传进去特征构造在函数内部对tr单独做。这样任何“用全量数据算均值再 transform”的写法都会在代码审查时暴露出来。做时序回测接口设计比纪律更可靠。4. 把项目跑起来三条命令和四个输出目录结构看明白了接下来是实打实的环境。这类项目依赖不重主要是 pandas、numpy、scikit-learn、lightgbm加个 pyarrow 读 parquet。真正卡人的往往不是装包而是数据文件和路径。4.1 环境安装与数据准备建议单独建虚拟环境别往系统 Python 里灌。LightGBM 在部分平台上需要编译工具链用 wheel 装最省事。# 1) 建环境Python 3.9 均可 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate # 2) 装依赖版本锁在 requirements.txt 里 pip install -r requirements.txt # 3) 准备原始数据放到 configs/default.yaml 里配置的路径 mkdir -p data/raw # 把整理好的开奖记录存成 data/raw/draws.csv至少要 issue,date,red,blue 四列 # 4) 先跑最小配置验证链路几百条数据也能走通 python -m src.data.build_dataset --config configs/fast.yaml原始 csv 至少要有issue、date、red、blue四列红球用空格或逗号分隔都行解析函数两种都兼容。如果你的数据里日期格式不统一先在parse_draw里补format参数别指望to_datetime自动猜对所有情况——errorscoerce会把解析失败的行静默变成 NaT然后被dropna悄悄丢掉数据少了一截你还不一定发现。fast.yaml存在的意义就是先跑通再上量。我第一次解这类包时习惯直接上全量结果卡在一个字段类型上折腾半小时用fast.yaml二十秒就能定位是环境问题还是数据问题。4.2 三个命令行入口主链路就三条命令分别对应构建、训练、预测回测是训练之后的可选步骤。# 构建特征矩阵输出到 data/processed/ python -m src.data.build_dataset --config configs/default.yaml # 训练红球 33 个二分类器 蓝球模型模型存到 outputs/models/ python -m src.train --config configs/default.yaml --model lightgbm # 滚动回测折数可在命令行覆盖 yaml 里的配置 python -m src.backtest --config configs/default.yaml --folds 12 # 对下一期做推理输出概率矩阵和候选组合 python -m src.predict --config configs/default.yaml --topk 12--model参数支持lightgbm、xgboost、logistic三个值最后一个用来跑基线很方便。--folds覆盖配置而不是改文件是我比较推荐的做法实验参数走命令行工程参数留在 yaml两者职责别混。--topk 12的意思是输出概率最高的 12 个红球候选从里面组合出若干注。这个数别调太大超过 18 之后候选集几乎覆盖全部号码输出就没信息量了。4.3 输出物与日志排查跑完之后outputs/下会出现这些东西每个都有明确用途路径内容什么时候看outputs/models/red/*.txt33 个 LightGBM 模型文件换机器复现时确认版本一致outputs/models/blue.pkl校准后的蓝球模型推理前必须存在outputs/metrics/backtest.json每折 logloss、Brier、平均命中数判断模型有没有学到东西outputs/metrics/feature_importance.csv33 个模型的平均增益砍冗余特征outputs/predict/next.csv下一期概率矩阵 候选组合日常输出的结果日志级别用--log-level DEBUG打开会打印每一折的训练区间和验证区间排查泄漏时非常有用。feature_importance.csv我一般看前 20 行如果排在最前面的全是hot_*和miss_*说明模型主要在学号码频率如果形态特征和值、跨度排得很高反而要警惕这类特征和具体号码没有因果关系多半是在拟合噪声。5. 踩坑排查这套代码在我机器上翻过的五次车跑通只是开始能不能得到有意义的结论是另一回事。下面五条都是我实际遇到过的问题按现象、原因、解决记下来照方抓药能省不少时间。5.1 数据与环境两个看起来像 bug 的配置问题现象一换台机器跑训练明明成功了预测时报KeyError: miss_1。原因特征矩阵用 parquet 落盘不同 pandas 版本对列顺序和索引的恢复行为不一致加上部分环境没装 pyarrow静默退回了 csv索引列被当成了普通列。 解决在build_dataset末尾显式df df[sorted(df.columns)]固定列序读文件时显式指定encodingutf-8-sig并把 pyarrow 写进 requirements。自从踩过这次坑之后凡是落盘的特征表我都固定列序再存。现象二build_dataset跑完没报错但样本数比原始文件少了两千多条。原因pd.to_datetime(..., errorscoerce)把格式异常的日期变成 NaT紧接着的dropna把它们全删了日志里没有任何提示。 解决解析后先打印raw_len和clean_len的差值超过 1% 就raise。数据静默丢失是最难查的一类问题宁可中断也别默默少一截。5.2 模型层标签泄漏和错误的取法现象三训练集 AUC 0.78验证集 AUC 0.51一路横着走。原因特征里混进了当期开奖的统计量。最隐蔽的一处是遗漏值代码里遍历reds[i]更新last_seen之后没有把结果整体下移第 i 行拿到的是“本期开完之后”的遗漏。 解决在特征构造函数末尾加断言检查任意一行的特征是否与当期号码相关最直接的办法是构造一个仅用df.iloc[:i]重算的参照值做比对。这条断言我后来写成了模板所有时序特征一律带。现象四每期预测出来都是同一组 6 个红球连续跑十期一模一样。原因predict_red_proba输出的边际概率矩阵直接argsort取前 6等于每期都取边际概率最高的那批号码而边际概率本身随时间变化很慢。 解决改成按概率无放回抽样或者加上和值、奇偶、区间分布的约束后求解让输出体现模型的不确定性。这件事说明概率矩阵和“一注号码”之间还差一层采样逻辑别混为一谈。5.3 回测与结果解读别拿训练集成绩当结论现象五回测平均命中 2.4 个红球激动了半小时换时间段复现掉到 1.1。原因features里的滚动均值和冷热窗口在切分之前对全量数据算好了等于用未来数据构造了历史特征另外回测只有 3 折样本量太小2.4 这个数字本身就是噪声。 解决所有窗口统计量只在训练窗口内fit验证窗口只做transform折数提到 12 折以上每次必须打印超几何基线 1.09 做对比。做完这三步命中数回到 1.1 附近才是真实水平。另外提醒一句蓝球的accuracy 0.07这类指标单独看没有意义1/16 6.25%样本量少的时候 7% 完全在随机波动范围内。把 logloss 和 Brier 一起打出来再跟均匀分布基线并排放结论才站得住。6. 二次开发把它改成正经的时间序列实验框架这个包底子不错但要真拿来做实验有几处必须改。第一处是把 33 个独立二分类换成多标签模型MultiOutputClassifier或者 LightGBM 的multiclass直接对 33 个位置做联合建模边际概率之间会互相约束输出更合理。第二处是加一层组合采样把概率矩阵变成可解释的候选注数。import numpy as np def sample_ticket(proba: np.ndarray, k: int 6, rngNone) - list[int]: 按边际概率无放回抽样生成一注比 argsort 取 top-k 更能反映不确定性 p np.asarray(proba, dtypenp.float64) p np.clip(p, 1e-6, None) p p / p.sum() idx rng.choice(np.arange(1, 34), sizek, replaceFalse, pp) return sorted(idx.tolist()) def sample_tickets(proba, n_tickets5, k6, seed7): rng np.random.default_rng(seed) seen, out set(), [] while len(out) n_tickets: t tuple(sample_ticket(proba, k, rng)) if t not in seen: # 去重避免输出重复注 seen.add(t) out.append(list(t)) return outsample_ticket的关键在replaceFalse无放回抽样天然保证一注里号码不重复比先取 33 个概率再过滤重复组合要干净得多。clip防止某些号码概率被校准压到 0 之后抽样报错。seed固定是为了让同一次实验可复现出报告时能对得上。改造清单按优先级排一下改造点现状建议做法模型结构33 个独立二分类换多标签联合建模或加号码间相关性特征采样层argsort取 top-k无放回抽样 约束过滤和值区间、奇偶配比评估单点命中数加概率校准曲线、分位数覆盖率特征全是号码频率类引入外部变量销售数据、时间间隔做对照实验实验管理手动改 yaml接 mlflow 或简单的 run_id 目录每次实验独立落盘我最想强调的还是评估协议。改完模型先别急着看命中数把校准曲线画出来横轴预测概率、纵轴实际频率理想情况是一条 45 度直线。如果模型输出的 0.3 对应实际 0.18那后面的组合采样全在放大偏差。这套流程我从一个翻了车的项目里学来的当时模型指标好看上线后全线崩查了两天才发现是校准没做概率全挤在 0.5 附近。从那以后我每次做概率输出都强制先过一遍校准曲线和 Brier 分数再谈业务指标。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?