Files
MCM-ICM_Toolkit/docs/04-environment.md
T
guzhujushi f95fbd2726 fix(stats): t_test/confidence_interval 收口 —— 307/308 是存根误报,CI 的夹取才是真 bug(L1 提前 2 个)
## 用户问的「t_test 307 / 308 两行报错」
- 那两行是 `float(res.statistic)` / `float(res.pvalue)`,报错来自 scipy 的 `TtestResult`:
  它和 `PearsonRResult` 一样是 `_make_tuple_bunch` **运行时**拼出来的类,存根里没有这两个字段。
- 探针实测 `TtestResult(statistic=3.873, pvalue=0.0305)`,运行时取值完全正常 —— **不是真 bug**。
- 按决策 40 的路子补 `_statistic(result: Any) -> float`(与 `_pvalue` 成对),一行 `# type: ignore` 都没挂。
  详情见 `docs/04-environment.md` 坑 10(已扩写为 pvalue / statistic 两条)。

## 顺手查出来的真 bug:`confidence_interval`
- 原码 `return (float(min(0.0, lower)), float(max(upper, 1.0)))`:
  ① `min` / `max` 的**参数写反**,下界被钉死成 `0.0`;
  ② **无条件**把三种 method 的区间都夹到 `[0, 1]`。
- 实测 `confidence_interval(rng.normal(170, 8, 50), method="t")` 下界是 `0.0`(正确 `171.0 ± 2.1`);
  `confidence=1.5` 被夹成 `(0.0, nan)` 而不报错。
- 现在:**只有 `proportion` 夹**(决策 42),`normal` / `t` 返回 `mean ± z|t·se` 不夹;
  `method` 非法抛 `ValueError`(§4.7)、`confidence` 走 `(0, 1)` 开区间、样本量 < 2 抛 `ValueError`。
- 入参放宽到 `list` / `tuple` / `ndarray` / `Series` / `DataFrame`:
  docstring 的 Examples 传 list、L0 用例传 ndarray,原来**两条都直接 `TypeError`**。

## t_test
- docstring 补齐 `Parameters` / `Returns` / `Raises` / `Notes`(§4.7);
  `_clean_data` 改用 `isinstance(data, pd.DataFrame)` 收窄(坑 8 的 `cast`);
  `alpha` 开区间校验;`df` 按 Welch–Satterthwaite / pooled / paired 分别给。
- **4 个口径未代选**(决策 39),已记进 `STATUS.md`「下一步」第 6 条待用户拍板:
  单样本结论文案、`paired` 的 NaN 对齐、`value` 被静默忽略、`DataFrame` 摊平多列。

## 测试与文档
- `tests/test_stats.py` +10 条(36 → 46),含把原先挂 skip 的
  `test_confidence_interval_matches_scipy` 转正;对照一律用解析式(z 公式 / Welch df / t 公式)。
- 全量:**125 passed / 37 skipped / 0 xfail**,ruff 0,pyright 0。
- 个人易错点新增第 15 条,并把**停在 6 号**的索引表补齐到 15(只补索引,正文未动)。
- `docs/04-environment.md` 顺手修掉残留的 `[FENCE]` 代码围栏占位符与一处重复的 `---`。
- `STATUS.md`:L0 记 9/12;校正测试数为 162 条;新增决策 42 / 43。
2026-10-03 18:40:41 +08:00

19 KiB
Raw Blame History

环境配置

记录本机(Windows / PowerShell)上 conda 与 venv 两套环境的实际配置方式、 实测数据、以及踩过的坑。相关脚本在 scripts/。

状态:两套环境均已建好、版本已对齐、验证通过(2026-10-01)。


一、结论速查

