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
+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
|
||||
|
||||
|
||||
Reference in new issue
Block a user