用户实现的第一版 markov_simulate 算法本体经探针实测正确(终点频率 [0.657, 0.343] 对照平稳分布 [2/3, 1/3],同种子可复现),本次只动工程侧,未改任何算法行: - ruff 8 条全清(I001 导入排序 + 5 处 W293 / 2 处 W291) - docstring 从一行补全为 Parameters / Returns / Raises / Notes / Examples - 文案:马尔科夫→马尔可夫(3 处)、initail→initial、半角逗号→全角、paths[:,0]→paths[:, 0] - arr, kind = as_array(...) 的 kind 全程未用 → arr, _ - tests/test_solve.py 新增 9 条 markov_simulate 测试(此前 0 条) - STATUS.md 新增决策 56(initial 语义待用户拍板,A/B/C 三选项)与 5 条待办 - CHANGELOG 与 docs/dev/personal-pitfalls.md 易错点 27(入口分流后两条分支口径矛盾) 校验:pytest 188 passed / 31 skipped(219 收集);ruff 0;pyright 0
48 KiB
更新日志
格式参考 Keep a Changelog, 版本号参考 语义化版本。
[Unreleased]
Added
-
仓库骨架:
src/mcm_toolkit/7 个包(core / data / stats / models / solve / decision / viz) -
顶层
__init__.py只导出 7 个子包 + 8 个快捷函数,__all__即公开 API 清单 -
core.Result:唯一的结果对象(替代原建议里的 5 个 Result 类) -
docs/01-suggestion-review.md:对 ChatGPT 建库建议的审查与精简(14 包 → 7 包,~250 函数 → ~40) -
docs/02-architecture.md:包边界、Result/ RNG /fit-predict-score协议、依赖分层 -
docs/03-roadmap.md:L0/L1/L2 分级 + 永久不做清单 -
docs/04-environment.md:conda / venv 配置与三个已踩的坑 -
docs/dev/function-card-template.md:S 档算法的函数卡片模板 -
.venv(基于 Anaconda base,--system-site-packages)+ 可编辑安装 —— 10.5 MB,离线可用 -
conda 环境
mcm-toolkit(D:\anaconda\envs\mcm-toolkit)—— Python 3.13.15,含 SALib / pymoo / pyright -
scripts/setup-conda.ps1/scripts/setup-venv.ps1/scripts/doctor.ps1 -
environment.yml:需联网时的精确复现方案 -
pyproject.toml:核心依赖只 4 个,其余走 extras -
docs/02-architecture.md§4.7「类型约定」:注解要写全、numpy 标量的判断写法、 非法类型必须抛TypeError。配套在docs/03-roadmap.md的开发纪律里加了指针。 -
docs/dev/personal-pitfalls.md:个人易错点清单,只追加、不通读 -
data.clean:四个函数落地(由用户本人实现)——clean_missing(L0 第 4 项)、impute_missing/detect_outliers/winsorize(原属 L1,顺手写了)。 -
tests/test_data.py:新增 13 条 clean 行为测试,覆盖clean_missing的阈值语义与轴向、impute_missing的 mean/ffill/mode 与全 NaN 列、winsorize的分位点与异常类型。 -
tests/test_data.py:另加 7 条xfail,把已知但未修的契约钉在测试报告里(见 Notes)。 -
docs/algorithms/detect_outliers.md:第一张 S 档函数卡片(roadmap §3.1 要求 约 15 张,这是第 1 张)。除模板九节外,额外记了三条容易踩的判断:为什么不返回Result、 非数值列恒False的两条备选路各错在哪、以及ddof口径与stats.zscore不一致。 -
examples/01_clean_and_describe.py:load_table/save_table/clean_missing/impute_missing/detect_outliers/winsorize六个函数的最小用法,实测跑通(exit 0); 末尾留TODO(L0)等stats.describe。 -
tests/test_data.py:data的 transform 段 18 条用例 ——normalize的三种 method、feature_range区间与两条校验、常量列不出 nan、docstring 那个 Examples 的精确值、按列语义;standardize的按列与ddof(用解析关系钉住:ddof=1时结果的 std =sqrt((n-1)/n));log_transform的定义域平移;one_hot_encode的自动识别、drop_first、列不存在报错。 其中两条是把原先挂着的skip转为实跑(见坑 9 / 个人易错点 9)。 -
tests/test_stats.py:新写 20 条用例 ——describe的方向约定(行 = 指标、列 = 变量)、missing/missing_ratio/skewness/kurtosis四行不能是 NaN、非数值列自动跳过、 接受 ndarray / list / Series、加权均值与加权标准差对照手算公式、5 种非法weights;zscore的手算对照 / 按列 / 常量列 / 非法类型;coefficient_of_variation的手算对照、 单列 DataFrame、均值为 0 抛ValueError、空数组与二维输入。 -
examples/01_clean_and_describe.py:填掉末尾的TODO(L0),加一段describe输出(实测 exit 0)。
Added
stats.correlation_pvalues/stats.correlation_table:correlation_matrix的伴随函数 (方案 A,用户 2026-10-03 拍板,见决策 41)——correlation_matrix只返回数值矩阵;correlation_pvalues给数值 p 值矩阵、correlation_table给"系数 + 显著性星标"的论文表格。 三者共用_check_corr_args入参守卫与成对删缺失口径,correlation_table在最外层调另外两个。
Changed
-
core.get_rng/core.set_seed:实现(L0 第 2 项)。get_rng收int/np.integer/Generator/None,其余类型抛TypeError;无参调用复用同一个会推进的共享生成器。set_seed同时覆盖 numpy 旧全局状态,让 sklearn / statsmodels 也可复现。 -
core.Result:补上__repr__与summary()实现(L0 第 1 项落地)。repr紧凑单行且不打印data内容;summary()输出可直接贴进论文的 Markdown 表格。 -
data.load_table:实现(L0 第 3 项)。按扩展名分派.csv/.xlsx/.xls;.csv默认做编码探测(utf-8 → utf-8-sig → gbk → gb18030),显式传encoding=时不再探测;na_values默认吃下空串 /NA/N/A/#N/A/null/-。 -
data.save_table:实现(原属 L1,顺手落地)。按扩展名分派,父目录不存在时自动创建, 返回实际写入的Path。 -
data.load_table/data.save_table:docstring 补Parameters/Raises(docs/02-architecture.md§4.7 对每个公开函数的硬要求)。 -
data.load_table的返回注解Any→pd.DataFrame,与 docstring 的承诺对齐。 -
data.clean:docstring 按docs/02-architecture.md§4.7 补全 Parameters / Returns / Raises (Codex 代写,与 io.py 同一套 numpydoc 格式)。 -
data.clean:ruff 从 6 条清到 1 条——导入分组(I001)、elif 链改 if 加末尾无条件 return (RET505 / RET503)、去掉多余 f 前缀(F541)、删掉只会被覆盖的 mask 初始化(死代码)、 去掉行尾空白(W293)。 -
pyright报"无法解析导入 numpy / pandas"(17 errors)的原因:分析器会去 PATH 上找 python, 本机 PATH 上只有 Codex 运行时的 3.14 shim(无科学栈),且 pyright 不认pyvenv.cfg的include-system-site-packages。 改为[tool.pyright]指定venvPath = "."/venv = ".venv"/pythonVersion = "3.13",并让scripts/setup-venv.ps1往.venv里写一个 指向 base site-packages 的.pth。pyright 17 errors → 0。 详见docs/04-environment.md坑 7。 -
tests/test_data.py:df["b"].isna().all()的类型是Series | bool, 直接当断言条件 pyright 会报"条件操作数无效",外面套bool(); 两条故意传错类型的用例加# type: ignore[arg-type]。 -
data.save_table的后缀校验排在path.parent.mkdir(...)之后,传.txt会先留下一个 空目录再抛ValueError。改为先校验、后副作用,并加回归测试。 -
data/io.py里__all__与下一个def之间只有 1 个空行(PEP 8 要 2 个)。 -
tests/test_data.py顶上的模块级pytestmark = pytest.mark.skip(...)会连带跳过同文件里 已经写好的函数,导致load_table零覆盖。改为逐条 skip 未实现的算法,并补 8 条 io 测试 (编码探测、NA 归一、后缀校验、缺文件报错、往返读写、类型校验)。测试数 31 → 39 passed。 -
conda base 里 zstandard 的
*.conda_trash残骸目录被 Python 当成 namespace package 导入, 使.venv(= base)的pd.read_csv抛ImportError: Can't determine version for zstandard。 已把残骸目录改名禁用(可逆,未删除),详见docs/04-environment.md坑 6。 -
data.clean第二轮审查(2026-10-02),Codex 直接修掉 4 处轻度问题:winsorize(Series)因select_dtypes在Series上不存在而直接崩(Series 分支提到最前, 并加数值 dtype 守卫);winsorize的data.quantile()改为只在数值列上算 (文本列不再崩,与已有的numeric_cols判断对齐);删掉detect_outliers里已成空操作的df = pd.DataFrame(data)(数据在更早处就转过了,顺手去掉df这个别名); 清掉行尾空白(W293)、函数体内的连续空行,并把clean_missing的嵌套if合并 (SIM102,ruff 最后 1 条)。ruff1 error→All checks passed,pyright 仍 0。 -
tests/test_data.py:5 条xfail经 XPASS 信号确认已修好,摘掉 mark 转正式用例 (detect_outliers的 4 条 +winsorize混合类型 1 条)。测试 53 passed / 7 xfailed → 58 passed / 2 xfailed。 -
CHANGELOG.md:[Unreleased] 里有一句被重复追加撑坏 ("…删掉它即全绿。:分析器会去 PATH 上找 python,"),已修。 -
data.detect_outliers收尾(2026-10-02,由用户本人实现):统计量(quantile/mean/std)与掩码写入都收窄到数值列,含文本列的赛题表不再崩; 非数值列在掩码里恒False(用户本人定的语义,见决策 32)。docstringNotes补上这条语义。 -
data.detect_outliers里data=data.copy()是死代码:下面的to_frame()/pd.DataFrame()本来就返回新对象,函数全程也没原地改过data,白白复制一份数据;而且它排在类型分派 之前,会把错误入参的报错从ValueError变成AttributeError。已删。 -
data.detect_outliers:删掉那行死代码后 pyright 反而报了 1 个真错 ——df[Index]被类型存根当成Series,.std()于是被判成标量、std.replace(...)非法。 在取列子表处用cast(pd.DataFrame, ...)按运行时真值收窄。详见docs/04-environment.md坑 8。 -
tests/test_data.py:test_detect_outliers_survives_text_columns摘掉 xfail 并加强断言 (文本列必须恒False);新增test_detect_outliers_all_text_frame_returns_all_false钉住"整表没有数值列"这条边界。测试 58 passed / 2 xfailed → 60 passed / 1 xfailed。 -
docs/dev/personal-pitfalls.md:追加第 8 条(限制掩码 ≠ 限制统计量)。 -
data.impute_missing的 mode 分支改为返回新表(原先data[col] = ...会就地改调用方的 DataFrame,另外四支都返回新对象)。最后一条xfail随之转正,测试里xfail清零。 -
data.detect_outliers补入参类型校验(§4.7:非法类型抛TypeError,不静默兜底): 接受DataFrame/Series/list/tuple/ndarray。tuple同时补进is_ld_array, 与list同等对待;dict不再被接受(以前靠pd.DataFrame()碰巧能过,见决策 33)。 -
tests/test_data.py:摘掉最后一条xfail(mode 就地改)并加强断言;新增 3 条 (非法类型抛TypeError、元组按一维数组型处理、全 NaN 列与单元素列不抛异常)。 测试 60 passed / 1 xfailed → 64 passed / 0 xfailed。 -
data.transform四个函数实现(由用户本人写):normalize(minmax / vector / max,均按列)、standardize(按列,ddof真的传下去)、log_transform(offset=None时自动取1 - min(x))、one_hot_encode。data/下的 L0 至此结清。 -
data.transform:Codex 清掉 5 条 ruff —— RET503(normalize末尾缺无条件return)、 F541(无占位符的 f-string)、RET504(多余的result中转)、RET505(return后的elif)、 SIM108(log_transform的 if/else 改三元)。 -
data.transform.normalize:supported_method与feature_range的校验提到np.asarray之前 (§4.7 的"守卫集中在开头");并修掉说反的报错文案 —— 条件是要求下界 < 上界, 原文却写"上界必须小于下界",且TypeError那条用了无占位符 f-string。 -
data.transform.one_hot_encode:自动识别类别列时显式加上'str'/'string'。 原来只写['object', 'category'],在 pandas 3 上是已弃用路径(每次调用发Pandas4Warning), 未来会变成一列都选不中的静默错误。详见坑 9。 -
测试计数:64 passed → 82 passed(新增 18 条,另两条 skip 转为实跑),skip 42 → 40。
-
data.transform:修掉上一版引入的 3 处真 bug(Codex 代修,用户授权"报错顺手修掉")。 症状是 7 条测试变红 + pyright 在transform.py:105/:133报reportArgumentType。 这不是编辑器误报 —— pyright CLI 与pytest都能复现,所以一行# type: ignore都没加:- 输入白名单漏了
pd.DataFrame:standardize(df)/log_transform(df)抛TypeError, 函数体里的is_df分支全是死代码。白名单统一成模块级_NUMERIC_TYPES并移到np.asarray之前。 normalize的三个 method 分支互相覆盖:minmax/vector算完的结果被末尾无条件的x / max冲掉,三种 method 实际都只走 max(test_normalize_*5 条因此变红)。改成if / elif / else。one_hot_encode把get_dummies的结果按data.columns重包一层DataFrame: 哑变量列名(city_A)与原列名(city)对不上 → 编码列被整列丢掉。改为直接返回get_dummies结果。log_transform的offset守卫条件写反(offset > 0就抛错,恰好把唯一合法用例拒掉), 改为校验定义域前提min(x) + offset > 0;offset=0对全正数据合法(见决策 35)。
- 输入白名单漏了
-
data.transform四个函数的 docstring 按docs/02-architecture.md§4.7 补齐Parameters/Returns/Raises(Codex 代写,与io.py/clean.py同一套 numpydoc 格式), 并把常量列行为(决策 34)写进Notes。 -
data.transform:口径定稿后 ruff 剩的 4 条 RET505(return后的elif)一并清掉 ——All checks passed;pyright 2 errors → 0 errors;测试 7 failed / 75 passed → 82 passed / 40 skipped。 -
stats.describe:实现落地(由用户本人写,Codex 审查 + 代修),并修掉一个静默 bug ——df_num.describe().T把表转反了(行 = 变量、列 = 指标),而 docstring 与test_describe_reports_expected_keys定的是"行 = 指标、列 = 变量"。 转置后.loc[指标名] = <按变量索引的 Series>与列名零交集,pandas 不报错, 直接把这四行(missing/missing_ratio/skewness/kurtosis)整行填成 NaN;weights分支则报cannot set a row with mismatched columns。去掉.T一处修好三件事。 另补median行(pandas 只有50%,测试契约定的是median)、入参放宽到list/tuple/ndarray(00_quickstart.py传的就是 ndarray)、weights校验补齐(维度 / 长度 / NaN / 负值 / 和为 0,原先和为 0 时裸抛ZeroDivisionError)、 docstring 按 §4.7 补Parameters/Returns/Raises。 -
stats.zscore:修 RET505;docstring 按 §4.7 补齐;DataFrame 取列处按坑 8 用cast收窄 (pyright 的DataFrame | Series重载)。 -
stats.coefficient_of_variation:均值为 0 由TypeError改为ValueError(那是取值问题不是类型问题,§4.7);空数组提前抛ValueError(原先静默返回 nan + RuntimeWarning); 报错文案里"异变系数"的错别字与 "N 维数组" 改为 "N 列"。 -
stats.coefficient_of_variation:分母由mean改为|mean|(用户本人改的,决策 38)—— 均值为负时σ/μ会给出负值,容易被读成"波动更小";现在 CV 恒非负。docstring 的Notes同步, 并加一条测试钉住"均值为负时 CV 仍为正"。 -
tests/test_stats.py:去掉模块级pytestmark = pytest.mark.skip—— 它把该文件 6 条测试 全部跳过(包括已经实现好的describe),正是决策 26 要禁的写法:实现零覆盖而测试全绿。 改为逐条 skip 未实现的 5 条。 -
测试计数:82 passed / 40 skipped → 103 passed / 39 skipped(新增 21 条); ruff 7 error → All checks passed;pyright 1 error → 0。
-
stats.correlation_matrix:实现落地(由用户本人写,Codex 审查 + 代修),修掉一个硬 bug ——if numeric_cols == 0:拿Index和标量比,得到的是布尔数组:0 列时静默不报错、 1 列时碰巧能跑、≥2 列直接ValueError: The truth value of an array ... is ambiguous—— 而"≥2 个数值列"正是最常见的情况,等于整个函数不可用。改成len(numeric_cols) == 0, 并加回归用例(见易错点 13)。 -
stats.correlation_matrix:pyright 的两处报错都不是运行时问题,也都没挂# type: ignore——data[numeric_cols]是坑 8 的DataFrame | Series重载(cast收窄);_, p = stats.pearsonr(...)解包后会退化成object(scipy 的返回值是_make_tuple_bunch在运行时拼的类,存根里看不到pvalue)。新增_pvalue(result: Any) -> float小助手统一取 p 值, 后面t_test/anova/chi_square_test可复用(见docs/04-environment.md坑 10、决策 40)。 -
stats.correlation_matrix:method校验提到最前(守卫集中开头);add_stars的elif链改if(RET505);清掉 3 条 F541 与 1 条 I001(import 未排序)—— ruff 归零。 -
tests/test_stats.py:test_correlation_matrix_diagonal_is_one由skip改成xfail(strict=False)(决策 30)—— 契约要数值矩阵,实现返回带星标的字符串表,口径待拍板; 另新增 2 条(多列数值表能跑通、非法输入报错)。 -
测试计数:103 passed / 39 skipped → 105 passed / 38 skipped / 1 xfail。
-
stats.correlation_matrix:按方案 A 拆开 —— 只返回数值矩阵,不再返回带星标的字符串表。 字符串表做不了后续计算(挑高相关对、喂聚类 / PCA、画热力图),而反过来随时能格式化; 一旦返回字符串,数值信息不可逆地丢失(见易错点 14)。 -
stats.correlation_pvalues:对角线由0.0改成NaN—— 写成 0 会被下游读成"极显著", 而"自己和自己相关"根本没有 p 值可言。 -
stats.correlation_pvalues:p 值与 r 现在用同一批观测 —— 先成对dropna()再算, 并同样尊重min_periods(不足给 NaN)。此前corr是成对删缺失、pearsonr不删, 两者基于不同的样本量,r 和 p 对不上。 -
stats.correlation_pvalues:只算上三角再镜像到对称位置(成对变量的 p 天然对称), 省掉一半重复计算。 -
tests/test_stats.py:test_correlation_matrix_diagonal_is_one摘掉xfail转正; 新增 8 条(数值性 + 对称性、非数值列跳过、p 值对照解析式t = r·sqrt((n-2)/(1-r²))、 对角线 NaN、成对删缺失口径、min_periods、对角线无星标、表格与数值输出一致)。 -
测试计数:105 passed / 38 skipped / 1 xfail → 114 passed / 38 skipped / 0 xfail。
Notes
detect_outliers原有的三个 bug 加一处设计空白已全部收口:threshold范围校验挪进clean_missing、is_series/is_ld_array提到类型转换之前、统计量与掩码都限制在数值列、 非数值列恒False(决策 32)、入参类型校验补上(决策 33)。 S 档卡片与examples/用法也已落地 ——detect_outliers这条线结清。transform.py的 4 个口径已全部收口:① 三个函数共用_NUMERIC_TYPES白名单(含DataFrame); ② 全部接受DataFrame且保持"DataFrame 进 DataFrame 出";③ 显式offset按定义域校验 (min(x) + offset > 0,见决策 35,待用户确认);④ 常量列行为写进 docstring 的Notes; 另外Parameters/Returns/Raises已按 §4.7 补齐。- 新增易错点第 10 条:把真报错当成"编辑器误报" —— 红线先跑 CLI 与
pytest再判断(docs/dev/personal-pitfalls.md)。 stats这条线开工了:describe落地,填掉examples/01_clean_and_describe.py的TODO(L0);correlation_matrix功能上也能跑了(硬 bug 已修)。correlation_matrix的返回契约已按方案 A 定案(决策 41):数值归correlation_matrix、 p 值归correlation_pvalues、星标表归correlation_table。对角线不带星、p 与 r 同批观测两条也一并修掉。correlation_matrix仍只收DataFrame(describe/zscore/transform都收 ndarray)—— 要不要放开由用户决定,记在STATUS.md「下一步」第 5 条。- 三处口径已拍板(2026-10-03):
log_transform的offset维持现状(决策 35);describe/zscore默认丢掉非数值列、与detect_outliers的"保留列"刻意不统一(决策 37); 变异系数改用σ/|μ|(决策 38)。另立决策 39 明确分工:语义口径用户拍板、工程事务 Codex 顺手补。 - 已实现函数的
examples/最小用法仍不全:00_quickstart.py要等bootstrap_ci/viz,Result的用法届时并进去。 - 已知缺口:
- 除
core.Result、core.get_rng、core.set_seed、data.load_table、data.save_table外,其余函数体尚未实现(统一raise NotImplementedError+TODO(L0/L1/v0.2)标记)。 这是刻意的:先定接口,再按docs/03-roadmap.md的 L0 → L1 顺序填实现。 - 已知缺口:
SALib(Sobol/Morris)、pymoo、pyright已装进 conda 环境mcm-toolkit,.venv里没有。
Fixed
stats.confidence_interval:修掉一个会给出错误数字的硬 bug —— 原码返回(float(min(0.0, lower)), float(max(upper, 1.0))),min/max的参数写反(下界被钉死成0.0), 而且无条件把区间夹到[0, 1]。实测confidence_interval(rng.normal(170, 8, 50), method="t")的下界是0.0(正确是171.0 ± 2.1);confidence=1.5被夹成(0.0, nan)而不报错。 现在只有proportion夹到[0, 1](决策 42),normal/t直接返回mean ± z|t·se;method非法改抛ValueError、confidence按(0, 1)开区间校验、样本量 < 2 抛ValueError。stats.confidence_interval的入参曾只收DataFrame,而 docstring 的 Examples 传list、 L0 用例传ndarray—— 两条都会TypeError。现放宽到list/tuple/ndarray/Series/DataFrame。
Added
stats.inference._statistic(result: Any) -> float助手,与_pvalue成对(决策 43)。 scipy 的TtestResult与PearsonRResult同源,都是_make_tuple_bunch运行时拼的类, 存根里.statistic/.pvalue都看不到。用户报告的t_test「307 / 308 两行报错」即此, 不是真 bug(探针实测TtestResult(statistic=3.873, pvalue=0.0305),运行时完全正常); 换成助手后 pyright 归零,一行# type: ignore都没挂。见docs/04-environment.md坑 10。stats.t_test(L1 提前落地):docstring 补齐Parameters/Returns/Raises/Notes;_clean_data改用isinstance(data, pd.DataFrame)收窄(坑 8 的cast);alpha开区间校验;df按 Welch–Satterthwaite / pooled / paired 分别给。tests/test_stats.py:+10 条(36 → 46),含把原先挂着skip的test_confidence_interval_matches_scipy转正。对照一律用解析式(z 公式、Welchdf公式、t = (mean − value)/(s/√n)),不拿实现对自己。docs/dev/personal-pitfalls.md:新增第 15 条「区间被无条件夹到[0, 1],且min/max参数写反」; 并把停在 6 号就没人管的索引表补齐到 15(只补索引,正文一个字没动)。docs/04-environment.md:坑 10 扩写成pvalue/statistic两条;顺手修掉残留的[FENCE]代码围栏占位符与一处重复的---。
Changed
STATUS.md:L0 记为 9/12(另 L1 的confidence_interval/t_test提前落地); 校正测试数为 162 条 / 125 passed / 37 skipped;新增决策 42 / 43;「下一步」列出t_test待用户拍板的 4 个口径(单样本结论文案、paired的 NaN 对齐、value被静默忽略、DataFrame摊平多列)—— 按决策 39,Codex 只列选项、不代选。
Added
core.validate.as_array:所有收数值函数的统一入口(决策 44)。 一次做完"类型白名单 + 转ndarray+ 报告原来是什么容器",返回(arr, kind),kind是"frame"/"series"/"array"。还原不代做 —— 调用方按kind手写, 因为describe/correlation_*/confidence_interval/t_test的输出天生与输入不同形。 入口三条硬规矩:numeric_only按"输出是否与输入同形"选、默认绝不摊平(要摊平显式ndim=1)、 不删缺失值(删缺失是各函数的语义)。见docs/02-architecture.md§4.8。
Changed
- 入口去重:
normalize/standardize/log_transform/zscore/confidence_interval/t_test六处各自手抄的isinstance(x, pd.DataFrame)/select_dtypes/np.asarray全部删掉, 各只剩 1 行as_array(...);data/transform.py的_NUMERIC_TYPES白名单连同 3 份副本一起删除。 原来这类判断散在 5 个文件 31 处。 data.transform的normalize/standardize/log_transform走numeric_only=False: 它们的输出必须与输入同形,跳过非数值列会让返回的表少列、对不上原表。zscore/normalize/standardize/log_transform的 Series 分支补回name—— 此前pd.Series(result, index=...)会把name丢掉,管道里concat会变成无名列。- 测试:
tests/test_core.py新增TestAsArray8 条(五种容器的kind、只取数值列、numeric_only=False遇文本报错、默认不摊平、ndim=2只校验不重塑、白名单外一律TypeError、 保留 NaN、dtype=None保持原 dtype);tests/test_stats.py新增 1 条(zscore保住 Series 的index与name)。全量 171 条 / 134 passed / 37 skipped。 docs/02-architecture.md:新增 §4.8「入口只有一个:core.validate.as_array」; 目录树里validate.py补上as_array。docs/dev/personal-pitfalls.md:新增第 16 条「同一段入口转换抄了 N 遍,还当它是约定」,索引补齐到 16。
Fixed
normalize(含文本列的表)等"容器对、内容转不成 float"的情况,错误类型由 numpy 的ValueError统一收成TypeError(决策 45,Codex 代选、待用户确认)—— 与t_test/confidence_interval已经写进 docstring 的口径一致。
Added
stats.bootstrap_ci(L0 第 10 项,S 档,实现由用户本人写):百分位法 bootstrap 置信区间,statistic可以是任意f(sample) -> float(默认np.mean)。返回Result,params = {estimate, low, high, confidence, n_resamples}、metrics.statistic_name。 重采样按块处理(batch_size=1000),不落地B × n整矩阵。docs/algorithms/bootstrap_ci.md:第 2 张 S 档函数卡片。除模板九节外,记了一条非平凡性质 —— 统计量有上界时百分位区间会塌到上界(极差的重采样值永远 ≤ 原样本极差,实测high == estimate), 以及 5 个待拍板口径。docs/algorithms/README.md的进度表同步打勾。
Fixed
bootstrap_ci的random_state=None分支原来另开np.random.default_rng(),绕过了core.get_rng—— 与决策 24 冲突:set_seed对它无效、区间不可复现。改成无条件get_rng(random_state)(决策 46)。bootstrap_ci的metrics["statistic_name"]:hasattr(statistic, "__name__")判不出 lambda —— lambda 的__name__恒为"<lambda>",于是else "custom"是死代码,自定义统计量拿不到"custom"。 改成getattr(f, "__name__", None)再判name in (None, "<lambda>")(易错点 17)。bootstrap_ci的 docstring 补齐Parameters/Returns/Raises(§4.7),并如实写明method="bca"尚未实现(此前 docstring 承诺支持、实现却抛NotImplementedError)。
Changed
tests/test_stats.py:摘掉 3 条skip(等bootstrap_ci的那 3 条),并新增 6 条断言 ——estimate等于原样本统计量、confidence单调性、五种容器结果一致、自定义 lambda 走回退分支、 不给种子时跟随set_seed、非法/未实现method。bootstrap 段共 9 条。 全量 177 条:143 passed / 34 skipped;ruff 0,pyright 0。docs/dev/personal-pitfalls.md:第 9 条「实现写完了测试还挂着 skip」复发 +1(这次是bootstrap_ci—— 实现写完、3 条测试仍挂 skip、全量照样全绿);新增第 17 条「hasattr(f, "__name__")挡不住 lambda,else分支是死代码」;索引补到 17。STATUS.md:E 节记bootstrap_ci(L0 10/12),决策 46,「下一步」改写并新增第 8 条列 5 个待拍板口径。
Changed
tests/test_solve.py:模块级pytestmark = pytest.mark.skip(...)拆成 9 条逐条@pytest.mark.skip(reason=_NOT_IMPLEMENTED)—— 模块级 skip 会把整个文件吞掉(决策 26、易错点 9)。 用例数与跳过数不变(177 收集 / 143 passed / 34 skipped),仅粒度变细。STATUS.md:新增分工硬规矩(决策 47) —— 算法一律由用户本人写(即便用户改口也不代写)、 工程事务由 Codex 写;L0 计数 9/12 → 10/12;「下一步」新增「⭐ 明天从这里开始」区块 (monte_carlo的契约 / 向量化要求 / 三条测试建议 / 卡片路径 / 一个待拍板口径); 验证结果里的 skip 说明由 37 条订正为 34 条。
Added
solve.monte_carlo落地(L0 第 11 项,S 档,实现由用户本人写):向量化"采样 → 仿真 → 汇总", 返回Result(8 个 metrics:mean / std / median / p05 / p95 / ci_low / ci_high),随机数无条件走get_rng。 配套tests/test_solve.py10 条测试(含"simulator只被调用一次"这条可执行的向量化断言)。
Fixed
monte_carlo的布尔结果此前让np.std在 numpy 2 里抛numpy boolean subtract ... is not supported, 整个函数判失败 —— 而"事件是否发生"是 MC 最常见的用法。入口改为np.asarray(..., dtype=float)(布尔按 0/1 统计); 字符串结果也从"ufunc isnan 不支持"变成可读的could not convert string to float。由行为探针挖出。monte_carlo的 docstringExamples跑不通:原采样器返回标量,实测success=False("simulator 返回的结果数量(1)必须与模拟次数 n(1000)一致");改成向量化写法。monte_carlo的 ruff 三条(I001 合并同源 import、F541 空 f-string ×2)、success= True的空格、 失败文案"重采样" → "模拟"(从bootstrap_ci抄过来忘改)。
Changed
monte_carlo按 §4.7 收严n:np.int64不再被误拒(注解放宽为int | np.integer);n=True/ 浮点n抛TypeError;n<=0抛ValueError。- 用户第二轮改的三处口径(决策 48):批量大小改由实现传
sampler(rng, size=n);结果含 NaN 判success=False;np.std显式ddof=0。docstring 同步改写(sampler契约、Raises、Notes的 NaN 口径)。 STATUS.md:L0 10/12 → 11/12,新增决策 48;验证计数更新为 187 收集 / 153 passed / 34 skipped。
Changed
- 工作约定收窄(决策 49):入口没收紧 / 少考虑情况 / 不符合简单原则这三类改动交回用户本人写, Codex 只报告 + 列选项(判据:这处改动要不要"想一下才对")。决策 47 里"入口 / 校验重构归 Codex"的部分作废。
docs/dev/personal-pitfalls.md:新增 第 18 / 19 / 20 条(入口校验没照 §4.7 清单逐条过 / 只想着主路径那种值 / "不简单"的信号是同一个信息要写两遍),第 1 条(类型判断只写int)复发 +1,索引补到 20。STATUS.md:新增决策 49 与两个待拍板小口径(random_state注解是否全项目对齐、std的ddof是否统一)。
Added
solve.monte_carlo_probability落地(L1,S 档,实现由用户本人写):向量化event(batch) -> bool 数组, 返回Result(probability/ci_low/ci_high/std_error),区间是 Wald 区间并夹在[0, 1](沿用决策 42 口径)。tests/test_solve.py新增 5 条 + 摘掉 2 条 skip(实现已写完却还挂着,易错点 9),solve段共 24 条。
Fixed
monte_carlo_probability的 docstringExamples跑不通:原采样器返回标量,实测success=False("event 返回的结果数量(1)必须与模拟次数 n(2000)一致");改成批量采样写法并实测通过。monte_carlo_probability的 ruff 六条:SIM300(0.0 >= confidence写成 Yoda 条件)、W291 ×2、W293 ×2、F541。- 该函数 NaN 提示里的 "simulator" 改成 "event"(从
monte_carlo抄来没改);metrics缩进与{"n":n}空格。
Changed
monte_carlo_probability的 docstring 补齐Parameters/Raises(原文只有 Returns / Notes / Examples), 并把"Wald 区间 + 夹到 [0,1]"与"p <= 1e-3判失败"两条现状如实写进 Returns / Notes。STATUS.md:L1 清单加入monte_carlo_probability;验证计数更新为 192 收集 / 160 passed / 32 skipped; 「下一步」新增该函数的 6 条待办口径(全部留给用户,Codex 未动)。
Fixed
monte_carlo_probability的z_score^2是异或不是乘方(Python^语义):np.float64 ^ 2直接抛TypeError: ufunc 'bitwise_xor' not supported,被except Exception吞成success=False—— 函数实际上整个不可用, 6 条测试全红才暴露。改成z_score**2。新增经验第 21 条。monte_carlo_probability的 message 在没有警告时留了个尾逗号;{"confidence":confidence}补空格。
Changed
- 用户第二轮改定(决策 50):区间由 Wald 换成 Wilson score(实测 p=1、n=1000 从退化的
(1.0, 1.0)变成(0.99617, 1.0);p=0 从"判失败"变成(0.0, 0.00383));小概率不再判失败,改为 message 里的警告;confidence记进Result.params;callable入口校验(现在抛TypeError);float()统一ci_*类型;n注解放宽为int | np.integer。 tests/test_solve.py:新增 1 条对照测试(区间必须等于statsmodels.proportion_confint(method="wilson")), 共 25 条;全量 193 收集 / 161 passed / 32 skipped / 0 xfail。docs/dev/personal-pitfalls.md:新增第 21 条(公式里的^不是乘方,且被except吞成success=False),索引补到 21。- docstring 的 "Wald 区间" 同步改为 "Wilson score 区间"。
Added
solve.monte_carlo_integral落地(L1,S 档,实现由用户本人写):bounds→ 自造(n, d)均匀样本 → 逐点求值 →mean(func(X) × 体积);内部自造sampler/simulator再复用monte_carlo—— "造批 → 一次算 → 汇总"的第二次复用。params额外记bounds/dimension;配套tests/test_solve.py新增 4 条 + 摘 1 条 skip。
Fixed
monte_carlo_integral的体积符号写反(真 bug,按决策 31 修):写成wights = lows - highs,奇数维体积为负 —— 1Dx²于[0,1]实测 -0.3324(应 +1/3)、3Dx得 -0.5005(应 +1/2);2Dx·y却得 +0.2499 "看着对" (两个负号相乘抵消,把 bug 藏住)。改成widths = highs - lows后复测 0.3324 / 0.5005 / 1.9971([0,2]) 全部正确。monte_carlo_integral的 docstringExamples跑不通:断言abs(r.data - 1 / 3) < 0.01,而data是 n 个样本的贡献数组,bool(数组)会抛ValueError: truth value ... is ambiguous;改成断言metrics["mean"]。- 格式:
simulate.py里def random_walk前只留了 1 个空行;tests/test_solve.py有 3 处留了 3 个空行。
Changed
monte_carlo_integral的 docstring 补齐func/n/random_state三节Parameters与Raises(原文只有bounds+ Notes + Examples);Returns写清"metrics["mean"]才是积分估计值,data是 n 个样本各自的贡献"(决策 51)。tests/test_solve.py:摘掉test_monte_carlo_integral_converges_to_analytic_value的 skip,新增 3 条"抓符号"用例 (3D 奇数维、[0,2]宽度、2D +name/dimension/bounds三个 params)与 1 条非法bounds抛ValueError;solve段 25 → 30 条,全量 197 收集 / 166 passed / 31 skipped / 0 xfail(ruff 0 / pyright 0)。STATUS.md:L1 清单加入monte_carlo_integral;新增决策 51 与「monte_carlo_integral的 3 条待办」; 「下一步」改成从random_walk接着写;验证计数更新为 166 / 31;monte_carlo_probability的 6 条待办收窄到 1 条。docs/dev/personal-pitfalls.md:新增第 22 / 23 条(失败结果的message被包装层无条件覆盖;体积/宽度也会减反、 维度奇偶会把符号错误藏住),第 3 条(注解与 docstring 不一致)复发 +1,索引补到 23。tmp/type_demo.py(gitignored):用 pyright 证明Callable[..., bool]会拒绝一个注解良好的向量化event(ndarray与bool不兼容),Callable[..., Any]才收 —— 这是决策 50 那条遗留的实际代价。
Fixed
monte_carlo_integral的bounds形状校验缺失(用户第二轮补齐):bounds=[0, 1]从裸IndexError: too many indices变成可读ValueError(带当前形状与"请检查是否漏了括号");[]/ 非list-tuple/ 参差不齐的输入也一并收口。monte_carlo_integral的失败message被包装层覆盖(用户第二轮改成if result.success:/else:两支):func返回标量时不再是success=False+ "成功"文案,而是"蒙特卡洛多重积分失败:维度 1,体积 1.0000,模拟 100 次。 蒙特卡洛模拟失败:TypeError - object of type 'float' has no len()"。- 格式:
simulate.py两行"只有空格的行"(ruff W293 ×2,Codex 顺手补)。
Changed
monte_carlo_probability的event注解Callable[..., bool]→Callable[..., Any](用户改,决策 50 的遗留清零): pyright 实测原注解会拒绝一个注解良好的向量化事件函数(ndarray与bool不兼容),运行期却一直收得下。monte_carlo_integral的n注解放宽为int | np.integer(与全模块对齐,易错点 1 的老口径)。monte_carlo_integral的 docstring:bounds入参说明与Raises同步到新入口校验(补"非空list/tuple、形状(d, 2)")。tests/test_solve.py:非法bounds从 1 类扩到 4 类([(1, 0)]/[0, 1]/[]/ 参差不齐),新增"失败 message 不许出现 '成功'"与np.int64两条;solve段 30 → 32 条,全量 199 收集 / 168 passed / 31 skipped / 0 xfail(ruff 0 / pyright 0)。STATUS.md:integral 的 3 条待办标记办结、概率函数的 6 条待办全部办结;新增决策 52 与 2 条新口径 (bounds收不收 ndarray、if/else里那三行重复);验证计数更新为 168 / 31。
Added
solve.random_walk落地(L1,S 档,实现由用户本人写):step_sampler(rng, size=(n_paths, n_steps))一次抽完整批增量 →np.cumsum(..., axis=1)→ 首列填start、其余列start + cumsum;data形状(n_paths, n_steps + 1)。 配套tests/test_solve.py新增 6 条(此前这个函数一条测试都没有)。
Fixed
random_walk的默认采样器整个不可用(真 bug,按决策 31 修):rng.integers(0, 2, size=size, dtype=float)被 numpy 拒掉(TypeError: Unsupported dtype dtype('float64') for integers)—— 默认路径 100%success=False、data=None,连 docstring 的 Examples 都跑不通。改成rng.integers(0, 2, size=size) * 2 - 1(由后面的np.asarray(..., dtype=float)统一转 float);实测形状(5, 101)、步长恒 ±1、同种子可复现。- 格式:
simulate.py又两行"只有空格的行"(ruff W293 ×2)。
Changed
monte_carlo_integral按用户第二轮口径收口(决策 52):bounds现在也收np.ndarray;name/params["bounds"]/params["dimension"]从if/else两支提到if之前,只让message分两支。random_walk的 docstring 补全(原文只有step_sampler一节):补齐n_steps/n_paths/start/random_state与Returns/Raises/Notes,并写清step_sampler收的是二维size=(n_paths, n_steps)。tests/test_solve.py:新增 6 条random_walk测试(形状与start列、默认步长恒 ±1、同种子可复现、 自定义高斯采样器、3 类非法入参、docstring Example 真能跑);solve段 32 → 38 条, 全量 205 收集 / 174 passed / 31 skipped / 0 xfail(ruff 0 / pyright 0)。STATUS.md:新增决策 53 与random_walk的 4 条待办;monte_carlo_integral的 2 条新口径办结 1 条 (剩"第一道isinstance分支已冗余");「下一步」推进到markov_simulate/markov_steady_state。
Fixed
random_walk的失败路径全部炸成UnboundLocalError(真 bug,按决策 31 修):call_params = {...}写在try:里, 而except分支要引用它 —— 实测 5 种失败输入(形状不对 / 增量含 NaN / 采样器抛异常 /start非数值 / 返回 1-D 数组) 5 个全中,本该返回success=False的Result,变成把异常抛给调用方。按原意把call_params前移到try之前, 未改任何口径;失败时照样带上n_steps/n_paths/start/random_state。
Changed
random_walk按用户第二轮口径改定(决策 54):n_steps/n_paths拆成TypeError(类型)/ValueError(取值); 新增steps.shape != (n_paths, n_steps)与np.isnan(steps).any()两条校验;默认步长改rng.choice([-1.0, 1.0], size=size);params补random_state;metrics由"抄 params"改成mean_terminal/std_terminal(终点位置,ddof=0)。random_walk的 docstring 同步:Returns写实际 metrics、Raises改成"类型错TypeError/ 取值错ValueError"、 去掉已不存在的start校验、Notes记明形状与 NaN 都判失败。tests/test_solve.py:+3 条("失败也必须返回带 params 的 Result"、NaN 判失败、终点std ≈ √t),start="a"改为断言success is False(口径待定);solve段 38 → 41 条, 全量 208 收集 / 177 passed / 31 skipped / 0 xfail(ruff 0 / pyright 0)。STATUS.md:新增决策 54 与random_walk的 3 条新口径;4 条待办标记办结;验证计数更新为 177 / 31。docs/dev/personal-pitfalls.md:新增第 26 条(except用到的变量在try里才赋值 —— 每条失败路径都炸)。
Changed
random_walk收口 2 条口径(用户第二轮改动):恢复start的入口类型校验(isinstance(start, (int, float, np.number))); 签名注解放宽为n_steps: int | np.integer/n_paths: int | np.integer(与 §4.7 及monte_carlo_integral的n对齐)。random_walk的start报错文案(Codex 顺手):原文"start必须是整数"与校验本身矛盾 —— 这条校验收 float, 改成"start 必须是数值";类型名统一{type(start).__name__}(全模块口径);n_paths: int | np.integer= 1补空格。random_walk的 docstring 同步:start的说明去掉"入口不校验类型",Raises补回start的TypeError。tests/test_solve.py:start="a"恢复断言TypeError;补回np.int64的n_steps/n_paths;solve段 41 → 42 条,全量 209 收集 / 178 passed / 31 skipped / 0 xfail(ruff 0 / pyright 0)。STATUS.md:新增决策 55(n_paths == n_steps的"方阵歧义"留作已知边界,A 接受 / B 换契约两条路); 3 条口径办结 2 条,验证计数更新为 178 / 31。
Changed
random_walk的step_sampler契约按方案 B 重定(用户拍板,决策 55):size由(n_paths, n_steps)改为整数n_paths * n_steps,必须返回一维数组,内部reshape(n_paths, n_steps)(行主序)。 动因:转置(n_steps, n_paths)与目标(n_paths, n_steps)在n_paths == n_steps时形状相同,形状校验天生分不出来(易错点 25)。 探针实测:默认采样器(5, 101)正常;转置二维 / 方阵二维 / 长度 +1 /(N, 1)四种返回一律判失败。 关键点:不要用.ravel()救场,那会把转置的二维摊平、歧义原样回来。红利:与monte_carlo的sampler(决策 48 的size=n) 契约统一,给monte_carlo写的采样器可直接复用(实测通过)。random_walk的 docstring 同步:step_sampler说明改为"整数size+ 一维返回 + 行主序",Notes记明二维返回一律判失败。tests/test_solve.py:把"转置采样器"那条改成test_random_walk_rejects_2d_step_sampler(含方阵情形), 新增test_random_walk_step_sampler_gets_flat_size_and_is_row_major(验size == 6与cumsum的行主序结果);solve段 42 → 43 条,全量 210 收集 / 179 passed / 31 skipped / 0 xfail(ruff 0 / pyright 0)。STATUS.md:决策 55 改为"已选 B 并落地";random_walk的 7 条待办全部办结;验证计数更新为 179 / 31。
Added
solve.markov_simulate落地(L1,S 档,实现由用户本人写):arr[current_states]取当前状态那一行 →cumsum得累积分布 → 用[0, 1)均匀数"落格"((cum_probs < r[:, None]).sum(axis=1))→ 写进下一列;data形状(n_paths, steps + 1)、值是整数状态编号。Codex 审查结论:算法本体正确 (探针实测 200 步后终点频率[0.657, 0.343]对照平稳分布[2/3, 1/3]、同种子可复现)。tests/test_solve.py:新增 9 条markov_simulate测试(此前 0 条)—— 形状 / 首列 / 状态取值 / 整数 dtype、 单位矩阵吸收态自锁、可复现(同种子相同、异种子不同)、终点频率收敛到[2/3, 1/3]、metrics = 终点状态、 非法转移矩阵(非方阵 / 行和不为 1)、非法steps/n_paths(0 / float / bool)、np.int64可收、docstring Example 真能跑。
Changed
markov_simulate的 docstring 从一行补到完整的Parameters/Returns/Raises/Notes/Examples。markov_simulate的工程侧清理:kind全程未用 →arr, _;马尔科夫→马尔可夫(3 处)、initail→initial、半角逗号 → 全角;paths[:,0]→paths[:, 0];ruff 8 条全清(I001+W291/W293)。STATUS.md:新增决策 56(initial的语义待拍板,含 A / B / C 三个选项)与 5 条待办; 验证计数更新为 188 passed / 31 skipped(219 收集)。
Notes
markov_simulate的 5 条口径留给用户(决策 49):initial语义(标量按路径数 / 数组按状态数,两条口径互相矛盾)、 转移矩阵未查非负(行和恰为 1.0 的[[1.5, -0.5], [0, 1]]会静默给出全 0)、paths[:, 0]赋值在try之外 (长度不匹配时裸抛ValueError)、状态越界没在入口拦、std_terminal对分类状态的意义。
[0.0.1] - 2026-10-01
Added
- 项目初始化,包结构落盘(实现为空)。