简介:《原神抽卡模拟器.zip》面向原神玩家与C++初学者,提供一套可运行的祈愿过程模拟程序,用于复刻抽卡概率、保底计数与结果展示,帮助理解游戏机制及C++随机数、类和对象等编程实践。压缩包共38个文件、约4.04MB,包含cpp与hpp源码、h头文件、sln与vcxproj工程配置、exe可执行文件、pdb/obj等调试构建产物,以及txt说明、日志和md文档,便于直接运行、阅读源码和排查构建过程。目前已有1606人学习下载。读者可从中获取抽卡池定义、随机数生成、保底逻辑和统计展示等实现思路,也可参考工程目录组织与Visual Studio配置,适合课程设计、C++练手或原神抽卡机制兴趣研究。
1. 抽卡模拟器不是随机数练习:一份 C++ 源码包能验证什么
十连下去全是三星,很多人第一反应是“概率被暗改了”。真要验证这句话,靠感觉没用,得有一份能跑一百万次、把分布画出来的东西。这个 C++ 抽卡模拟器源码包干的就是这件事:它把二次元卡池里那套硬保底、软保底概率爬坡、大小保底(歪了下次必中)、四星十连保底、抽卡货币消耗全部拆成独立模块,然后做蒙特卡洛实验,输出平均出金抽数、分位数和出金率。
它适合三类人:想复盘自己账号运气到底正不正常的玩家、要做概率分布实验的学生、以及想找一个结构干净、能编译能跑的 C++ 项目练手的开发者。改一行配置就能换卡池,跑一遍十万抽只要几秒。
2. 拆包先看骨架:C++ 抽卡模拟器的三层对象与卡池配置落地
拿到一个陌生源码包,我一般不看 main,先看头文件里有哪些 struct——数据结构定死了,后面的逻辑基本就跑不出那个范围。这个抽卡模拟器的结构算是教科书式的分层:配置层(卡池权重、保底规则)、状态层(玩家保底计数、货币、历史记录)、抽样层(随机源 + 判定逻辑),最后一层是统计输出。
这个分层不是为了好看,而是为了让“改参数”和“改逻辑”互不干扰。你把五星基础概率从 0.006 改成 0.008,只动配置;你想换一种软保底曲线,只动抽样层的一个函数。如果全写在一个 main 里,跑出来的数你根本不知道是参数变了还是逻辑变了。
2.1 目录与核心对象:卡池、玩家状态、随机源三条线
解压后大致是这样一套文件分布:
| 文件 | 职责 | 改动频率 |
|---|---|---|
main.cpp | 命令行参数解析、实验循环、结果落盘 | 低 |
pool_config.hpp/.cpp | 卡池配置加载与校验 | 高 |
gacha.hpp/.cpp | 稀有度判定、保底状态机、UP 判定 | 中 |
player.hpp | 玩家状态:保底计数、货币、历史 | 低 |
rng.hpp | 随机源封装 | 几乎不动 |
stats.cpp | 均值、分位数、出金率统计 | 低 |
data/pool.json | 权重表与卡池规则 | 高 |
状态层是整个模拟器的核心。它长这样:
enum class Rarity { R3, R4, R5 }; struct PlayerState { int pity5 = 0; // 距离上次五星已经抽了多少次 int pity4 = 0; // 距离上次四星已经抽了多少次 bool guaranteedUp = false; // 上次五星是否歪了,歪了这次必中 UP long long currency = 0; // 剩余抽卡货币 std::vector<PullRecord> history; // 每次抽卡的结果,用于事后统计 };三个字段的含义要说清楚,不然很容易写反。pity5是“已经连续多少次没出五星”,抽一次加一,出了五星立刻清零。guaranteedUp不是计数器而是布尔标记,它只在“出了五星但歪了”的时候置为 true,在“出了五星且命中 UP”之后清回 false。这两个状态的更新时机完全不一样,一个是每抽都动,一个是一个保底周期才动一次,混在一起写就是灾难。
历史记录用std::vector存全量,一百万抽大概占几十 MB,跑统计够用。如果你要跑上千万抽做长期回归,建议改成只累加统计量、不存明细,或者按批次落盘后清空——这一点在第四章讲性能时还会提到。
2.2 卡池配置怎么落到代码里:权重表和 50/50 的拆解
配置项我建议全部堆在一个结构体里,别用散落的全局常量。原因是标定参数的时候你会反复改,集中在一个地方才好做扫描。
struct PoolConfig { double base5 = 0.006; // 五星基础概率 0.6% double base4 = 0.051; // 四星基础概率 5.1% int hardPity5 = 90; // 硬保底:第 90 抽必出五星 int softPityStart= 74; // 软保底起始抽数 double softPityStep = 0.06; // 软保底每抽递增 6 个百分点 int pity4Limit = 10; // 四星十连保底 double upRate5 = 0.5; // 小保底歪的概率,50/50 };几个参数值得单独说。base5和base4是互斥概率,不是独立概率——抽一次卡只有一次“定稀有度”的机会,三星的概率是1 - base5 - base4再扣掉保底修正后的余量。这一点新手最容易搞错,后面避坑章有一条专门讲。upRate5 = 0.5表示出五星时有五成概率歪到常驻,配合guaranteedUp标记,长期看 UP 占五星总数的三分之二,这个数后面还会出现。
softPityStart = 74和softPityStep = 0.06是整个模型里最敏感的两个参数。改动 0.01,平均出金抽数会掉将近一抽,所以做对比实验时一定要固定住另一个。
2.3 编译与首次跑通:三条命令拿到基线分布
源码包用 CMake 组织,Release 构建是必须的。Debug 模式下这种纯计算循环能慢 10 倍以上,跑一千万抽你会以为程序卡死了。
# 解压并进入目录 unzip gacha-sim.zip -d gacha-sim cd gacha-sim # Release 构建,-O2 由 CMakeLists 里的 CMAKE_BUILD_TYPE 控制 cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j # 跑一百万抽,固定种子,结果落 CSV ./build/gacha_sim --pulls 1000000 --seed 20240501 --out result.csv逐条说。-DCMAKE_BUILD_TYPE=Release决定优化等级,如果是单文件编译,等价于g++ -O2 -std=c++17 main.cpp gacha.cpp -o gacha_sim。--pulls是实验总量,建议第一次就跑一百万,十万抽的波动肉眼可见,容易误判。--seed固定种子,保证你和我跑出来的数一模一样,这是排查问题的前提。--out输出 CSV,字段至少要有trial_id, pull_index, rarity, pity_at_hit, is_up。
首跑完,控制台会打印一行汇总:平均出金抽数、中位数、80 抽内出金率。用 0.6% 基础概率 + 74 起每抽 +6% 的模型,平均出金应该落在 62 抽附近,中位数 76 抽左右,80 抽内出金率 92% 上下。如果你的结果离这三条差得远,先别急着怀疑源码,去看第三章的判定顺序。
3. 保底状态机与软保底爬坡:把 0.6% 到 100% 的曲线写对
抽卡概率模型最容易出错的地方不是随机数,是判定顺序和状态更新时机。一个逻辑正确的实现,跑出来的分布应该能用手算对上一半——至少平均出金抽数和中位数这两项,纸笔加一个等比数列就能验。
先说清楚这套模型的形状。前 73 抽,每抽五星概率恒为 0.6%,累计出金概率按1 - 0.994^n爬升,到第 73 抽大约 35.6% 的人已经出过金了。从第 74 抽开始,每抽额外加 6 个百分点:第 74 抽是 6.6%,第 75 抽 12.6%,一路涨到第 90 抽被硬保底强制拉到 100%。这条爬坡曲线决定了绝大多数人是在 74 到 82 抽之间出货的,而不是均匀分布在 1 到 90。
3.1 硬保底用状态机,不用 if 堆
核心抽样函数建议长这样,一个函数里把三件事按顺序做完:
PullResult drawOne(PlayerState& p, const PoolConfig& cfg, Rng& rng) { ++p.pity5; ++p.pity4; // 1. 先算这一抽的五星概率,含软保底爬坡 double p5 = cfg.base5; if (p.pity5 >= cfg.softPityStart) { p5 += cfg.softPityStep * (p.pity5 - cfg.softPityStart + 1); } if (p.pity5 >= cfg.hardPity5) p5 = 1.0; // 硬保底直接拉满 if (p5 > 1.0) p5 = 1.0; // 双保险,别指望上面那行 // 2. 五星判定 if (rng.uniform() < p5) { bool isUp = p.guaranteedUp || (rng.uniform() < cfg.upRate5); p.guaranteedUp = !isUp; // 歪了下次必中 PullResult r{ Rarity::R5, p.pity5, isUp }; p.pity5 = 0; // 先记录再清零 p.pity4 = 0; return r; } // 3. 四星判定,含十连保底 bool hit4 = (p.pity4 >= cfg.pity4Limit) || (rng.uniform() < cfg.base4); if (hit4) { PullResult r{ Rarity::R4, p.pity4, false }; p.pity4 = 0; return r; } return PullResult{ Rarity::R3, 0, false }; }这段代码的关键点不在语法,在顺序。第一步先把概率算出来,第二步判五星,第三步判四星——三级判定必须是互斥的,一次抽样只定一个稀有度。如果写成“先判五星,再无条件判四星”,那一抽有 0.6% 加 5.1% 的独立两次机会,四星的实际出率会被五星吃掉一部分,总出率也会超标。正确的做法要么像上面这样短路返回,要么干脆用一次std::discrete_distribution按权重抽稀有度。
第二个关键点是p.pity5 = 0的位置。它在构造PullResult之后、返回之前。这行如果写在构造之前,CSV 里记录的pity_at_hit就永远是 0,你的分位数统计会全部失效,而且不报错——这是最难查的一类 bug。
第三个关键点是p5 > 1.0的夹取。理论上hardPity5 = 90那行已经把概率拉满了,但如果有人把hardPity调大到 100,或者softPityStep设成 0.1,第 88 抽算出来就是 1.5,uniform() < 1.5恒真,出货就提前了,平均抽数会被悄悄拉低。多写一行夹取,省一次排查。
3.2 软保底曲线:分段线性够用,别上花哨模型
软保底有两种常见写法。分段线性就是上面那种,从第 74 抽起每抽加固定百分点;指数形式是p(n) = base * r^(n - 73),需要反解一个r让第 90 抽接近 1。
我一般选分段线性,理由有三条:参数有明确物理意义(每抽多 6 个百分点,谁都听得懂)、调参时线性可控(改 0.06 到 0.07,平均抽数变化接近线性)、和公开的抽卡统计数据对得上。指数模型的拟合自由度更高,但你手上的观测样本往往只有几百抽,根本支撑不起两个参数的拟合,纯属给自己找麻烦。
两种模型跑出来的差异大致如下,这是我按 0.6% 基础概率、90 抽硬保底跑的基线值:
| 模型 | 平均出金抽数 | 80 抽内出金率 | 中位数 |
|---|---|---|---|
| 纯硬保底(无软保底) | 约 69.7 抽 | 约 38% | 约 69 抽 |
| 分段线性(74 起 +6%/抽) | 约 62 抽 | 约 92% | 约 76 抽 |
注意这张表里最反直觉的一点:加了软保底之后,平均抽数下降了 7 抽多,但中位数反而更高了。原因是软保底把“运气好提前出货”的那部分概率压薄了,曲线整体右移、尾部收缩。所以验证模型时只看平均数是不够的,必须看分位数,下一节讲。
参数修改的边界也要说清楚。softPityStart调到 60,平均出金会掉到 50 抽出头;调到 80,整个模型就和纯硬保底差不多了,软保底形同虚设。softPityStep低于 0.03 时爬坡太缓,第 89 抽才 50% 出头,会有大量样本撞硬保底;高于 0.1 时从第 74 抽开始就接近必出,分布被压成一根竖线。0.05 到 0.07 是合理区间。
3.3 用分位数验证曲线,别看平均数
跑完一百万抽,CSV 里每个五星记录一行,pity_at_hit就是这次出金花的抽数。用几行 Python 就能看出模型对不对:
import pandas as pd df = pd.read_csv("result.csv") pulls = df.loc[df["rarity"] == 5, "pity_at_hit"] print("样本数:", len(pulls)) print("均值:", round(pulls.mean(), 2)) # 打印关键分位数,和理论值对照 print(pulls.quantile([0.10, 0.25, 0.50, 0.75, 0.90, 0.99]).round(1))pity_at_hit这一列必须是清零前记录的值,也就是“本次出金一共花了多少抽”。跑一百万抽大约能得到一万一千个五星样本,分位数已经相当稳定了。对照值:中位数约 76 抽,P90 约 80 抽,P99 约 83 抽,P10 大约在 20 抽附近(前 73 抽是恒定 0.6%,早期出货全靠运气)。
如果 P10 明显偏高(比如 40 抽才有 10% 的人出货),说明前段的基础概率被什么东西压低了,多半是保底判定里把pity5的初值设成了 1 而不是 0,或者++p.pity5写在了判定之后。如果 P99 顶到 90,说明软保底爬坡没生效,所有倒霉样本都撞了硬保底。这两头一夹,中间那段的形状基本就对了。
4. 随机源与可复现:mt19937_64、种子扫描和加权抽样的性能账
概率模型写对之后,剩下的误差来源就只有随机数质量和样本量了。这两件事在抽卡模拟里格外重要,因为 0.6% 这种量级的概率对随机源的分辨率要求比一般场景高得多。
4.1 rand() 的坑:分辨率不够,位数不够
rand()在多数实现上返回 0 到RAND_MAX,而RAND_MAX常常是 32767。也就是说rand() % 10000只有一万个可能值,最小可分辨概率 0.01%。你要模拟 0.6% 的五星概率,理论上够用,但rand() % 10000的分布偏差在低概率区间会被放大,尤其是很多实现的低位周期性很差,做十连判定时会出现肉眼可见的聚集。
换成std::mt19937_64,并且自己把 64 位整数归一化成[0, 1)的 double,比调std::uniform_real_distribution更快也更可控:
class Rng { public: explicit Rng(uint64_t seed) : eng_(seed) {} // 取高 53 位,映射到 [0, 1),乘以 2^-53 double uniform() { return (eng_() >> 11) * 0x1.0p-53; } uint64_t raw() { return eng_(); } private: std::mt19937_64 eng_; };0x1.0p-53就是 2 的负 53 次方,用十六进制浮点字面量写出来是因为它精确。eng_() >> 11保留高 53 位,正好是 double 尾数的精度上限,再乘上去就得到[0, 1)上均匀分布的浮点数。这样每次抽样只有一次移位一次乘法,比构造uniform_real_distribution对象快一个量级。
要注意的是别把 distribution 对象写在函数里当局部变量。std::uniform_real_distribution在某些实现里带内部状态,每次构造还会做参数校验,一百万次循环里构造一百万次,纯属浪费。std::discrete_distribution更夸张,构造时要算一遍累积权重,O(n) 的开销,卡池权重表有几十项的话,这个开销能占到总耗时的三成以上。
4.2 种子管理:单次复现和多组扫描是两回事
种子这件事要分两个场景用。调逻辑的时候,固定一个种子,改一行代码跑一次,结果必须完全一样,否则你没法判断差异是代码改动带来的还是随机波动。做统计的时候,反过来要跑多组不同种子,看结果的离散程度,报告里带上置信区间。
单次复现很简单,命令行传--seed,程序内部只构造一个Rng对象,全程复用。多组扫描我一般用 shell 循环,每组十万抽,跑一百组:
mkdir -p runs for s in $(seq 1 100); do ./build/gacha_sim --pulls 100000 --seed $s --out runs/run_$s.csv done一百组跑完,用 Python 汇总各组的平均出金抽数,看均值的标准差。理论上单组十万抽的标准误大约是 0.06 抽,一百组均值的标准误会降到 0.006 抽。如果你的实测标准差远大于这个数,说明种子之间不独立——常见原因是拿时间戳的低位做种子,循环跑一百次拿到的是同一批种子,等于只跑了一组。
判断样本量够不够有个土办法:把一百万抽切成十份,分别算平均出金抽数,看这十个数极差有多大。极差超过 0.5 抽,说明样本量偏小,结论不可信;极差在 0.1 抽以内,可以拿去做参数对比了。
4.3 加权抽样与卡池分桶:把分布对象缓存起来
稀有度抽完之后,还要在稀有度内部决定出哪张卡。这里有两种做法:一是先抽稀有度,再在同稀有度的卡池里按权重抽具体卡;二是把整张卡池摊平,用一次加权抽样直接定卡。第一种可读性好,第二种性能好。
如果选第一种,std::discrete_distribution一定要放在卡池对象里做成员,构造一次复用到底:
class CardPool { public: CardPool(const std::vector<double>& weights, Rng& rng) : dist_(weights.begin(), weights.end()), rng_(rng) {} // 返回权重表下标,O(log n) int pick() { return dist_(rng_.raw()); } private: std::discrete_distribution<int> dist_; Rng& rng_; };std::discrete_distribution内部维护累积权重,抽样是二分查找,复杂度 O(log n)。把dist_做成成员之后,构造开销被摊到整个实验周期里,可以忽略。注意构造时传的是权重而不是概率,权重不需要归一化,全为正数就行,这对卡池配置很友好——你直接填“这张卡权重 3、那张卡权重 1”就可以。
还有一个容易忽略的点:如果做的是多卡池模拟(比如限定池和常驻池并行),每个池子要有独立的CardPool实例和独立的保底计数。共用一个PlayerState是最常见的错误,跑出来的数据两个池子的分布会互相污染,而且不会报错。
5. 避坑与排查:五条让统计结果失真的翻车记录
这一章的五条都是我实际踩过或者帮人排查过的。共同特征是:程序不崩、不报错、跑得还挺快,但结果就是不对。抽卡模拟最难受的地方就在这,它不会给你一行红色错误提示。
5.1 平均出金只有 50 多抽,比预期低了整整十抽
现象:一百万抽跑完,平均出金抽数 54 左右,中位数却还是 76,两个数对不上。
原因:软保底爬坡那段写成了p5 += cfg.softPityStep * (p.pity5 - cfg.softPityStart),少了个+1,导致第 74 抽的加成是 0 而不是 6%。单独看只差一抽的概率,但整个爬坡曲线整体左移了一格,早期就有大量样本提前出货,把均值拉低了。中位数不受影响是因为分布的主体还在,只是尾巴被削了一点——这正是为什么验证必须同时看均值和分位数。
解决:把概率公式单独抽成一个函数softPityProbability(int pity, const PoolConfig&),写单元测试断言pity=73时返回 0.006、pity=74时返回 0.066、pity=90时返回 1.0。三个断言一跑,这类差一错误全部现形。
5.2 程序跑一千万抽要几分钟,明显不对劲
现象:一千万抽耗时超过三分钟,CPU 单核跑满,但代码里没有任何 I/O。
原因:两个。一是编译时没加-O2,CMake 里CMAKE_BUILD_TYPE没设,默认是空字符串,等价于-O0,光这一项就慢八到十倍。二是std::uniform_real_distribution<double>写在了drawOne函数体里当局部变量,每抽构造一次,一千万抽就构造一千万次。
解决:构建命令里显式带上-DCMAKE_BUILD_TYPE=Release;把随机数归一化改成第四章那种位运算写法,或者至少把 distribution 提成类的成员。这两项改完,同一个实验从三分钟降到五秒以内。顺手把历史记录从std::vector<PullRecord>改成统计聚合,内存占用也能从几百 MB 降到几 MB。
5.3 换个种子平均出金就变 2 抽,结论完全没法用
现象:种子 1 跑出来 60.1 抽,种子 2 跑出来 64.3 抽,两份报告给出完全不同的结论。
原因:样本量只有一万抽。出金抽数的标准差在 18 到 20 之间,一万抽的标准误就是 0.18 抽,两倍标准误的差距已经出现了,但这是纯随机波动,不是模型差异。更隐蔽的情况是种子本身不独立,比如用time(NULL)做种子然后循环跑,同一秒内跑十次拿到的种子全一样。
解决:对比实验的样本量至少一百万抽,标准误降到 0.02 抽以内,才谈得上分辨 0.5 抽的差异。种子用递增整数序列而不是时间戳,保证每次循环种子都不同。做参数扫描时,固定同一批种子跑所有参数组合,这样组间差异不会被种子差异污染——这个技巧叫公共随机数,能大幅降低对比实验的方差。
5.4 CSV 里出现 pity_at_hit = 0 或者 91
现象:统计脚本跑出来最小值 0、最大值 91,画出来的直方图左边多一根柱子右边多一根尾巴。
原因:0 是因为清零写在记录之前,记录到的是清零后的值。91 是因为硬保底判定用了p.pity5 > cfg.hardPity5而不是>=,第 90 抽没触发,第 91 抽才必出。这两个都是典型的边界差一问题,而且都不会导致程序崩溃,只是让统计结果整体偏移一格。
解决:把“记录结果”和“更新状态”拆成两步,先记录后清零,顺序不能反。硬保底的边界用>=,并且在函数入口处加一行断言assert(p.pity5 <= cfg.hardPity5),一旦越界立刻停下。抽卡这种状态机,断言比日志有用得多。
5.5 UP 率统计出来 66.7%,有人以为是 bug 改成了 75%
现象:统计所有五星里 UP 卡的占比,得到 66.7%,和配置里的upRate5 = 0.5对不上,于是有人去改逻辑“修复”它。
原因:这是大小保底机制的正确结果,不是 bug。长期看,每一轮保底周期要么在小保底直接命中 UP(概率 1/2,消耗 1 个五星),要么歪掉然后大保底必中(概率 1/2,消耗 2 个五星)。一个周期期望产出 1 张 UP、1.5 个五星,比值就是 2/3,也就是 66.7%。配置里的 0.5 是“单次五星的 UP 概率”,不是“长期 UP 占比”,这两个数是不同的东西。
解决:统计口径要在代码里写清楚,输出两个字段:up_ratio_per_pull(单次判定命中率,应该是 50%)和up_ratio_longrun(长期占比,应该是 66.7%)。如果有人把大保底逻辑改成“歪了之后不置标记”,长期占比会掉到 50%;如果改成“大保底命中后不清标记”,会涨到接近 100%。这两个数一左一右,正好把guaranteedUp的置位和清位都测出来。
6. 进阶用法:用 KS 检验和参数扫描把模拟器变成标定工具
跑通基线之后,这个模拟器的价值才真正开始。它不只是个“验证概率”的玩具,还能反过来做参数标定——你手上有自己几百抽的真实记录,想知道它更符合哪一套参数,模拟器可以直接给出答案。
第一步是分布检验。把自己账号的出金抽数整理成一列,和模拟器的一百万抽样本做 KS 检验,看两个分布是否有显著差异。样本量小的时候(几十次出金),KS 检验的功效有限,只能发现很大的偏差;但如果你攒了两三百次出金,就足够判断“我这个账号的分布是不是明显右偏”了。做之前一定要先把“跨卡池的记录”剔除,不同卡池的保底计数是独立的,混在一起检验没有意义。
第二步是参数扫描。固定住硬保底 90 和基础概率 0.006,只扫softPityStep,从 0.04 到 0.09 每 0.005 一档,每档跑一百万抽,记录中位数和 P90,然后找和实测分位数最接近那一档:
import numpy as np, subprocess, json results = [] for step in np.arange(0.04, 0.095, 0.005): subprocess.run([ "./build/gacha_sim", "--pulls", "1000000", "--seed", "20240501", "--soft-step", f"{step:.3f}", "--out", f"scan_{step:.3f}.csv" ], check=True) # 这里的 read_quantiles 读 CSV 后返回 (中位数, P90) med, p90 = read_quantiles(f"scan_{step:.3f}.csv") results.append({"step": round(step, 3), "median": med, "p90": p90}) print(json.dumps(results, indent=2))固定种子在这里很关键:所有档位共用同一个种子,随机噪声在组间被抵消掉,扫描出来的曲线会非常平滑,参数敏感性一眼就能看出来。这套流程我用来标定过好几种不同的卡池模型,扫一遍十几秒,比手算快得多。
第三步是敏感度表格。把扫描结果整理成下面这样,基本上就是一份卡池参数说明书了:
| softPityStep | 中位数 | P90 | 平均出金 |
|---|---|---|---|
| 0.04 | 约 78 抽 | 约 85 抽 | 约 65 抽 |
| 0.06 | 约 76 抽 | 约 80 抽 | 约 62 抽 |
| 0.08 | 约 75 抽 | 约 78 抽 | 约 59 抽 |
中位数变化很小、平均抽数变化很大,说明这个参数主要在削尾部的形状,而不是整体平移。看懂这张表之后,你再听到“某个卡池变良心了”的说法,第一反应应该是去问:是中位数降了还是尾部削了?这两个是完全不同的改法,玩家体感也完全不同。
改参数的时候还有个小习惯建议养成:每次动完概率配置,先把种子固定住跑十万抽的基线回归,对比上一次的平均、中位数、P90 三个数,全部在容差范围内再去做正式实验。有一次我为了快速验证一个想法,直接跳过了这一步,结果拿着一个软保底根本没生效的模型跑了两小时参数扫描,白跑。从那以后我每次改配置都强制走一遍基线回归,三个数对不上就不往下走。希望帮到你。
本文还有配套的精品资源,点击获取