refactor(core): 统一入口 as_array —— 六处手抄的入参处理收成一处(决策 44/45)

## 新增
- `core.validate.as_array(x, *, numeric_only=True, dtype=float, ndim=None) -> (arr, kind)`:
  一次做完"类型白名单 + 转 ndarray + 报告原来是什么容器"。`kind` 是 `"frame"` / `"series"` / `"array"`。
- **还原不代做**(用户拍的板):`describe` / `correlation_*` / `confidence_interval` / `t_test` 的
  输出天生与输入不同形,统一还原只会重演易错点 14;所以只报告 `kind`,调用方自己手写三行。
- `core/__init__.py` 导出 `as_array`;`check_array`(L1 未实现)的 docstring 注明实现时复用它。
- 入口三条硬规矩:**默认绝不摊平**(要摊平显式 `ndim=1`);**不删缺失值**;
  `numeric_only` 按"输出是否与输入同形"选,不按喜好选。

## 去重
- `normalize` / `standardize` / `log_transform` / `zscore` / `confidence_interval` / `t_test`
  六处手抄的 `isinstance(x, pd.DataFrame)` / `select_dtypes` / `np.asarray` 全部删掉,各剩 1 行入口。
- `data/transform.py` 的 `_NUMERIC_TYPES` 白名单连同 3 份副本一起删除 ——
  这类判断原来散在 **5 个文件 31 处**。
- `transform` 的三个函数走 `numeric_only=False`:它们的输出必须与输入同形,跳过非数值列会少列。

## 修掉
- `zscore` / `normalize` / `standardize` / `log_transform` 的 Series 分支**补回 `name`** ——
  此前 `pd.Series(result, index=...)` 会把 `name` 丢掉,管道里 `concat` 会变成无名列。
- "容器对、内容转不成 float"的错误类型,由 numpy 的 `ValueError` 统一成 `TypeError`(决策 45,**待确认**)。

## 测试与文档
- `tests/test_core.py` 新增 `TestAsArray` 8 条:五种容器的 kind、默认只取数值列、
  `numeric_only=False` 遇文本报错、默认不摊平、`ndim=2` 只校验不重塑、白名单外一律 TypeError、
  **保留 NaN**、`dtype=None` 保持原 dtype;`tests/test_stats.py` 新增 1 条(Series 保住 index/name)。
- 全量 **171 条:134 passed / 37 skipped / 0 xfail**;ruff 0,pyright 0;`examples/01` 跑通(exit 0)。
- `docs/02-architecture.md` 新增 §4.8「入口只有一个:`core.validate.as_array`」,目录树补 `as_array`;
  `docs/03-roadmap.md` 的 L1 表下补说明;`docs/dev/personal-pitfalls.md` 新增第 16 条并把索引补到 16。
- `STATUS.md`:决策 44 / 45,E 节记 as_array,「下一步」第 7 条列 3 个待拍板口径。

## 待用户拍板(Codex 一律未代选)
- `int` / `float` 标量是否继续留在白名单(现在会产出 0 维数组,`axis=0` 的算法照样炸)。
- "内容转不成 float" 该用 `TypeError`(现状)还是 `ValueError`。
- 要不要让 `as_array` 顺带返回选中的列(改三元组),省掉 `frame` 分支重写 `select_dtypes`。
This commit is contained in:
guzhujushi committed 2026-10-03 19:17:22 +08:00
1 parent f95fbd2726
commit b70a96dd0c
12 files changed
+356 -101

No files matched your search

+18
View File
@@ -37,6 +37,7 @@
| 13 | `Index == 标量` 得到布尔数组,`if` 要等到第 2 列才炸 | 2026-10-03 |
| 14 | 把"能看的表"和"能算的表"塞进同一个返回值 | 2026-10-03 |
| 15 | 区间被无条件夹到 `[0, 1]`,且 `min` / `max` 参数写反 | 2026-10-03 |
| 16 | 同一段入口转换抄了 N 遍,还当它是"本项目的写法" | 2026-10-03 |
---
@@ -269,3 +270,20 @@
④ 记住 `min` / `max` **不传播 nan、结果还取决于参数顺序**(`min(0.0, nan)` → `0.0`、
`min(nan, 0.0)` → `nan`)—— "夹一下"就能把 nan 悄悄换成边界值。
- 首次:2026-10-03|复发:0
### 16. 同一段入口转换抄了 N 遍,还当它是"本项目的写法"
- 现象:`normalize` / `standardize` / `log_transform` / `zscore` / `confidence_interval` / `t_test`
六个函数各自抄了同一段 5~8 行:白名单 `isinstance` → `is_df` / `is_series` → 抓 `columns` / `index`
→ `np.asarray`。这类判断一度散在 **5 个文件 31 处**。抄到第三遍时已经不觉得是重复了,
反而觉得"这就是本项目的入参写法"。
- 为什么会错:重复代码每次只多 3~5 行,看着不痛;但每多一处就多一次**口径分叉**的机会 ——
而且真的分叉了:`normalize` 拿整表 `np.asarray`(遇文本列直接抛)、`zscore` 用 `select_dtypes`
只取数值列、`confidence_interval` 还多一个 `.ravel()`。同一件事三种口径,出 bug 时你还得先判断
"这是哪个副本的行为"。
- 以后怎么防:① 同一段入参处理**抄到第 2 遍**就该抽,别等第 3 遍;
② 抽之前先问一句"输出的形状是不是和输入一样"——**入口能统一、出口不能一刀切**
(`describe` / `correlation_*` 的输出天生不同形,硬套还原会重演第 14 条);
③ 抽出来的函数只干**一件事**:把容器转成数组、并告诉调用方原来是什么。
选列 / 摊平 / 删缺失这些是**语义**,必须留在各函数里(否则重演第 15 条与 `paired=True` 的 NaN 错位)。
- 首次:2026-10-03|复发:0