venv conda env
路径 D:\MCM&ICM_Toolkit\.venv D:\anaconda\envs\mcm-toolkit
解释器 Anaconda base,Python 3.13.9 Python 3.13.15
占用 10.5 MB 1.55 GB(+ 共用 pkgs 缓存)
来源 --system-site-packages 复用 base conda 从 defaults 通道安装
用途 轻量开发 / 离线兜底 跑题主力环境
缺什么 SALib、pymoo、pyright 无缺口

两套的数值计算栈已完全一致(见 §5),只差 Python 补丁号。可以放心在两套之间切换。


二、venv(轻量、离线可用)

D:\anaconda\python.exe -m venv --system-site-packages --prompt "mcm-toolkit" .venv
.venv\Scripts\python.exe -m pip install -e . --no-deps --no-build-isolation

一键:./scripts/setup-venv.ps1

为什么用 --system-site-packages

让 venv 直接复用 Anaconda base 里已有的包,零下载就有可用的科学栈, 而且只有 10.5 MB,可以安心放在项目目录里。

已验证可导入(版本随 base 走,2026-10-01 base 升级后为):

text numpy 2.4.6 scipy 1.18.1 pandas 3.0.6 matplotlib 3.11.2 scikit-learn 1.9.1 statsmodels 0.15.0 networkx 3.6.1 sympy 1.14.0

--no-deps --no-build-isolation 是必须的

  • --no-deps:依赖已由 base 提供,不必联网解析;
  • --no-build-isolation:否则 pip 会新建隔离环境去下载 setuptools 构建后端。

三、conda 环境 mcm-toolkit(跑题主力)

# 一键(推荐)
.\scripts\setup-conda.ps1

# 等价的手工命令
$env:CONDA_PLUGINS_AUTO_ACCEPT_TOS = 'yes'   # 见 §4 坑 1
conda create -n mcm-toolkit -y python=3.13 ^
    numpy scipy pandas matplotlib scikit-learn statsmodels ^
    networkx sympy openpyxl pytest ruff pip

实测:约 14.5 分钟(868 秒),下载约 3 GB,环境本体 1.55 GB。

建完后补装 pip-only 的依赖并安装本项目:

$py = 'D:\anaconda\envs\mcm-toolkit\python.exe'
& $py -m pip install SALib pymoo pyright
& $py -m pip install -e . --no-deps --no-build-isolation

这些 setup-conda.ps1 已全部自动完成。

为什么不推荐 --clone base

原以为 conda create -n X --clone base 是"本地复制、秒级完成、不用下载"。 实测不是。 在 conda 25.11 上它会重放 base 的完整包列表并逐个下载安装 (连 pandoc、m2-msys2-runtime、anaconda-navigator 这些都用不上的也照下), 跑 253 秒才下完约 2 GB 就已被中止,按比例算远慢于精准列表。

所以 setup-conda.ps1 的默认路径是精准包列表;-CloneBase 只在你确实想 要一份 base 的完整副本时才用。

断网怎么办

提前建好环境,并保留 D:\anaconda\pkgs 缓存(现在 8.4 GB)。 包已在缓存里时 conda create --offline 才能工作;否则会失败。 比赛现场不要指望临时建环境,走 venv(它 100% 离线可用)。


四、踩过的坑

坑 1 —— Anaconda ToS 插件要联网。 Anaconda 25.x 自带 conda_anaconda_tos 插件,任何 create/install 之前都会去 repo.anaconda.com 拉 ToS 元数据。断网时它不是报"没网",而是抛一长串 traceback。

$env:CONDA_PLUGINS_AUTO_ACCEPT_TOS = 'yes'     # 推荐:跳过联网,保留 libmamba 求解器
conda --no-plugins create ...                  # 彻底关插件(退化成经典求解器,慢)

坑 2 —— envs_dirs / pkgs_dirs 必须可写。 不可写时报 NotWritableError 或 Error encountered while attempting to create cache directory。排查:

conda info --json | Select-String "envs_dirs","pkgs_dirs"
conda config --show envs_dirs pkgs_dirs

