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