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:
1 parent
f95fbd2726
commit
b70a96dd0c
12 files changed
+356
-101
No files matched your search
@@ -271,6 +271,36 @@
|
||||
**`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` 新增 `TestAsArray` **8 条**(五种容器的 `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 的口径一致。
|
||||
|
||||
## [0.0.1] - 2026-10-01
|
||||
|
||||
### Added
|
||||
|
||||
@@ -193,13 +193,25 @@
|
||||
- 测试 `tests/test_stats.py`:**+10 条(36 → 46)**,含把原先挂着 skip 的 `test_confidence_interval_matches_scipy` 转正;
|
||||
对照一律用解析式(z 公式、Welch `df` 公式、`t = (mean − value)/(s/√n)`),不拿实现对自己
|
||||
- ⚠️ **`t_test` 还有 3 个口径待用户拍板**(Codex 未改,见「下一步」第 6 条)
|
||||
- [x] **`core/validate.py` 的 `as_array`**(统一入口,2026-10-03 落地)
|
||||
- 用户提出"每个函数都抄一遍入口/校验/交换太麻烦"。结论:**入口能统一,出口不能一刀切** ——
|
||||
还原要求"输出与输入同形",而 `describe` / `correlation_*` / `confidence_interval` / `t_test`
|
||||
的输出形状天生和输入不同,硬套只会重演易错点 14。
|
||||
- 契约:`as_array(x, *, numeric_only=True, dtype=float, ndim=None) -> (arr, kind)`,
|
||||
`kind` 是 `"frame"` / `"series"` / `"array"`(**只报告原类型,还原由调用方手写**,用户拍的板)。
|
||||
- 覆盖掉重复:原先 `select_dtypes` / `isinstance(..., pd.DataFrame|Series)` 散在 **5 个文件 31 处**;
|
||||
现在 `normalize` / `standardize` / `log_transform` / `zscore` / `confidence_interval` / `t_test`
|
||||
各只剩 1 行入口。`data/transform.py` 的 `_NUMERIC_TYPES` 白名单连同 3 份副本一起删掉。
|
||||
- 两条硬规矩:**默认绝不摊平**(要摊平显式 `ndim=1`);**不删缺失值**(删缺失是各函数的语义,
|
||||
见决策 45 与易错点 15)。`numeric_only` 按"输出是否与输入同形"选,不按喜好选(决策 44)。
|
||||
- `check_array`(L1,未实现)实现时直接复用 `as_array`,一份实现两张脸
|
||||
- [ ] 剩余 3 项:`bootstrap_ci` `monte_carlo` `save_figure`
|
||||
|
||||
### 验证结果
|
||||
|
||||
| 检查 | venv | conda `mcm-toolkit` |
|
||||
|---|---|---|
|
||||
| `pytest` | 见下(单列) | **125 passed, 37 skipped(0 xfail)** |
|
||||
| `pytest` | 见下(单列) | **134 passed, 37 skipped(0 xfail)** |
|
||||
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
||||
| `ruff check` | All checks passed | **All checks passed**(0 error) |
|
||||
| `scripts/doctor.ps1` | 跑通 | — |
|
||||
@@ -209,13 +221,14 @@
|
||||
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
||||
`docs/04-environment.md` 坑 7)
|
||||
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
||||
- **162 条用例:125 passed / 37 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
||||
`test_core.py` 15、`test_solve.py` 9、`test_viz.py` 8、`test_decision.py` 7、`test_stats.py` 46、
|
||||
- **171 条用例:134 passed / 37 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
||||
`test_core.py` 23、`test_solve.py` 9、`test_viz.py` 8、`test_decision.py` 7、`test_stats.py` 47、
|
||||
`test_models.py` 5
|
||||
- **`xfail` 再次清零**:`correlation_matrix` 的契约冲突已按方案 A 解决(决策 41),那条用例转正
|
||||
- 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
|
||||
- `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz`
|
||||
- 37 条 skip 是等实现的算法(`train_test_split`、`bootstrap_ci`、`monte_carlo`、`anova`、`fit_distribution` 等)
|
||||
- 37 条 skip 是等实现的算法(`train_test_split`、`bootstrap_ci`、`monte_carlo`、`anova`、`fit_distribution` 等);
|
||||
`TestValidate` 的 4 条仍挂着 —— `check_*` 还是 `NotImplementedError`,但它们实现时应当直接复用 `as_array`
|
||||
- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**,
|
||||
且 pyright 在同一处报 `reportArgumentType`。**不是静态误报**:pyright CLI 复现 + 测试真红。
|
||||
结论:见到红线先跑 CLI 与 `pytest`,**不许直接挂 `# type: ignore`**(见个人易错点 10)
|
||||
@@ -293,6 +306,10 @@
|
||||
原码把三种 method 的区间**无条件**夹进 `[0, 1]`,且 `min` / `max` 参数写反(见易错点 15),实测把身高均值区间下界压成 `0.0`。均值区间的量纲本来就不在 `[0, 1]` 里;只有比例 `p ± z·√(p(1−p)/n)` 的边界才有意义。**Codex 代改、待用户确认** —— 若原意是"什么区间都夹",改回 `_clip` 一行即可 |
|
||||
| 43 | scipy 检验结果的**统计量**也走 `_statistic(result: Any) -> float` 助手,与决策 40 的 `_pvalue` 成对 |
|
||||
`TtestResult` 与 `PearsonRResult` 同源(`_make_tuple_bunch` 运行时拼类),存根里 `.statistic` / `.pvalue` 都没有。用户报的「307 / 308 行报错」就是这两处,**不是真 bug**(探针实测取值正常)。见 `docs/04-environment.md` 坑 10 |
|
||||
| 44 | 所有收数值的函数**统一从 `core.validate.as_array` 进**;**还原不代做**,调用方按返回的 `kind` 自己手写 |
|
||||
用户 2026-10-03 提"每个函数都抄一遍入口/校验/交换太麻烦"。能统一的只有**入口**:出口要求"输出与输入同形",而 `describe` / `correlation_*` / `confidence_interval` / `t_test` 天生不同形,硬套会重演易错点 14。用户明确"不要三个函数""转换回去自己手打",所以 `as_array` 只返回 `(arr, kind)`,`ArraySpec` / `restore` 都不做。`numeric_only` 默认 `True`(用户拍的板),但**按输出契约选**:同形函数必须显式 `False` |
|
||||
| 45 | `as_array` **不删缺失值**;容器类型/内容转换失败**统一抛 `TypeError`** |
|
||||
删缺失是**语义**(`df.corr` 成对删、`describe` 整列统计、`paired=True` 的 t 检验必须成对删),在入口层删掉就把行对应关系永久丢了 —— 这正是 `t_test` 待拍板的口径之一。错误口径统一为 `TypeError` 是**Codex 代选、待用户确认**:`normalize(含文本列的表)` 原先由 numpy 抛 `ValueError`,现在是 `TypeError`(内容转不成 float);`describe` 的 `weights` 同类情况目前仍抛 `ValueError`(决策 39 说不统一就列出来) |
|
||||
`PearsonRResult` / `SignificanceResult` 是 `_make_tuple_bunch` 在**运行时**拼出来的类,存根里没有 `pvalue` 字段:解包会退化成 `object`、属性访问会报"未知"。运行时完全正常(实测三个函数返回的都是 `np.float64`)。收成 `Any` 是**一处**收窄,调用点仍然干净;`# type: ignore` 要挂在每一处,且会连带关掉该行后面的检查。见 `docs/04-environment.md` 坑 10 |
|
||||
用户 2026-10-03 明确:没考虑到的非法情况"告诉我我自己改"——因为那是设计决策,得由写实现的人形成判断;而 lint / 进度板 / 测试这类机械活再由用户动手只是浪费。这条与决策 25 / 31 配套:25 定"谁写 src",31 定"Codex 可改的 bug 范围",39 定"非算法事务的默认归口" |
|
||||
docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向,实现里多做的 `.T` 是净负担 —— 且不是"风格不同"那么轻:转置后 `.loc[指标名] = <按变量索引的 Series>` 与列名零交集,pandas **不报错**、把四行整行填成 NaN。方向约定以"能按列名对齐"为准,见易错点 11 |
|
||||
@@ -321,3 +338,12 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
|
||||
- **④ `t_test(DataFrame)` 会把所有数值列摊平成一个样本**(已写进 `Notes`)。
|
||||
多列不同量纲(`gdp` + `pop`)会被混在一起,而且**没法**拿"一张表里两列"当两组比。
|
||||
要不要改成:多数值列的 DataFrame 直接 `ValueError`、强制先自己取列?
|
||||
|
||||
7. **`as_array` 落地后留下的 3 个小口径(Codex 没代选,按决策 39)**:
|
||||
- `int` / `float` 标量还在白名单里(为兼容 `normalize` / `standardize` / `log_transform`
|
||||
三个 docstring 写的"数值型标量")。但标量进来是 **0 维数组**,`axis=0` 的算法照样炸 ——
|
||||
要不要把标量摘掉、同时改三处 docstring?
|
||||
- 内容转不成 float 时统一抛 `TypeError`(决策 45)—— `normalize(含文本列的表)` 原来抛 `ValueError`。
|
||||
要不要改回 `ValueError`("取值问题")?
|
||||
- 现在 `kind == "frame"` 时你要自己再写一遍 `x.select_dtypes(include=[np.number]).columns` 取列名。
|
||||
嫌烦的话可以让 `as_array` 顺带把选中的列返回(改成三元组),一句话的事。
|
||||
+27
-1
@@ -24,7 +24,7 @@ src/mcm_toolkit/
|
||||
├── core/ ① 基础设施 —— 所有包的地基
|
||||
│ ├── result.py Result
|
||||
│ ├── random.py set_seed / get_rng
|
||||
│ └── validate.py check_array / check_same_length / check_probability / check_square
|
||||
│ └── validate.py as_array(统一入口)/ check_array / check_same_length / check_probability / check_square
|
||||
│
|
||||
├── data/ ② 数据进出 —— 最高频
|
||||
│ ├── io.py load_table / save_table
|
||||
@@ -183,6 +183,32 @@ docstring 给人看。两处打架时,合法用法会被标红,真正的错
|
||||
|
||||
参考实现:`core/random.py` 的 `get_rng`。
|
||||
|
||||
### 4.8 入口只有一个:`core.validate.as_array`
|
||||
|
||||
**凡是要数值数组的函数,第一行都从 `as_array` 进**,不要各自抄
|
||||
`isinstance(x, pd.DataFrame)` / `select_dtypes` / `np.asarray`:
|
||||
|
||||
```python
|
||||
arr, kind = as_array(x, numeric_only=False) # 入口:白名单 + 转 ndarray + 报告原类型
|
||||
... # 中间只写算法
|
||||
if kind == "frame": # 出口:自己手写,各函数形状不同,没法一刀切
|
||||
return pd.DataFrame(result, index=x.index, columns=x.columns)
|
||||
if kind == "series":
|
||||
return pd.Series(result, index=x.index, name=x.name)
|
||||
return result
|
||||
```
|
||||
|
||||
三条硬规矩:
|
||||
|
||||
1. **`numeric_only` 按"输出契约"选,不按喜好选**:输出要与输入**同形**的函数
|
||||
(`normalize` / `standardize` / `log_transform`)必须 `False` —— 跳过非数值列会让返回的表少列、
|
||||
对不上原表;输出是"一张数值结果表"的(`zscore` / `confidence_interval` / `t_test`)用默认的 `True`。
|
||||
2. **默认不摊平**。要摊平必须显式 `ndim=1`(`t_test` 曾因静默摊平把 `gdp` 和 `pop` 混成一个样本)。
|
||||
3. **`as_array` 不删缺失值**。`df.corr` 成对删、`describe` 整列统计、`paired=True` 的 t 检验必须**成对**删 ——
|
||||
在这一层删掉,行与行的对应关系就永久丢了。删缺失是**语义**,归各函数。
|
||||
|
||||
只走入口、不走出口的(输出与输入不同形):`describe`、`correlation_*`、`confidence_interval`、`t_test`。
|
||||
|
||||
---
|
||||
|
||||
## 五、测试策略
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
|---|---|
|
||||
| `core` | `check_array` `check_same_length` `check_probability` `check_square_matrix` |
|
||||
| `data` | `impute_missing` `winsorize` `log_transform` `one_hot_encode` `train_test_split` `kfold_split` `bootstrap_sample` |
|
||||
|
||||
> `core.validate.as_array`(**统一入口**,决策 44)已提前落地。它不是"多一个公开 API",
|
||||
| `stats` | `zscore` `coefficient_of_variation` `confidence_interval` `t_test` `anova` `chi_square_test` `fit_distribution` `goodness_of_fit` `bootstrap_statistic` |
|
||||
| `models` | `rmse` `mae` `mape` `smape` `r2_score` `adjusted_r2` `compare_models` `fit_linear` `fit_polynomial` `fit_nonlinear` `predict` `exponential_smoothing` `fit_arima` `forecast` `fit_forest` `fit_logistic` `kmeans` `pca` `cross_validate` |
|
||||
| `solve` | `minimize` `linear_program` `integer_program` `dynamic_programming` `knapsack_dp` `simulated_annealing` `pareto_front` `monte_carlo_probability` `monte_carlo_integral` `random_walk` `markov_simulate` `markov_steady_state` `mm1_metrics` `local_sensitivity` `one_at_a_time` `parameter_sweep` `robustness_test` `sobol_analyze` `morris_analyze` |
|
||||
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
"""core —— 基础设施:结果对象、随机数、参数校验。
|
||||
"""core —— 基础设施:结果对象、随机数、参数校验与入口转换。
|
||||
|
||||
其他所有包都依赖本包;本包**不**依赖任何其他子包。
|
||||
"""
|
||||
@@ -6,6 +6,7 @@
|
||||
from .random import get_rng, set_seed
|
||||
from .result import Result
|
||||
from .validate import (
|
||||
as_array,
|
||||
check_array,
|
||||
check_probability,
|
||||
check_same_length,
|
||||
@@ -14,6 +15,7 @@ from .validate import (
|
||||
|
||||
__all__ = [
|
||||
"Result",
|
||||
"as_array",
|
||||
"get_rng",
|
||||
"set_seed",
|
||||
"check_array",
|
||||
|
||||
@@ -1,33 +1,147 @@
|
||||
"""参数校验。
|
||||
"""参数校验与入口转换。
|
||||
|
||||
目的不是"教用户用库",而是**把错误提前到调用点**:
|
||||
比赛时最贵的是 debug 时间,一个 `ValueError: 数组长度不一致` 比
|
||||
十层 traceback 里的 `IndexError` 值钱得多。
|
||||
|
||||
**所有收数值的函数都从 `as_array` 进**:它一次做完"类型白名单 + 转 ndarray + 告诉调用方原来是什么",
|
||||
省掉每个函数各抄一遍 `isinstance(x, pd.DataFrame)` / `select_dtypes` / `np.asarray`。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
__all__ = [
|
||||
"as_array",
|
||||
"check_array",
|
||||
"check_probability",
|
||||
"check_same_length",
|
||||
"check_square_matrix",
|
||||
]
|
||||
|
||||
ArrayKind = Literal["frame", "series", "array"]
|
||||
|
||||
_INPUT_TYPES = (pd.DataFrame, pd.Series, np.ndarray, list, tuple, int, float)
|
||||
|
||||
|
||||
def as_array(
|
||||
x: Any,
|
||||
*,
|
||||
numeric_only: bool = True,
|
||||
dtype: Any = float,
|
||||
ndim: int | None = None,
|
||||
) -> tuple[np.ndarray, ArrayKind]:
|
||||
"""统一入口:把入参收成 `ndarray`,并告诉调用方"原来是什么"。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x:
|
||||
`pandas.DataFrame` / `pandas.Series` / `numpy.ndarray` / `list` / `tuple`。
|
||||
其余类型一律 `TypeError` —— 注意 `str` **也是可迭代对象**,白名单必须显式挡住,
|
||||
否则 `"abc"` 会被当成三个字符悄悄算下去。
|
||||
numeric_only:
|
||||
默认 `True`:`DataFrame` **只取数值列**,非数值列自动跳过(一列都没有则 `ValueError`)。
|
||||
走"元素级、输入输出同形"的函数(`normalize` / `standardize` / `log_transform`)
|
||||
必须写 `numeric_only=False`:跳过列会让返回的表**少列**,与输入对不上;
|
||||
输出是"一张数值结果表"的函数(`zscore` / `confidence_interval` / `t_test`)用默认值。
|
||||
对 `Series` / 数组 / 列表无意义,自动忽略。
|
||||
dtype:
|
||||
目标 dtype,默认 `float`;传 `None` 表示保持原 dtype。
|
||||
ndim:
|
||||
期望维度,默认 `None` 表示**保持原样、绝不摊平**。
|
||||
`1` 会**摊平**成一维("一串样本"的函数如 `t_test` / `confidence_interval` 要显式写);
|
||||
`2` 只**校验**不重塑 —— 一维数据到底是"1 个样本"还是"1 个特征"没有唯一答案,不能替你猜。
|
||||
|
||||
Returns
|
||||
-------
|
||||
(numpy.ndarray, ArrayKind)
|
||||
第二个元素是 `"frame"` / `"series"` / `"array"`,表示 `x` 原来是哪一类。
|
||||
还原**故意不代做**,调用方自己手写(同一个函数不同形,没法一刀切):
|
||||
|
||||
```python
|
||||
arr, kind = as_array(x, numeric_only=False)
|
||||
... # 只写算法
|
||||
if kind == "frame":
|
||||
return pd.DataFrame(result, index=x.index, columns=x.columns)
|
||||
if kind == "series":
|
||||
return pd.Series(result, index=x.index, name=x.name)
|
||||
return result
|
||||
```
|
||||
|
||||
`numeric_only=True` 时列名要用**同一份口径**:
|
||||
`x.select_dtypes(include=[np.number]).columns`。
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
`x` 不在白名单里,或内容转不成 `dtype`。
|
||||
ValueError
|
||||
`numeric_only=True` 但 `DataFrame` 里一列数值都没有;`ndim` 不是 `None` / `1` / `2`;
|
||||
`ndim=2` 但 `x` 不是二维。
|
||||
|
||||
Notes
|
||||
-----
|
||||
**本函数不删缺失值。** `DataFrame.corr` 是成对删、`describe` 是整列统计、
|
||||
`paired=True` 的 t 检验必须**成对**删 —— 一旦在这里写 `arr[~isnan(arr)]`,
|
||||
行与行的对应关系就永久丢了。删缺失是**语义**,交给各函数自己决定。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> arr, kind = as_array(pd.DataFrame({"a": [1, 2], "city": ["X", "Y"]}))
|
||||
>>> arr.shape, kind
|
||||
((2, 1), 'frame')
|
||||
"""
|
||||
if isinstance(x, pd.DataFrame):
|
||||
kind: ArrayKind = "frame"
|
||||
source: Any = x
|
||||
if numeric_only:
|
||||
numeric_cols = x.select_dtypes(include=[np.number]).columns
|
||||
if len(numeric_cols) == 0:
|
||||
raise ValueError(f"x 里一列数值都没有,只有 {list(x.columns)}")
|
||||
# x[Index] 在 pandas 存根里是 DataFrame | Series 重载,按运行时真值收窄(坑 8)
|
||||
source = cast(pd.DataFrame, x[numeric_cols])
|
||||
elif isinstance(x, pd.Series):
|
||||
kind = "series"
|
||||
source = x
|
||||
elif isinstance(x, _INPUT_TYPES):
|
||||
kind = "array"
|
||||
source = x
|
||||
else:
|
||||
raise TypeError(
|
||||
f"x 必须是 DataFrame / Series / ndarray / list / tuple,你传入了 {type(x).__name__}"
|
||||
)
|
||||
|
||||
try:
|
||||
arr = np.asarray(source, dtype=dtype)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise TypeError(f"x 的内容转不成 {dtype} 数组:{e}") from None
|
||||
|
||||
if ndim is not None:
|
||||
if ndim not in (1, 2):
|
||||
raise ValueError(f"ndim 只能是 None / 1 / 2,你传入了 {ndim}")
|
||||
if ndim == 1:
|
||||
arr = arr.ravel()
|
||||
elif arr.ndim != 2:
|
||||
raise ValueError(f"ndim=2 要求二维输入,但 x 是 {arr.ndim} 维")
|
||||
|
||||
return arr, kind
|
||||
|
||||
|
||||
def check_array(x: Any, ndim: int | None = None, finite: bool = True) -> np.ndarray:
|
||||
"""转成 ndarray 并做基本检查(维度、有限值)。
|
||||
|
||||
实现时应当复用 `as_array`(丢掉第二个返回值即可),不要重写一遍转换逻辑。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> check_array([1, 2, 3]).shape
|
||||
(3,)
|
||||
"""
|
||||
# TODO(L0)
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -38,17 +152,17 @@ def check_same_length(*arrays: Any) -> None:
|
||||
--------
|
||||
>>> check_same_length([1, 2], [3, 4])
|
||||
"""
|
||||
# TODO(L0)
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def check_probability(p: float) -> float:
|
||||
"""检查 p 落在 [0, 1],返回 float。"""
|
||||
# TODO(L0)
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def check_square_matrix(a: Any) -> np.ndarray:
|
||||
"""检查是方阵,返回 ndarray。AHP 判断矩阵、转移矩阵都要用。"""
|
||||
# TODO(L0)
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -13,10 +13,9 @@ from typing import Any
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
__all__ = ["log_transform", "normalize", "one_hot_encode", "standardize"]
|
||||
from ..core.validate import as_array
|
||||
|
||||
# 数值输入白名单。DataFrame 必须写进来,否则下面所有 is_df 分支都是死代码。
|
||||
_NUMERIC_TYPES = (int, float, list, tuple, np.ndarray, pd.Series, pd.DataFrame)
|
||||
__all__ = ["log_transform", "normalize", "one_hot_encode", "standardize"]
|
||||
|
||||
|
||||
def normalize(x: Any, method: str = "minmax", feature_range: tuple[float, float] = (0.0, 1.0)) -> Any:
|
||||
@@ -68,15 +67,8 @@ def normalize(x: Any, method: str = "minmax", feature_range: tuple[float, float]
|
||||
if feature_range[0] >= feature_range[1]:
|
||||
raise ValueError(f"feature_range的下界必须小于上界,你传入了{feature_range}")
|
||||
|
||||
if not isinstance(x, _NUMERIC_TYPES):
|
||||
raise TypeError(f"输入必须为数值型、列表、numpy array、pandas Series 或 DataFrame,你传入了{type(x)}")
|
||||
|
||||
is_df = isinstance(x, pd.DataFrame)
|
||||
is_series = isinstance(x, pd.Series)
|
||||
original_cols: Any = x.columns if is_df else None
|
||||
original_index: Any = x.index if (is_df or is_series) else None
|
||||
|
||||
arr = np.asarray(x, dtype=float)
|
||||
# numeric_only=False:输出要与输入**同形**,跳过非数值列会让返回的表少列、对不上原表。
|
||||
arr, kind = as_array(x, numeric_only=False)
|
||||
|
||||
if method == "minmax":
|
||||
x_min = np.min(arr, axis=0)
|
||||
@@ -95,10 +87,10 @@ def normalize(x: Any, method: str = "minmax", feature_range: tuple[float, float]
|
||||
x_max = np.where(x_max == 0, 1.0, x_max)
|
||||
result = arr / x_max
|
||||
|
||||
if is_df:
|
||||
return pd.DataFrame(result, columns=original_cols, index=original_index)
|
||||
if is_series:
|
||||
return pd.Series(result, index=original_index)
|
||||
if kind == "frame":
|
||||
return pd.DataFrame(result, index=x.index, columns=x.columns)
|
||||
if kind == "series":
|
||||
return pd.Series(result, index=x.index, name=x.name)
|
||||
return result
|
||||
|
||||
|
||||
@@ -128,15 +120,8 @@ def standardize(x: Any, ddof: int = 0) -> Any:
|
||||
常量列(`std == 0`)不产生 nan / inf,固定输出 0,与 `sklearn` 的
|
||||
`StandardScaler` 对零方差列的处理一致。
|
||||
"""
|
||||
if not isinstance(x, _NUMERIC_TYPES):
|
||||
raise TypeError(f"输入必须为数值型、列表、numpy array、pandas Series 或 DataFrame,你传入了{type(x)}")
|
||||
|
||||
is_df = isinstance(x, pd.DataFrame)
|
||||
is_series = isinstance(x, pd.Series)
|
||||
original_cols: Any = x.columns if is_df else None
|
||||
original_index: Any = x.index if (is_df or is_series) else None
|
||||
|
||||
arr = np.asarray(x, dtype=float)
|
||||
# numeric_only=False:输出要与输入**同形**,跳过非数值列会让返回的表少列、对不上原表。
|
||||
arr, kind = as_array(x, numeric_only=False)
|
||||
|
||||
mean = np.mean(arr, axis=0)
|
||||
|
||||
@@ -146,10 +131,10 @@ def standardize(x: Any, ddof: int = 0) -> Any:
|
||||
|
||||
result = (arr - mean) / std
|
||||
|
||||
if is_df:
|
||||
return pd.DataFrame(result, columns=original_cols, index=original_index)
|
||||
if is_series:
|
||||
return pd.Series(result, index=original_index)
|
||||
if kind == "frame":
|
||||
return pd.DataFrame(result, index=x.index, columns=x.columns)
|
||||
if kind == "series":
|
||||
return pd.Series(result, index=x.index, name=x.name)
|
||||
return result
|
||||
|
||||
|
||||
@@ -181,15 +166,8 @@ def log_transform(x: Any, offset: float | None = None) -> Any:
|
||||
>>> log_transform([1.0, 2.718281828459045]).round(3).tolist()
|
||||
[0.0, 1.0]
|
||||
"""
|
||||
if not isinstance(x, _NUMERIC_TYPES):
|
||||
raise TypeError(f"输入必须为数值型、列表、numpy array、pandas Series 或 DataFrame,你传入了{type(x)}")
|
||||
|
||||
is_df = isinstance(x, pd.DataFrame)
|
||||
is_series = isinstance(x, pd.Series)
|
||||
original_cols: Any = x.columns if is_df else None
|
||||
original_index: Any = x.index if (is_df or is_series) else None
|
||||
|
||||
arr = np.asarray(x, dtype=float)
|
||||
# numeric_only=False:输出要与输入**同形**,跳过非数值列会让返回的表少列、对不上原表。
|
||||
arr, kind = as_array(x, numeric_only=False)
|
||||
|
||||
if offset is None:
|
||||
min_val = np.min(arr)
|
||||
@@ -201,10 +179,10 @@ def log_transform(x: Any, offset: float | None = None) -> Any:
|
||||
|
||||
result = np.log(arr + offset)
|
||||
|
||||
if is_df:
|
||||
return pd.DataFrame(result, columns=original_cols, index=original_index)
|
||||
if is_series:
|
||||
return pd.Series(result, index=original_index)
|
||||
if kind == "frame":
|
||||
return pd.DataFrame(result, index=x.index, columns=x.columns)
|
||||
if kind == "series":
|
||||
return pd.Series(result, index=x.index, name=x.name)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ from typing import Any, cast
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from ..core.validate import as_array
|
||||
|
||||
__all__ = ["coefficient_of_variation", "describe", "zscore"]
|
||||
|
||||
|
||||
@@ -139,25 +141,7 @@ def zscore(x: Any, ddof: int = 0) -> Any:
|
||||
与 `data.transform.standardize` 是同一个公式;差别只在 DataFrame 里遇到
|
||||
非数值列时:这里**跳过**,`standardize` 会直接抛错。
|
||||
"""
|
||||
allowed_types = (pd.DataFrame, pd.Series, list, tuple, np.ndarray)
|
||||
if not isinstance(x, allowed_types):
|
||||
raise TypeError(f"仅接受{allowed_types},不接受{type(x)}")
|
||||
|
||||
is_df = isinstance(x, pd.DataFrame)
|
||||
is_series = isinstance(x, pd.Series)
|
||||
if is_df:
|
||||
numeric_cols = x.select_dtypes(include=[np.number]).columns
|
||||
if len(numeric_cols) == 0:
|
||||
raise ValueError("数据中没有数值列,无法计算 Z 分数")
|
||||
target_data = cast(pd.DataFrame, x[numeric_cols])
|
||||
original_cols = target_data.columns
|
||||
original_index = target_data.index
|
||||
else:
|
||||
target_data = x
|
||||
original_cols = None
|
||||
original_index = x.index if is_series else None
|
||||
|
||||
arr = np.asarray(target_data, dtype=float)
|
||||
arr, kind = as_array(x, numeric_only=True)
|
||||
|
||||
mean = np.mean(arr, axis=0)
|
||||
std = np.std(arr, axis=0, ddof=ddof)
|
||||
@@ -165,10 +149,12 @@ def zscore(x: Any, ddof: int = 0) -> Any:
|
||||
std = np.where(std == 0, 1.0, std)
|
||||
|
||||
result = (arr - mean) / std
|
||||
if is_df:
|
||||
return pd.DataFrame(result, columns=original_cols, index=original_index)
|
||||
if is_series:
|
||||
return pd.Series(result, index=original_index)
|
||||
if kind == "frame":
|
||||
# 列名口径与 as_array(numeric_only=True) 一致,都是只取数值列
|
||||
cols = x.select_dtypes(include=[np.number]).columns
|
||||
return pd.DataFrame(result, index=x.index, columns=cols)
|
||||
if kind == "series":
|
||||
return pd.Series(result, index=x.index, name=x.name)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import numpy as np
|
||||
import pandas as pd
|
||||
import scipy.stats as stats
|
||||
|
||||
from ..core.validate import as_array
|
||||
|
||||
__all__ = [
|
||||
"anova",
|
||||
"chi_square_test",
|
||||
@@ -252,24 +254,14 @@ def confidence_interval(
|
||||
--------
|
||||
>>> lo, hi = confidence_interval([1, 2, 3, 4, 5], confidence=0.95)
|
||||
"""
|
||||
if not isinstance(x, (list, tuple, np.ndarray, pd.Series, pd.DataFrame)):
|
||||
raise TypeError(f"x 必须是 list / tuple / ndarray / Series / DataFrame,而你传入了{type(x)}")
|
||||
# 入口统一走 as_array(容器类型/数值转换的错误口径只有一处);样本是"一串数",显式摊平。
|
||||
arr, _ = as_array(x, numeric_only=True, ndim=1)
|
||||
|
||||
if method not in _VALID_CONFIDENCE_METHODS:
|
||||
raise ValueError(f"不支持的方法:{method},仅支持{list(_VALID_CONFIDENCE_METHODS)}")
|
||||
if not 0 < confidence < 1:
|
||||
raise ValueError(f"confidence 必须在 (0, 1) 开区间内,而你传入了{confidence}")
|
||||
|
||||
if isinstance(x, pd.DataFrame):
|
||||
numeric_cols = x.select_dtypes(include=[np.number]).columns
|
||||
if len(numeric_cols) == 0:
|
||||
raise ValueError("x 里没有数值列")
|
||||
arr = cast(pd.DataFrame, x[numeric_cols]).to_numpy(dtype=float).ravel()
|
||||
else:
|
||||
try:
|
||||
arr = np.asarray(x, dtype=float).ravel()
|
||||
except (ValueError, TypeError) as e:
|
||||
raise TypeError(f"x 必须是数值型数据,无法转成 float 数组:{e}") from None
|
||||
|
||||
arr = arr[~np.isnan(arr)]
|
||||
n = len(arr)
|
||||
if n < 2:
|
||||
@@ -328,16 +320,7 @@ def t_test(a: Any, b: Any | None = None, value: float = 0.0, equal_var: bool = F
|
||||
if not (0 < alpha < 1):
|
||||
raise ValueError(f"显著性水平 alpha 必须在 (0, 1) 之间,你传入了 {alpha}")
|
||||
def _clean_data(data: Any) -> np.ndarray:
|
||||
if isinstance(data, pd.DataFrame):
|
||||
numeric_cols = data.select_dtypes(include=[np.number]).columns
|
||||
if len(numeric_cols) == 0:
|
||||
raise ValueError("没有找到数值列")
|
||||
arr = cast(pd.DataFrame, data[numeric_cols]).to_numpy(dtype=float).ravel()
|
||||
else:
|
||||
try:
|
||||
arr = np.asarray(data, dtype=float).ravel()
|
||||
except (ValueError, TypeError) as e:
|
||||
raise TypeError(f"样本必须是数值型数据,无法转成 float 数组:{e}") from None
|
||||
arr, _ = as_array(data, numeric_only=True, ndim=1)
|
||||
return arr[~np.isnan(arr)]
|
||||
|
||||
a_arr = _clean_data(a)
|
||||
|
||||
+79
-1
@@ -1,11 +1,12 @@
|
||||
"""core 包测试。
|
||||
|
||||
TODO(L0):TestValidate 实现后去掉 skip 标记(Result 与 Random 已完成)。
|
||||
TODO(L0):TestValidate 的 check_* 实现后去掉 skip 标记(Result / Random / as_array 已完成)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -96,6 +97,83 @@ class TestValidate:
|
||||
check_square_matrix([[1, 2, 3], [4, 5, 6]])
|
||||
|
||||
|
||||
class TestAsArray:
|
||||
"""`as_array` 是所有收数值函数的统一入口:白名单 + 转 ndarray + 报告原类型。"""
|
||||
|
||||
def test_reports_kind_for_every_supported_container(self):
|
||||
from mcm_toolkit.core import as_array
|
||||
|
||||
df = pd.DataFrame({"a": [1.0, 2.0], "city": ["X", "Y"]})
|
||||
series = pd.Series([1.0, 2.0], index=["u", "v"], name="s")
|
||||
|
||||
assert as_array(df)[1] == "frame"
|
||||
assert as_array(series)[1] == "series"
|
||||
for raw in (np.array([1.0, 2.0]), [1.0, 2.0], (1.0, 2.0)):
|
||||
assert as_array(raw)[1] == "array"
|
||||
|
||||
def test_frame_keeps_only_numeric_columns_by_default(self):
|
||||
# numeric_only 默认 True:一列数值都没有就报 ValueError(与 describe / zscore 同口径)
|
||||
from mcm_toolkit.core import as_array
|
||||
|
||||
df = pd.DataFrame({"a": [1.0, 2.0], "b": [3.0, 4.0], "city": ["X", "Y"]})
|
||||
arr, kind = as_array(df)
|
||||
|
||||
assert arr.shape == (2, 2) and kind == "frame"
|
||||
assert list(df.select_dtypes(include=[np.number]).columns) == ["a", "b"]
|
||||
|
||||
with pytest.raises(ValueError, match="数值"):
|
||||
as_array(pd.DataFrame({"city": ["X", "Y"]}))
|
||||
|
||||
def test_frame_with_text_and_numeric_only_false_raises(self):
|
||||
# 元素级函数(normalize / standardize / log_transform)走 numeric_only=False,
|
||||
# 因为跳过列会让返回的表少列、与输入对不上;转不动就该报错,不能静默丢列。
|
||||
from mcm_toolkit.core import as_array
|
||||
|
||||
df = pd.DataFrame({"a": [1.0, 2.0], "city": ["X", "Y"]})
|
||||
with pytest.raises(TypeError, match="转不成"):
|
||||
as_array(df, numeric_only=False)
|
||||
|
||||
def test_default_never_flattens(self):
|
||||
# 摊平曾是 t_test(DataFrame) 静默混合多列的根源,必须是显式行为
|
||||
from mcm_toolkit.core import as_array
|
||||
|
||||
df = pd.DataFrame({"a": [1.0, 2.0, 3.0], "b": [4.0, 5.0, 6.0]})
|
||||
assert as_array(df)[0].shape == (3, 2)
|
||||
assert as_array(df, ndim=1)[0].shape == (6,)
|
||||
assert as_array(pd.Series([1.0, 2.0, 3.0]), ndim=1)[0].shape == (3,)
|
||||
|
||||
def test_ndim_2_checks_instead_of_reshaping(self):
|
||||
# 一维数据是"1 个样本"还是"1 个特征"没有唯一答案,不能替调用方猜
|
||||
from mcm_toolkit.core import as_array
|
||||
|
||||
assert as_array([[1.0, 2.0]], ndim=2)[0].shape == (1, 2)
|
||||
with pytest.raises(ValueError, match="二维"):
|
||||
as_array([1.0, 2.0], ndim=2)
|
||||
with pytest.raises(ValueError, match="ndim"):
|
||||
as_array([1.0, 2.0], ndim=3)
|
||||
|
||||
def test_rejects_types_outside_the_whitelist(self):
|
||||
from mcm_toolkit.core import as_array
|
||||
|
||||
# str 也是可迭代对象:不显式挡住,"abc" 会被当成三个字符算下去
|
||||
for bad in ("abc", {1, 2}, {"a": 1}, None, object()):
|
||||
with pytest.raises(TypeError):
|
||||
as_array(bad)
|
||||
|
||||
def test_keeps_missing_values(self):
|
||||
# 删缺失是各函数的**语义**(corr 成对删、t_test 配对要成对删),不能在这里做
|
||||
from mcm_toolkit.core import as_array
|
||||
|
||||
arr = as_array([1.0, np.nan, 3.0])[0]
|
||||
assert arr.shape == (3,) and np.isnan(arr[1])
|
||||
|
||||
def test_dtype_none_keeps_original_dtype(self):
|
||||
from mcm_toolkit.core import as_array
|
||||
|
||||
assert as_array([1, 2, 3], dtype=None)[0].dtype.kind == "i"
|
||||
assert as_array([1, 2, 3])[0].dtype.kind == "f"
|
||||
|
||||
|
||||
def test_result_repr_is_readable():
|
||||
from mcm_toolkit.core import Result
|
||||
|
||||
|
||||
@@ -460,6 +460,18 @@ def test_zscore_constant_column_stays_zero():
|
||||
assert np.allclose(np.asarray(zscore([2.0, 2.0, 2.0])), 0.0)
|
||||
|
||||
|
||||
def test_zscore_series_keeps_index_and_name(rng):
|
||||
"""Series 进 Series 出,索引和 name 都要带回来 —— 否则管道里 concat 会丢列名。"""
|
||||
from mcm_toolkit.stats import zscore
|
||||
|
||||
s = pd.Series(rng.normal(size=50), index=[f"r{i}" for i in range(50)], name="price")
|
||||
out = zscore(s)
|
||||
|
||||
assert isinstance(out, pd.Series)
|
||||
assert out.name == "price"
|
||||
assert list(out.index) == list(s.index)
|
||||
|
||||
|
||||
def test_zscore_rejects_bad_type_and_all_text_frame():
|
||||
from mcm_toolkit.stats import zscore
|
||||
|
||||
|
||||
Reference in new issue
Block a user