坑 3 —— 被中止的 conda create 用 conda env remove 删不干净。 本次中止后 conda env remove -n mcm-toolkit -y 跑完,D:\anaconda\envs\mcm-toolkit 仍然存在且 conda env list 里已看不到它。要手工删目录:

Remove-Item -LiteralPath 'D:\anaconda\envs\mcm-toolkit' -Recurse -Force

坑 4 —— 别把 conda env 建在项目目录里。 conda create -p ./.conda 会把 GB 级目录放进仓库,脏 .gitignore 也容易误提交。

坑 5 —— 版本漂移。 本机 base 是 2025-10 装的,defaults 通道已是 2026-10 的版本,于是新建的 conda 环境拿到的是更新的版本(pandas 从 2.3.3 跳到 3.0.6,是 major 升级)。 见 §5,写代码时注意别依赖两套环境都有的行为。

坑 6 —— conda 升级留下的 *.conda_trash 残骸会被 Python 当成包导入。 base 从 2025.12 升到 2026.07 后,D:\anaconda\Lib\site-packages\zstandard\ 里只剩一个 backend_c.cp313-win_amd64.pyd.c~.conda_trash。目录没了 __init__.py,Python 就把它当作 namespace package 导入成功,于是:

>>> import zstandard
>>> zstandard.__file__      # None —— 导入"成功"了,但什么都没拿到
>>> zstandard.__version__
AttributeError

pandas 3.0 的 import_optional_dependency("zstandard") 拿到这种半残包时,抛的不是 ModuleNotFoundError(那会被正常忽略),而是 ImportError: Can't determine version for zstandard,直接打断 pd.read_csv。 症状很迷惑:同一个脚本今天能跑明天不能跑,报错还指向 csv 读取。

怎么查(关键是 __file__ 和 __version__ 两项一起看):

& 'D:\anaconda\python.exe' -c "import zstandard; print(zstandard.__file__, getattr(zstandard,'__version__','MISSING'))"
Get-ChildItem -LiteralPath 'D:\anaconda\Lib\site-packages\zstandard' -Force

怎么修:确认 conda 记录里本来就没有这个包(conda list -n base | findstr zstandard 只该看到 backports.zstd / zstd),再把这个残骸目录改名禁用(比删掉可逆):

