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
+229
-56
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user