实现(用户本人写 + Codex 轻度收尾): - 统计量(quantile / mean / std)与掩码写入都收窄到数值列,含文本列的赛题表不再崩 - 非数值列在掩码里恒 False(用户定的语义),docstring Notes 补上这条 - 删掉死代码 data=data.copy():下面的 to_frame()/pd.DataFrame() 本就返回新对象, 函数全程只读 data,白复制一份;且它排在类型分派之前,会把错误入参的报错 从 ValueError 变成 AttributeError - 删掉那行死代码后 pyright 反而暴露 1 个真错:df[Index] 被类型存根当成 Series, .std() 于是被判成标量、std.replace(...) 非法。在取列子表处 cast 成 DataFrame 按运行时真值收窄(详见 docs/04-environment.md 坑 8) 测试: - 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;ruff 0;pyright 0 - 仅剩 1 条 xfail:impute_missing 的 mode 分支就地改调用方的表(要选语义,留给用户) 文档: - STATUS.md:L0 5/12、验证结果表、下一步重写、新增决策 32(非数值列恒 False) - docs/04-environment.md:新增坑 8(pandas 存根让 df[Index] 退化成 Series,及三种无效修法) - docs/dev/personal-pitfalls.md:只追加第 8 条(限制掩码 ≠ 限制统计量) - CHANGELOG.md:本轮变更
383 lines
16 KiB
Markdown
383 lines
16 KiB
Markdown
# 环境配置
|
||
|
||
> 记录本机(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(轻量、离线可用)
|
||
|
||
```powershell
|
||
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`(跑题主力)
|
||
|
||
```powershell
|
||
# 一键(推荐)
|
||
.\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 的依赖并安装本项目:
|
||
|
||
```powershell
|
||
$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。
|
||
|
||
```powershell
|
||
$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`。排查:
|
||
|
||
```powershell
|
||
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` 里已看不到它。要手工删目录:
|
||
|
||
```powershell
|
||
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** 导入成功,于是:
|
||
|
||
```text
|
||
>>> 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__` 两项一起看):
|
||
|
||
```powershell
|
||
& '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`),再把这个残骸目录改名禁用(比删掉可逆):
|
||
|
||
```powershell
|
||
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` 一跑就露馅:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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 上碰运气:
|
||
|
||
```toml
|
||
[tool.pyright]
|
||
venvPath = "."
|
||
venv = ".venv"
|
||
pythonVersion = "3.13" # 不写就跟着 PATH 上那个 3.14 走
|
||
```
|
||
|
||
2. 在 `.venv\Lib\site-packages` 放一个 `.pth` 指向 base 的 site-packages。
|
||
pyright **不读** `pyvenv.cfg`,但**会读 `.pth`**:
|
||
|
||
```powershell
|
||
& '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` 里
|
||
|
||
```python
|
||
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` 一问就现原形:
|
||
|
||
```text
|
||
“numeric”的类型为“Series | Unknown | Self@DataFrame”
|
||
“numeric.std(axis=0)”的类型为“Series | float | Unknown”
|
||
```
|
||
|
||
几个**没用**的修法(都实测过):
|
||
|
||
- `numeric.std(axis=0)` —— 接收方本身还是并集,重载照样解析成并集;
|
||
- `data[list(numeric_cols)]` —— 不改变重载选择;
|
||
- 把参数改由一个类型明确的 `frame` 局部变量承接 —— `frame` 确实干净了,
|
||
但 `frame[Index]` 还是同一个重载问题。
|
||
|
||
有效的修法是**只在取列子表那一句上按运行时的真值收窄**:
|
||
|
||
```python
|
||
from typing import cast
|
||
|
||
numeric = cast(pd.DataFrame, data[numeric_cols])
|
||
```
|
||
|
||
`cast` 零运行时开销,而且能把**下面所有行**留在类型检查范围内 —— 比在 `.replace` 那一行
|
||
挂 `# pyright: ignore` 好:那样等于连带放弃后面每一行的检查。
|
||
|
||
**一句诊断口诀**:pyright 说某行"属性不存在"、但运行时确实能跑,先 `reveal_type(...)`
|
||
把真实推断打出来,看并集里混进了什么,别急着加 `# 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 升级记录
|
||
|
||
[FENCE]powershell
|
||
conda update -n base --all -y # 365 个包更新,下载 638 MB,耗时 1033 秒
|
||
conda install -n mcm-toolkit numpy=2.4.6 -y # 把项目环境对齐到 base 的 numpy
|
||
[FENCE]
|
||
|
||
* `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` 里。
|
||
|
||
## 六、国内镜像(需要装新包时)
|
||
|
||
```powershell
|
||
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`)两套环境都有。其余按需:
|
||
|
||
```powershell
|
||
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,会看到:
|
||
|
||
```text
|
||
Matplotlib created a temporary cache directory at ... because there was an issue with
|
||
the default path (C:\Users\guzhu\.matplotlib)
|
||
```
|
||
|
||
这不是配置问题,是 `~\.matplotlib` 不可写。正常终端里不会出现;
|
||
真遇到时设置 `MPLCONFIGDIR` 指向可写目录即可:
|
||
|
||
```powershell
|
||
$env:MPLCONFIGDIR = "$env:TEMP\matplotlib"
|
||
```
|