Move-Item -LiteralPath 'D:\anaconda\Lib\site-packages\zstandard' `
          -Destination 'D:\anaconda\Lib\site-packages\zstandard.disabled-conda-trash'

改名后 import zstandard 变成正常的 ModuleNotFoundError,pandas 会安静地跳过它。 本次已按此处理,.venv 的 pd.read_csv 恢复正常。

坑 7 —— 编辑器报"无法解析导入 numpy / pandas",但解释器明明是对的。

现象:.venv 与 conda 环境里 import numpy, pandas 都正常,pytest 也全过, 只有编辑器/pyright 满屏 无法解析导入"numpy"。

原因是静态分析器自己挑了一个 python,跟你运行代码用的那个没关系。本机上 pyright --verbose 一跑就露馅:

Assuming Python version 3.14.6.final.0
  Search paths:
    c:\Users\guzhu\AppData\Roaming\Python\Python314\site-packages
    c:\Users\guzhu\AppData\Local\Python\pythoncore-3.14-64\Lib\site-packages
    ...            <- 一个 Anaconda 路径都没有

那个 3.14 来自 PATH 上的 python,而 PATH 上的 python 是本机唯一的 python:

where python  ->  C:\Users\guzhu\agent-tools\bin\python.cmd
                  \-> C:\Users\guzhu\.cache\codex-runtimes\...\python\python.exe

Codex 运行时的 shim(Python 3.14,没有 numpy/pandas)。分析器从 PATH 找到它, 自然解析不到科学栈。注意持久化的 User/Machine PATH 里并没有这一项, 它是被 Codex 进程临时注入的——所以"我的终端里没问题"和"工具有问题"可以同时成立。

还有第二层原因:.venv 是 --system-site-packages 建的,三方包都在 D:\anaconda\Lib\site-packages,而 pyright / Pylance 不认 pyvenv.cfg 里的 include-system-site-packages,只会去 .venv\Lib\site-packages 找—— 那里只有 pip,于是照样报解析不到。

修法(两件事都要做):

  1. 在 pyproject.toml 里把分析器钉到工作区 venv,别让它去 PATH 上碰运气:
[tool.pyright]
venvPath = "."
venv = ".venv"
pythonVersion = "3.13"     # 不写就跟着 PATH 上那个 3.14 走
  1. 在 .venv\Lib\site-packages 放一个 .pth 指向 base 的 site-packages。 pyright 不读 pyvenv.cfg,但会读 .pth:
& 'D:\anaconda\python.exe' -c "import sysconfig; print(sysconfig.get_paths()['purelib'])"
# 把输出的那一行原样写进 .venv\Lib\site-packages\_mcm_base_site_packages.pth

scripts/setup-venv.ps1 已经自动做这一步(第 [2/4] 步),换机重建 .venv 不用手动补。

改完 pyright 从 17 errors 掉到 0 errors。IDE 侧还要各做一件事:

  • VS Code / Cursor:Ctrl+Shift+P → Python: Select Interpreter → 选 D:\anaconda\envs\mcm-toolkit\python.exe(或 .venv\Scripts\python.exe), 然后 Developer: Reload Window。Pylance 有缓存,不重载不生效。
  • PyCharm:Settings → Project → Python Interpreter 选同一个, 再 File → Invalidate Caches / Restart。

一句诊断口诀:编辑器说"解析不到包"时,先跑 pyright --verbose 看它的 Search paths,而不是先去 pip install。

坑 8 —— df[Index] 被 pyright 当成 Series,.std() 的结果于是被判成标量。

现象:data/clean.py 里

numeric = data[numeric_cols]        # numeric_cols = df.select_dtypes(...).columns
std = numeric.std()
std = std.replace(0, np.nan)        # pyright: 无法访问类"float"的属性"replace"

运行时 numeric 明明是 DataFrame、std 明明是按列的 Series,pyright 却报这个错。

原因:pandas 自带的类型注解里,DataFrame.__getitem__ 只声明了「Hashable → Series」 这一支。拿一个 Index 去索引时它挑中这个重载,numeric 就被推断成 Series | DataFrame; 而 Series.std() 返回的是标量,两支一取并集就把 float 带了进来 —— float 当然没有 .replace。用 reveal_type 一问就现原形:

“numeric”的类型为“Series | Unknown | Self@DataFrame”
“numeric.std(axis=0)”的类型为“Series | float | Unknown”

几个没用的修法(都实测过):

  • numeric.std(axis=0) —— 接收方本身还是并集,重载照样解析成并集;
  • data[list(numeric_cols)] —— 不改变重载选择;
  • 把参数改由一个类型明确的 frame 局部变量承接 —— frame 确实干净了, 但 frame[Index] 还是同一个重载问题。

有效的修法是只在取列子表那一句上按运行时的真值收窄:

from typing import cast

numeric = cast(pd.DataFrame, data[numeric_cols])

cast 零运行时开销,而且能把下面所有行留在类型检查范围内 —— 比在 .replace 那一行 挂 # pyright: ignore 好:那样等于连带放弃后面每一行的检查。

一句诊断口诀:pyright 说某行"属性不存在"、但运行时确实能跑,先 reveal_type(...) 把真实推断打出来,看并集里混进了什么,别急着加 # type: ignore。

坑 9 —— pandas 3 起字符串列的 dtype 是 str,select_dtypes(include=["object"]) 已走弃用路径。

现象:one_hot_encode 自动识别类别列时只写了 ['object', 'category'], 功能看起来正常,但每次调用都发一条 Pandas4Warning,而且 warning 里写着这个行为 "will be removed in a future version"。

For backward compatibility, 'str' dtypes are included by select_dtypes when 'object'
dtype is specified. This behavior is deprecated and will be removed in a future version.

原因:pandas 3 起字符串列的 dtype 是 str(不再是 object)。当前版本为了向后兼容 仍然把 str 列算进 object,但那是过渡行为。等它真的移除,include=['object'] 会一列都选不中 —— 那时 one_hot_encode(df) 会静默返回未编码的原表, 不报错、不告警,是最难查的那类错。

实测(本机 pandas 3.0.6):

写法 选中 告警
include=['object', 'category'] ✅ 1 条
include=['object', 'category', 'str', 'string'] ✅ 0
include=['str', 'string'] ✅ 0

修法:把 'str' / 'string' 显式写进 include(这也是 pandas 官方迁移指南推荐的写法, 同时兼容 pandas 2 与 3)。配套测试用 warnings.catch_warnings(record=True) 断言 一条告警都不该有,这样以后 pandas 再收紧行为时会立刻变红,不会静默。

一句诊断口诀:select_dtypes 选不中东西时,先 print(df[col].dtype) 看真实 dtype, 别照着记忆里的 object 写。


坑 10 —— scipy 的检验结果在类型检查器里没有 pvalue / statistic 字段。

现象:_, p = stats.pearsonr(x, y) 运行没问题,但 pyright 报 无法将 "_T_co@tuple" 类型的参数分配给 ... 的 "float";改成 .pvalue 又报 无法访问类 "PearsonRResult" 的属性 "pvalue"。两条都像"库的锅",其实是存根看不全。

原因:PearsonRResult / SignificanceResult 不是普通的 namedtuple, 而是在 scipy/stats/_stats_py.py 里用 _make_tuple_bunch(...) 在运行时拼出来的类 (PearsonRResult 还继承自 PearsonRResultBase)。类型检查器看不到运行时才生成的字段, 于是解包会退化成 object、属性访问会报"未知"。 这不是运行时问题 —— pearsonr / kendalltau / spearmanr 返回的 p 实测都是 np.float64。

修法:不要逐处挂 # type: ignore,写一个小助手把结果显式收成 Any:

def _pvalue(result: Any) -> float:
    return float(result.pvalue)

def _statistic(result: Any) -> float:
    return float(result.statistic)

一处收窄、调用点全干净,t_test / anova / chi_square_test / fit_distribution 都能复用。

TtestResult 同理。 stats.ttest_1samp / ttest_ind / ttest_rel 返回的也是 _make_tuple_bunch 在运行时拼出来的类,存根里 .statistic 和 .pvalue 两个字段都没有。 inference.py 的 t_test 里 float(res.statistic) / float(res.pvalue) 两行就是这么被 pyright 标红的 (实测:TtestResult(statistic=3.873, pvalue=0.0305),运行时取值完全正常)。 换成 _statistic(res) / _pvalue(res) 后红线消失,一行 # type: ignore 都没挂。


五、两套环境的实际版本(已对齐)

2026-10-01 已把 base 从 Anaconda 2025.12 升到 2026.07,两套环境版本已对齐。

包 venv(继承 base) conda mcm-toolkit
Python 3.13.9 3.13.15 补丁号,可忽略
numpy 2.4.6 2.4.6 ✅ 一致
scipy 1.18.1 1.18.1 ✅ 一致(连 build 号都相同)
pandas 3.0.6 3.0.6 ✅ 一致
matplotlib 3.11.2 3.11.2 ✅ 一致
scikit-learn 1.9.1 1.9.1 ✅ 一致
statsmodels 0.15.0 0.15.0 ✅ 一致
networkx 3.6.1 3.6.1 ✅ 一致
sympy 1.14.0 1.14.0 ✅ 一致
pytest 9.1.1 9.1.1 ✅ 一致
SALib / pymoo / pyright 缺失 已装 venv 本来就只做轻量开发
BLAS 后端 MKL MKL ✅ 一致

base 升级记录

conda update -n base --all -y      # 365 个包更新,下载 638 MB,耗时 1033 秒
conda install -n mcm-toolkit numpy=2.4.6 -y   # 把项目环境对齐到 base 的 numpy
  • conda 本身从 25.11.1 升到 26.7.3。
  • 升级前 conda list -n base --revisions 里已留有 revision 记录,可回滚: conda install -n base --revision <N>。
  • 踩过的坑:base 想装 numpy=2.5.3 时求解器会顺手把 BLAS 从 MKL 换成 OpenBLAS (_anaconda_depends 2026.07 ..._mkl_1 → ..._openblas_1、blas 1.0-mkl → 1.0-openblas)。 _anaconda_depends 把 base 的 numpy 钉在 2.4.6,硬升会牵动整套线性代数后端, 所以选择把项目环境的 numpy 降到 2.4.6 来对齐,而不是动 base。
  • 升级期间遇到 repo.anaconda.com 读超时(默认 9.15 秒太短),已把 remote_read_timeout_secs 调到 120、remote_connect_timeout_secs 调到 30、 remote_max_retries 调到 5。这几项写在 ~/.condarc 里。

六、国内镜像(需要装新包时)

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
conda config --set show_channel_urls yes

# pip(对 venv 和 conda 环境都生效)
python -m pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

本机当前通过 http://127.0.0.1:7993 的 UniClash 代理直连,未配镜像。镜像只影响下载, 不影响坑 1 的 ToS 行为。


七、可选依赖

核心 4 个包(numpy scipy pandas matplotlib)两套环境都有。其余按需:

python -m pip install -e ".[ml]"            # sklearn statsmodels  已有
python -m pip install -e ".[graph]"         # networkx            已有
python -m pip install -e ".[sensitivity]"   # SALib               conda 环境已有
python -m pip install -e ".[symbolic]"      # sympy               已有
python -m pip install -e ".[moo]"           # pymoo               conda 环境已有
python -m pip install -e ".[excel]"         # openpyxl            已有
python -m pip install -e ".[dev]"           # pytest ruff pyright

缺依赖时对应函数应当给出可读的安装提示,而不是裸抛 ImportError (写成约定,见 docs/02-architecture.md §4.6)。


八、脚本的编码约定(踩过的坑)

scripts/ 下的 .ps1 必须是纯 ASCII + UTF-8 无 BOM + LF。

原因是 Windows PowerShell 5.1 在没有 BOM 时按 ANSI(本机 GBK)解析 .ps1, 中文注释和中文消息会被拆成非法字节,报出来的却是一堆莫名其妙的 Missing closing '}' in statement block / The string is missing the terminator。 (实测:带中文的 doctor.ps1 直接语法错误;改成纯 ASCII 后立刻正常。)

两种可行方案,本项目选了第二种:

  1. 保留中文 → 必须存成 UTF-8 with BOM(违反 AGENTS.md 的"无 BOM"约定);
  2. 纯 ASCII,注释与输出用英文 → 符合 AGENTS.md,且任何编码下都能解析。← 采用

另一个相关的坑:PowerShell 5.1 向原生程序传参时会吃掉双引号, 所以 doctor.ps1 里传给 python -c 的探测代码只能用单引号字符串。


九、已知的沙箱相关现象

如果在受限环境(文件系统只读 / 无网络)里跑 matplotlib,会看到:

Matplotlib created a temporary cache directory at ... because there was an issue with
the default path (C:\Users\guzhu\.matplotlib)

这不是配置问题,是 ~\.matplotlib 不可写。正常终端里不会出现; 真遇到时设置 MPLCONFIGDIR 指向可写目录即可:

$env:MPLCONFIGDIR = "$env:TEMP\matplotlib"