feat(data): transform.py 四个函数落地;补 18 条测试、清 5 条 ruff
实现(用户本人写): - normalize(minmax / vector / max,三种都按列)+ feature_range 校验 + 常量列不出 nan - standardize(按列,ddof 真的传下去)+ 类型守卫 - log_transform(offset=None 时自动取 1 - min(x),保证 x + offset >= 1)+ 类型守卫 - one_hot_encode(自动识别对象列 / 指定列 / drop_first) - data/ 下的 L0 至此结清(L0 7/12) Codex 的轻度修复(不改接口与语义): - 清掉 5 条 ruff:RET503 / F541 / RET504 / RET505 / SIM108 - supported_method 与 feature_range 的校验提到 np.asarray 之前(守卫集中开头) - 修掉说反的报错文案(条件是要求下界 < 上界,原文写"上界必须小于下界") - one_hot_encode 自动识别时显式加上 str / string:只写 object 在 pandas 3 上是弃用路径, 每次调用发 Pandas4Warning,未来会变成"一列都选不中"的静默错误(详见坑 9) 测试: - 新写 18 条(transform 段),其中两条是把原先挂着的 skip 转为实跑 —— normalize / standardize 的实现此前一条测试都没跑到(详见个人易错点 9) - 82 passed / 40 skipped / 0 xfailed;ruff 0;pyright 0 文档: - STATUS.md:E 节 + 验证表(122 条用例分布)+ 下一步重写(transform 待定的 4 个口径)+ 决策 34 - docs/04-environment.md:新增坑 9(pandas 3 的 str dtype 与 select_dtypes 弃用路径,含实测对照表) - docs/dev/personal-pitfalls.md:只追加第 9 条(实现写完但测试还挂 skip,绿灯是假的) - CHANGELOG.md:本轮变更 + 记录 transform 待定的 4 个口径
This commit is contained in:
1 parent
5749e50e83
commit
6eab0babde
6 files changed
+330
-32
No files matched your search
+150
-3
@@ -354,11 +354,11 @@ def test_impute_missing_mode_does_not_mutate_input():
|
||||
assert bool(df["a"].isna().any())
|
||||
|
||||
|
||||
# ------------------------------------------------- 以下为未实现算法(L0/L1 待写)
|
||||
# ------------------------------------------------------ data: transform
|
||||
|
||||
|
||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||
def test_normalize_minmax_range(rng):
|
||||
"""minmax 是**按列**压到 [0, 1],不是整表压一次。"""
|
||||
from mcm_toolkit.data import normalize
|
||||
|
||||
x = rng.normal(size=(50, 3))
|
||||
@@ -367,7 +367,54 @@ def test_normalize_minmax_range(rng):
|
||||
assert np.allclose(out.max(axis=0), 1.0)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||
def test_normalize_minmax_doctest_values():
|
||||
"""docstring 里那个 Examples 就是规格,不能只有形似。"""
|
||||
from mcm_toolkit.data import normalize
|
||||
|
||||
assert normalize([1, 2, 3]).tolist() == [0.0, 0.5, 1.0]
|
||||
|
||||
|
||||
def test_normalize_respects_feature_range():
|
||||
from mcm_toolkit.data import normalize
|
||||
|
||||
out = np.asarray(normalize([1.0, 2.0, 3.0], feature_range=(-1.0, 1.0)))
|
||||
assert np.allclose(out, [-1.0, 0.0, 1.0])
|
||||
|
||||
|
||||
def test_normalize_constant_column_stays_finite():
|
||||
"""常量列 max == min,不能出 nan / inf;固定压到 feature_range 的下界。"""
|
||||
from mcm_toolkit.data import normalize
|
||||
|
||||
out = np.asarray(normalize([[2.0, 1.0], [2.0, 3.0], [2.0, 5.0]]))
|
||||
assert np.allclose(out[:, 0], 0.0)
|
||||
assert bool(np.isfinite(out).all())
|
||||
|
||||
|
||||
def test_normalize_vector_method_gives_unit_columns():
|
||||
from mcm_toolkit.data import normalize
|
||||
|
||||
out = np.asarray(normalize([[3.0, 0.0], [4.0, 0.0]], method="vector"))
|
||||
assert np.allclose(np.linalg.norm(out, axis=0), [1.0, 0.0])
|
||||
|
||||
|
||||
def test_normalize_max_method_scales_by_column_max():
|
||||
from mcm_toolkit.data import normalize
|
||||
|
||||
out = np.asarray(normalize([[1.0, 10.0], [2.0, 20.0]], method="max"))
|
||||
assert np.allclose(out, [[0.5, 0.5], [1.0, 1.0]])
|
||||
|
||||
|
||||
def test_normalize_rejects_bad_method_and_feature_range():
|
||||
from mcm_toolkit.data import normalize
|
||||
|
||||
with pytest.raises(ValueError, match="bogus"):
|
||||
normalize([1.0, 2.0], method="bogus")
|
||||
with pytest.raises(ValueError, match="feature_range"):
|
||||
normalize([1.0, 2.0], feature_range=(1.0, 0.0))
|
||||
with pytest.raises(TypeError, match="feature_range"):
|
||||
normalize([1.0, 2.0], feature_range=(0.0, 1.0, 2.0)) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_standardize_has_zero_mean_unit_std(rng):
|
||||
from mcm_toolkit.data import standardize
|
||||
|
||||
@@ -377,6 +424,106 @@ def test_standardize_has_zero_mean_unit_std(rng):
|
||||
assert abs(out.std() - 1.0) < 1e-9
|
||||
|
||||
|
||||
def test_standardize_is_per_column(rng):
|
||||
"""按列标准化:每列各自均值 0、标准差 1。"""
|
||||
from mcm_toolkit.data import standardize
|
||||
|
||||
x = rng.normal(size=(200, 3)) * [1.0, 10.0, 100.0] + [0.0, 50.0, -3.0]
|
||||
out = np.asarray(standardize(x))
|
||||
assert np.allclose(out.mean(axis=0), 0.0, atol=1e-12)
|
||||
assert np.allclose(out.std(axis=0), 1.0, atol=1e-12)
|
||||
|
||||
|
||||
def test_standardize_honours_ddof():
|
||||
"""ddof 要真的传下去:ddof=0 才得到单位方差;ddof=1 时结果的 std = sqrt((n-1)/n)。
|
||||
|
||||
除以更大的样本标准差,结果自然更小 —— 所以 ddof=1 时 out.std() **小于** 1。
|
||||
"""
|
||||
from mcm_toolkit.data import standardize
|
||||
|
||||
x = np.array([1.0, 2.0, 3.0, 4.0])
|
||||
assert np.allclose(standardize(x, ddof=0).std(), 1.0)
|
||||
assert np.allclose(standardize(x, ddof=1).std(), np.sqrt(3.0 / 4.0))
|
||||
|
||||
|
||||
def test_standardize_constant_column_stays_finite():
|
||||
from mcm_toolkit.data import standardize
|
||||
|
||||
out = np.asarray(standardize([[2.0, 1.0], [2.0, 3.0]]))
|
||||
assert np.allclose(out[:, 0], 0.0)
|
||||
assert bool(np.isfinite(out).all())
|
||||
|
||||
|
||||
def test_standardize_rejects_bad_type():
|
||||
from mcm_toolkit.data import standardize
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
standardize("abc")
|
||||
|
||||
|
||||
def test_log_transform_shifts_to_keep_domain():
|
||||
"""含 0 或负值时自动取 offset = 1 - min(x),保证 log 的定义域。"""
|
||||
from mcm_toolkit.data import log_transform
|
||||
|
||||
x = np.array([-5.0, 0.0, 5.0])
|
||||
out = np.asarray(log_transform(x))
|
||||
assert bool(np.isfinite(out).all())
|
||||
assert np.allclose(out, np.log(x + 6.0))
|
||||
|
||||
|
||||
def test_log_transform_positive_data_needs_no_offset():
|
||||
from mcm_toolkit.data import log_transform
|
||||
|
||||
assert np.allclose(log_transform([1.0, np.e]), [0.0, 1.0])
|
||||
|
||||
|
||||
def test_log_transform_rejects_bad_type():
|
||||
from mcm_toolkit.data import log_transform
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
log_transform("abc")
|
||||
|
||||
|
||||
def test_one_hot_encode_auto_detects_text_columns_quietly():
|
||||
"""pandas 3 起字符串列的 dtype 是 'str':要能选中,且不能走已弃用的 select_dtypes 路径。"""
|
||||
import warnings
|
||||
|
||||
from mcm_toolkit.data import one_hot_encode
|
||||
|
||||
df = pd.DataFrame({"city": ["A", "B", "C"], "v": [1.0, 2.0, 3.0]})
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
out = one_hot_encode(df)
|
||||
|
||||
messages = [str(w.message) for w in caught]
|
||||
assert not messages, f"自动识别不该产生任何告警,实际:{messages}"
|
||||
assert "city_A" in out.columns
|
||||
assert "v" in out.columns
|
||||
|
||||
|
||||
def test_one_hot_encode_explicit_columns_and_drop_first():
|
||||
from mcm_toolkit.data import one_hot_encode
|
||||
|
||||
df = pd.DataFrame({"city": ["A", "B", "C"], "v": [1.0, 2.0, 3.0]})
|
||||
out = one_hot_encode(df, columns=["city"], drop_first=True)
|
||||
assert "city_A" not in out.columns
|
||||
assert "city_B" in out.columns
|
||||
assert "city_C" in out.columns
|
||||
|
||||
|
||||
def test_one_hot_encode_rejects_missing_column_and_non_frame():
|
||||
from mcm_toolkit.data import one_hot_encode
|
||||
|
||||
df = pd.DataFrame({"city": ["A"], "v": [1.0]})
|
||||
with pytest.raises(ValueError, match="nope"):
|
||||
one_hot_encode(df, columns=["nope"])
|
||||
with pytest.raises(TypeError):
|
||||
one_hot_encode([1, 2, 3])
|
||||
|
||||
|
||||
# ------------------------------------------------- 以下为未实现算法(L0/L1 待写)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||
def test_train_test_split_sizes_and_no_overlap(rng):
|
||||
from mcm_toolkit.data import train_test_split
|
||||
|
||||
Reference in new issue
Block a user