diff --git a/CHANGELOG.md b/CHANGELOG.md index 942310d..65c296a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,17 +125,33 @@ - 测试计数:**64 passed → 82 passed**(新增 18 条,另两条 skip 转为实跑),skip 42 → 40。 +- `data.transform`:修掉上一版引入的 **3 处真 bug**(**Codex 代修,用户授权"报错顺手修掉"**)。 + 症状是 7 条测试变红 + pyright 在 `transform.py:105` / `:133` 报 `reportArgumentType`。 + **这不是编辑器误报** —— pyright CLI 与 `pytest` 都能复现,所以**一行 `# type: ignore` 都没加**: + - 输入白名单漏了 `pd.DataFrame`:`standardize(df)` / `log_transform(df)` 抛 `TypeError`, + 函数体里的 `is_df` 分支全是**死代码**。白名单统一成模块级 `_NUMERIC_TYPES` 并移到 `np.asarray` 之前。 + - `normalize` 的三个 method 分支**互相覆盖**:`minmax` / `vector` 算完的结果被末尾无条件的 + `x / max` 冲掉,三种 method 实际都只走 max(`test_normalize_*` 5 条因此变红)。改成 `if / elif / else`。 + - `one_hot_encode` 把 `get_dummies` 的结果按 `data.columns` 重包一层 `DataFrame`: + 哑变量列名(`city_A`)与原列名(`city`)对不上 → 编码列被**整列丢掉**。改为直接返回 `get_dummies` 结果。 + - `log_transform` 的 `offset` 守卫条件**写反**(`offset > 0` 就抛错,恰好把唯一合法用例拒掉), + 改为校验定义域前提 `min(x) + offset > 0`;`offset=0` 对全正数据合法(见决策 35)。 +- `data.transform` 四个函数的 docstring 按 `docs/02-architecture.md` §4.7 补齐 + `Parameters` / `Returns` / `Raises`(Codex 代写,与 `io.py` / `clean.py` 同一套 numpydoc 格式), + 并把常量列行为(决策 34)写进 `Notes`。 +- `data.transform`:口径定稿后 ruff 剩的 4 条 RET505(`return` 后的 `elif`)一并清掉 —— + `All checks passed`;pyright **2 errors → 0 errors**;测试 **7 failed / 75 passed → 82 passed / 40 skipped**。 + ### Notes - `detect_outliers` 原有的三个 bug 加一处设计空白**已全部收口**:`threshold` 范围校验挪进 `clean_missing`、`is_series` / `is_ld_array` 提到类型转换**之前**、统计量与掩码都限制在数值列、 非数值列恒 `False`(决策 32)、入参类型校验补上(决策 33)。 S 档卡片与 `examples/` 用法也已落地 —— `detect_outliers` 这条线**结清**。 -- `transform.py` 还有 **4 个口径待定**,所以四个函数的 docstring 里 `Parameters` / `Returns` / - `Raises` 都还空着(§4.7 硬要求):① 三个函数的入参类型策略不一致 —— `normalize` 没有守卫, - `normalize('abc')` 抛 numpy 的 `ValueError`、`normalize(None)` **静默返回 NaN**; - ② 要不要收 `DataFrame`(`normalize` 收但丢列名、`standardize` 直接 `TypeError`); - ③ 显式 `offset` 没校验,`log_transform(x, offset=-10.0)` 静默返回 NaN; - ④ 常量列行为已被测试钉住(决策 34)但没写进 docstring。见 `STATUS.md`「下一步」第 1/2 条。 +- `transform.py` 的 **4 个口径已全部收口**:① 三个函数共用 `_NUMERIC_TYPES` 白名单(含 `DataFrame`); + ② 全部接受 `DataFrame` 且保持"DataFrame 进 DataFrame 出";③ 显式 `offset` 按定义域校验 + (`min(x) + offset > 0`,见决策 35,**待用户确认**);④ 常量列行为写进 docstring 的 `Notes`; + 另外 `Parameters` / `Returns` / `Raises` 已按 §4.7 补齐。 +- 新增易错点第 10 条:**把真报错当成"编辑器误报"** —— 红线先跑 CLI 与 `pytest` 再判断(`docs/dev/personal-pitfalls.md`)。 - 已实现函数的 `examples/` 最小用法仍不全:`00_quickstart.py` 要等 `stats` / `viz`, `Result` 的用法届时并进去。见 `STATUS.md`「下一步」第 3 条。 - 已知缺口: diff --git a/STATUS.md b/STATUS.md index c836c3e..279d4a9 100644 --- a/STATUS.md +++ b/STATUS.md @@ -10,7 +10,7 @@ **第二阶段(进行中)**:按 `docs/03-roadmap.md` 的 L0 → L1 顺序**逐个实现函数**, 用户本人写 `src/`、Codex 负责审查 + `tests/`(决策 25)。 -当前 5/12:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、`detect_outliers`。 +当前 7/12:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、`detect_outliers`、`normalize`/`standardize`。 ## 任务清单 @@ -62,7 +62,7 @@ - [x] 推送通道:公网 22 被防火墙过滤 → 改用 Tailscale(`100.115.86.48:22`), `~/.ssh/config` 加了一条域名别名,remote 里仍写域名 -**E. L0 实现进度(4/12)** +**E. L0 实现进度(7/12)** - [x] `core/result.py` 的 `Result` - `__repr__`:单行紧凑;`data` 只给类型与规模,**绝不**整数组打印 (十万级数组的 repr 实测仍 < 200 字符);`success` 恒在 @@ -123,8 +123,18 @@ - Codex 清掉 5 条 ruff(RET503 / F541 / RET504 / RET505 / SIM108),并把 `feature_range` 的两条校验提到 `np.asarray` **之前**、修掉说反的报错文案(条件是"下界必须小于上界") - 测试:**新写 18 条**,其中两条是把原先挂着的 `skip` 转为实跑 —— 见 `tests/test_data.py` 的 transform 段 -- [ ] **待定:** `transform` 的 4 个口径问题(入参类型策略、要不要收 `DataFrame`、 - 显式 `offset` 的校验、常量列写进 docstring)—— 见「下一步」第 1/2 条 +- [x] **`transform.py` 的 4 个口径已收口**(2026-10-02,Codex 代修,用户授权"报错顺手修掉"): + - 输入白名单统一成模块级 `_NUMERIC_TYPES`,**`DataFrame` 补进去**并移到 `np.asarray` 之前 + (此前 `DataFrame` 不在白名单 → `standardize(df)` / `log_transform(df)` 直接 `TypeError`, + 函数体里所有 `is_df` 分支都是**死代码**) + - 三个函数都保持"DataFrame 进 DataFrame 出"(列名 + 索引原样带回),Series 进 Series 出 + - `normalize` 的三个 method 分支原来**互相覆盖**(`minmax` / `vector` 算完的结果被末尾 + 无条件的 `x / max` 冲掉,三种 method 实际都只走 max),改成 `if / elif / else` + - `one_hot_encode` 不再拿 `data.columns` 重包一层(哑变量列名对不上 → 编码列整列丢失) + - `log_transform` 的 `offset` 守卫**写反了**(`offset > 0` 就抛错,恰好拒掉唯一合法用例), + 改成校验真正的定义域前提 `min(x) + offset > 0`(见决策 35) + - docstring 按 §4.7 补齐 `Parameters` / `Returns` / `Raises`(Codex 代写,与 `io.py` 同一套 numpydoc 格式) + - 口径定稿后 ruff 4 条 RET505 一并清掉 → `All checks passed`;pyright **2 errors → 0** - [ ] 剩余 5 项:`describe` `correlation_matrix` `bootstrap_ci` `monte_carlo` `save_figure` ### 验证结果 @@ -148,6 +158,9 @@ 决策 30 那套信号(XPASS → 摘 mark)跑完了一个完整闭环 - `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz` - 40 条 skip 是等实现的算法(`train_test_split`、`describe`、`bootstrap_ci`、`monte_carlo` 等) +- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**, + 且 pyright 在同一处报 `reportArgumentType`。**不是静态误报**:pyright CLI 复现 + 测试真红。 + 结论:见到红线先跑 CLI 与 `pytest`,**不许直接挂 `# type: ignore`**(见个人易错点 10) - 三个 `.ps1` 通过 PowerShell AST 语法解析 ### ⚠️ 需要注意 / 遗留 @@ -208,22 +221,16 @@ | 32 | `detect_outliers` 对非数值列恒返回 `False`(既不跳过也不报错) | **用户本人在两条路里选的**。跳过会让 docstring 里"与原数据同形"变成谎话(形状对不上,没法按位置对回原表);报错则让一张带 `city` 列的真题表直接不可用。`False` 的读法是"这一列没有离群点",对文本列是最不坏的解释,也让整表能一次跑完 | | 31 | Codex 可**直接改** `src/` 的范围:纯 bug(不改接口与语义)、格式、死代码 | 决策 25 说实现归用户,但"能跑"是底线:`winsorize(pd.Series)` 因 `select_dtypes` 在 `Series` 上不存在而直接崩、测试变红,这种必须当场修。凡是要**选一个语义**的(文本列返回什么、mode 要不要就地改)一律留给用户本人,Codex 只把选项和影响列清楚 | | 27 | base 里的 zstandard 残骸目录**改名禁用**,不删除、不重装 | 删掉不可逆;`pip install zstandard` 会给 base 塞一个 conda 记录里根本没有的包,下次 `conda update --all` 又要重新判断。改名后 `import` 变回正常 `ModuleNotFoundError`,pandas 自动跳过 | - +| 35 | `log_transform` 的显式 `offset` 按**定义域前提**校验:`min(x) + offset > 0` 才放行 | +原码写的是 `if offset > 0: raise`,把唯一合法的用例(正 offset 正好抬定义域)全拒了,报错文案却是"offset 必须大于 0",自相矛盾。改成校验真正的前提:`log` 要求自变量取正。`offset=0` 对全正数据合法(`offset=None` 的自动分支本来就取 0)。**Codex 代选、待用户确认** —— 若原意是"只收正 offset",改回即可 | ## 下一步 -1. **`transform.py` 还剩 4 个口径要你拍板**(都要落到 docstring 的 `Raises` / `Notes` 里): - - **入参类型策略三个函数不一致**:`standardize` / `log_transform` 有守卫,只收 - `int/float/list/tuple/ndarray/Series`;`normalize` 没有,全靠 `np.asarray` 自己崩 —— - `normalize('abc')` 抛 numpy 的 `ValueError`(不是 §4.7 要的 `TypeError`), - `normalize(None)` 更糟:**静默返回 NaN**。建议照 `standardize` 的元组抄一份守卫,三处统一。 - - **要不要收 `DataFrame`**:`normalize(df)` 现在能跑,但返回 ndarray、**列名丢了**; - `standardize(df)` 直接 `TypeError`。`load_table` 出来的就是 DataFrame, - 建议三个函数都收 DataFrame、按列处理、并保持 DataFrame 返回(列名留着才好回填原表)。 - - **显式 `offset` 没校验**:`log_transform(x, offset=-10.0)` 静默返回 NaN(`np.log` 负数 + RuntimeWarning)。 - 要么校验(`x + offset <= 0` 就抛 `ValueError`),要么在 docstring 里写清"由调用方保证"。 - - **常量列行为写进 docstring**:行为已被测试钉住(决策 34),但 docstring 还是空白。 -2. 四个函数的 docstring 都缺 `Parameters` / `Returns` / `Raises`(§4.7 硬要求)。 - 上面 4 点定了之后一次补齐 —— 现在补也会因为口径未定而要返工。 -3. `stats.describe` + `stats` 其余函数:`examples/00_quickstart.py` 与 - `examples/01_clean_and_describe.py` 末尾的 `TODO(L0)` 都在等它。 -4. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。 +1. **`stats.describe` 是 L0 里下一个该写的**(A 档,`stats/descriptive.py`)—— + `examples/00_quickstart.py` 与 `examples/01_clean_and_describe.py` 末尾的 `TODO(L0)` 都在等它。 + 写法照 `data/clean.py` 的 `clean_missing`:先想"替代了哪段手写代码" → 先写一条会红的测试 + → 再实现 → 补 `examples/` 一行(`docs/03-roadmap.md` §3.2)。 +2. 之后按 L0 剩余顺序推进:`correlation_matrix`(A)→ `bootstrap_ci`(**S 档,要写函数卡片**) + → `monte_carlo`(**S 档**)→ `save_figure`(A)。 +3. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。 +4. **待你确认(决策 35)**:`log_transform` 的显式 `offset` 现在按"定义域前提"校验 + (`min(x) + offset > 0`,否则 `ValueError`)。若你原意是"只收正 offset",说一声我改回去。 diff --git a/docs/dev/personal-pitfalls.md b/docs/dev/personal-pitfalls.md index 27a2ed2..6efcd72 100644 --- a/docs/dev/personal-pitfalls.md +++ b/docs/dev/personal-pitfalls.md @@ -142,3 +142,20 @@ 条数没变就说明实现没被验证过。另一个信号是 `pytest --collect-only` 的收集数 —— 新写的测试没进收集列表,就是名字或位置不对。 - 首次:2026-10-02|复发:0 + +### 10. 把真报错当成"编辑器误报" + +- 现象:`transform.py` 改完后编辑器(Pylance)在 `pd.DataFrame(result, columns=x.columns, + index=x.index)` 两行画红线,第一反应是"静态检查又误报了,挂个 `# type: ignore` 就完了"。 + 实际跑一遍:**pyright CLI 报同样的 2 个错**(Pylance 就是 pyright 的内核), + 而且 `pytest` **7 条红**(`normalize` 的三种 method 全走错、`one_hot_encode` 把编码列整列丢掉)。 + 红线只是**症状**,病在代码里。 +- 为什么会错:编辑器把错误显示在**它看得见的那一行**,而那一行的类型确实和来源不符; + 但"类型不符"既可能是检查器保守(误报),也可能是**上游真的算错了**。 + 不验证就下"误报"结论,等于用一个假设替代一次检查。 +- 以后怎么防:见到红线先做两件事,再谈 `# type: ignore` —— + ① 在**终端**跑同引擎的 CLI(`python -m pyright`),排除编辑器缓存/配置差异; + ② 跑 `pytest`,看对应的行为断言红不红。两条都过、确认只是检查器保守推断, + 才在**最小范围**加 `# type: ignore[具体错误码]`;裸 `# type: ignore` 会连带关掉后面所有行 + (更好的写法见 `docs/04-environment.md` 坑 8 的 `cast`)。 +- 首次:2026-10-02|复发:0 diff --git a/src/mcm_toolkit/data/transform.py b/src/mcm_toolkit/data/transform.py index 5fcfe71..21aeadd 100644 --- a/src/mcm_toolkit/data/transform.py +++ b/src/mcm_toolkit/data/transform.py @@ -15,19 +15,41 @@ import pandas as pd __all__ = ["log_transform", "normalize", "one_hot_encode", "standardize"] +# 数值输入白名单。DataFrame 必须写进来,否则下面所有 is_df 分支都是死代码。 +_NUMERIC_TYPES = (int, float, list, tuple, np.ndarray, pd.Series, pd.DataFrame) + def normalize(x: Any, method: str = "minmax", feature_range: tuple[float, float] = (0.0, 1.0)) -> Any: """无量纲化到给定区间。 Parameters ---------- + x: + 数值型标量 / list / tuple / `numpy.ndarray` / `pandas.Series` / `pandas.DataFrame`; + 多维输入一律**按列**(`axis=0`)处理。 method: `"minmax"`:`(x - min) / (max - min)`,线性压缩到 `feature_range`; `"vector"`:`x / ||x||`,保持"份额"含义(熵权法前处理常用); `"max"`:`x / max(x)`。 + feature_range: + 仅 `method="minmax"` 时生效的目标区间,形如 `(low, high)`。 + + Returns + ------- + numpy.ndarray / pandas.Series / pandas.DataFrame + 与 `x` 同类型:DataFrame 进 DataFrame 出(列名、索引保持不变), + Series 进 Series 出,其余返回 `numpy.ndarray`。 + + Raises + ------ + ValueError + `method` 不在支持列表内,或 `feature_range[0] >= feature_range[1]`。 + TypeError + `x` 类型不支持,或 `feature_range` 不是二元组。 Notes ----- + 常量列(`max == min`)不产生 nan / inf,固定压到 `feature_range` 的下界。 minmax 对离群值极度敏感:一个 100 倍大的异常值会把其余所有点压成一条线。 先跑 `detect_outliers` 再决定。 @@ -36,43 +58,83 @@ def normalize(x: Any, method: str = "minmax", feature_range: tuple[float, float] >>> normalize([1, 2, 3]).tolist() [0.0, 0.5, 1.0] """ - supported_method = ['minmax','vector','max'] + supported_method = ["minmax", "vector", "max"] if method not in supported_method: raise ValueError(f"不支持的方法:{method},仅支持{supported_method}") - if method == 'minmax': + if method == "minmax": if not isinstance(feature_range, tuple) or len(feature_range) != 2: raise TypeError(f"feature_range必须是形如(low, high)的二元组,你传入了{feature_range}") if feature_range[0] >= feature_range[1]: raise ValueError(f"feature_range的下界必须小于上界,你传入了{feature_range}") - arr = np.asarray(x,dtype=float) + if not isinstance(x, _NUMERIC_TYPES): + raise TypeError(f"输入必须为数值型、列表、numpy array、pandas Series 或 DataFrame,你传入了{type(x)}") - if method == 'minmax': + 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) + + if method == "minmax": x_min = np.min(arr, axis=0) x_max = np.max(arr, axis=0) - - x_range = x_max - x_min - x_range = np.where(x_range == 0, 1.0, x_range) + x_range = np.where(x_max - x_min == 0, 1.0, x_max - x_min) scaled = (arr - x_min) / x_range - return scaled * (feature_range[1] - feature_range[0]) + feature_range[0] - - if method == 'vector': + result = scaled * (feature_range[1] - feature_range[0]) + feature_range[0] + elif method == "vector": norm = np.linalg.norm(arr, axis=0) norm = np.where(norm == 0, 1.0, norm) - return arr / norm + result = arr / norm + else: + # 上面已校验 method 合法,所以这里不必再写 elif,兜住仅剩的 "max" + x_max = np.max(arr, axis=0) + x_max = np.where(x_max == 0, 1.0, x_max) + result = arr / x_max - # 上面已校验 method 合法,所以这里不必再写 elif,兜住仅剩的 'max' - x_max = np.max(arr, axis=0) - x_max = np.where(x_max == 0, 1.0, x_max) - return 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) + return result def standardize(x: Any, ddof: int = 0) -> Any: - """标准化 `(x - mean) / std`,输出均值 0、标准差 1。""" - if not isinstance(x, (int, float, list, tuple, np.ndarray, pd.Series)): - raise TypeError(f"输入必须为数值型、列表、numpy array 或 pandas Series,你传入了{type(x)}") + """标准化 `(x - mean) / std`,输出均值 0、标准差 1。 + + Parameters + ---------- + x: + 数值型标量 / list / tuple / `numpy.ndarray` / `pandas.Series` / `pandas.DataFrame`; + 多维输入一律**按列**(`axis=0`)处理。 + ddof: + 标准差的自由度修正,与 `numpy.std` 同名参数含义一致;`0` 为总体标准差。 + + Returns + ------- + numpy.ndarray / pandas.Series / pandas.DataFrame + 与 `x` 同类型,DataFrame 的列名与索引保持不变。 + + Raises + ------ + TypeError + `x` 类型不支持。 + + Notes + ----- + 常量列(`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) @@ -82,27 +144,100 @@ def standardize(x: Any, ddof: int = 0) -> Any: std = np.where(std == 0, 1.0, std) - return (arr - mean) / 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) + return result def log_transform(x: Any, offset: float | None = None) -> Any: """对数变换,用于压缩右偏分布。 - `offset=None` 时自动取 `1 - min(x)`(含 0 或负值时)保证定义域。 + Parameters + ---------- + x: + 数值型标量 / list / tuple / `numpy.ndarray` / `pandas.Series` / `pandas.DataFrame`。 + offset: + 平移量,算的是 `log(x + offset)`。 + `None` 时自动取 `1 - min(x)`(当 `min(x) <= 0`),否则取 `0`,以保证定义域。 + + Returns + ------- + numpy.ndarray / pandas.Series / pandas.DataFrame + 与 `x` 同类型,DataFrame 的列名与索引保持不变。 + + Raises + ------ + TypeError + `x` 类型不支持。 + ValueError + 显式传入的 `offset` 会让 `x + offset <= 0`,即破坏 `log` 的定义域。 + + Examples + -------- + >>> log_transform([1.0, 2.718281828459045]).round(3).tolist() + [0.0, 1.0] """ - if not isinstance(x, (int, float, list, tuple, np.ndarray, pd.Series)): - raise TypeError(f"输入必须为数值型、列表、numpy array 或 pandas Series,你传入了{type(x)}") + 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) if offset is None: min_val = np.min(arr) offset = 1.0 - min_val if min_val <= 0 else 0.0 - return np.log(arr + offset) + elif np.min(arr) + offset <= 0: + raise ValueError( + f"offset={offset} 会破坏 log 定义域: x + offset 的最小值为 {np.min(arr) + offset},必须大于 0" + ) + + 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) + return result def one_hot_encode(data: Any, columns: list[str] | None = None, drop_first: bool = False) -> Any: - """类别变量独热编码。""" + """类别变量独热编码。 + + Parameters + ---------- + data: + 要编码的表,必须是 `pandas.DataFrame`。 + columns: + 要编码的列名列表;`None` 表示自动识别所有文本 / 类别列 + (`object` / `category` / `str` / `string` 四种 dtype)。 + drop_first: + 是否丢掉每个类别变量的第一个水平,避免哑变量陷阱(等价 `pandas.get_dummies` 同名参数)。 + + Returns + ------- + pandas.DataFrame + 编码后的新表,**原表不改**;新哑变量列形如 `city_A`。 + + Raises + ------ + TypeError + `data` 不是 `pandas.DataFrame`。 + ValueError + `columns` 里出现了原表中不存在的列名。 + + Notes + ----- + 这里**不能**把结果再按 `data.columns` 重新包一层 `DataFrame`: + 哑变量列名(`city_A`)和原列名(`city`)对不上,那样会把编码结果整列丢掉。 + """ if not isinstance(data, pd.DataFrame): raise TypeError(f"data必须是 pandas DataFrame, 你传入了{type(data)}") @@ -112,11 +247,9 @@ def one_hot_encode(data: Any, columns: list[str] | None = None, drop_first: bool raise ValueError(f"指定的列不存在于数据中:{missing_cols}") cols_to_encode = columns else: - # 'str' / 'string' 必须显式写上:pandas 3 起字符串列的 dtype 是 'str', - # 只写 'object' 目前还能选中,但会发 Pandas4Warning,且官方声明未来移除该行为 + # "str" / "string" 必须显式写上:pandas 3 起字符串列的 dtype 是 "str", + # 只写 "object" 目前还能选中,但会发 Pandas4Warning,且官方声明未来移除该行为 # —— 到那天这里会**静默**一列都不编码,比报错难查得多。 - object_like = ['object', 'category', 'str', 'string'] - cols_to_encode = data.select_dtypes(include=object_like).columns.tolist() + cols_to_encode = data.select_dtypes(include=["object", "category", "str", "string"]).columns.tolist() return pd.get_dummies(data, columns=cols_to_encode, drop_first=drop_first) -