"""输入数据处理工具函数.
提供统一的输入处理函数,用于处理两种API风格的输入数据:
1. sklearn风格: fit(X, y)
2. scorecardpipeline风格: fit(df) - target列在df中
"""
from typing import Union, List, Optional, Tuple, Any
import warnings
import numpy as np
import pandas as pd
from ..exceptions import FeatureNotFoundError, InputTypeError, InputValidationError
ArrayLike = Union[np.ndarray, pd.DataFrame, pd.Series, List]
[文档]
def convert_to_dataframe(
X: ArrayLike,
columns: Optional[List[str]] = None
) -> pd.DataFrame:
"""将输入转换为 DataFrame。
支持 ``DataFrame`` / numpy 数组(1 维或 2 维)/ ``Series`` / ``list`` / ``tuple``。
1 维输入转为单列 DataFrame;DataFrame 与 Series 会复制后返回。
:param X: 输入数据,不能为 None
:param columns: 列名列表,仅当 X 不是 DataFrame 时使用;为 None 时自动生成
``feature_0``、``feature_1`` …
:return: 转换后的 ``DataFrame``
:raises InputValidationError: X 为 None、维度 >2,或列名数量与列数不匹配时
:raises InputTypeError: 输入类型不受支持时
**参考样例**
>>> df = convert_to_dataframe(np.array([[1, 2], [3, 4]]), columns=['a', 'b'])
>>> df = convert_to_dataframe([[1, 2], [3, 4]])
"""
if X is None:
raise InputValidationError("输入数据X不能为None")
# 已经是DataFrame
if isinstance(X, pd.DataFrame):
return X.copy()
# numpy数组
if isinstance(X, np.ndarray):
if X.ndim == 1:
# 一维数组转为单列DataFrame
X = X.reshape(-1, 1)
elif X.ndim > 2:
raise InputValidationError(f"不支持{X.ndim}维数组,只支持1维或2维数组")
# 生成默认列名或验证列名
if columns is None:
columns = [f'feature_{i}' for i in range(X.shape[1])]
elif len(columns) != X.shape[1]:
raise InputValidationError(
f"列名数量({len(columns)})与数据列数({X.shape[1]})不匹配"
)
return pd.DataFrame(X, columns=columns)
# pandas Series
if isinstance(X, pd.Series):
return X.to_frame()
# list或其他可迭代对象
if isinstance(X, (list, tuple)):
arr = np.array(X)
return convert_to_dataframe(arr, columns=columns)
raise InputTypeError(
f"不支持的输入类型: {type(X).__name__},"
"只支持DataFrame, numpy数组, list, tuple或Series"
)
def _convert_to_series(
y: Union[np.ndarray, pd.Series, List],
name: str = 'target'
) -> pd.Series:
"""将y转换为pandas Series.
Args:
y: 目标变量
name: Series的名称
Returns:
转换后的Series
Raises:
TypeError: 当输入类型不支持时
"""
if isinstance(y, pd.Series):
return y.copy()
if isinstance(y, np.ndarray):
if y.ndim > 1:
# 处理二维数组(如(n, 1))
y = y.ravel()
return pd.Series(y, name=name)
if isinstance(y, (list, tuple)):
return pd.Series(y, name=name)
raise InputTypeError(
f"不支持的y类型: {type(y).__name__},"
"只支持numpy数组, list, tuple或Series"
)
def _validate_data(
X: pd.DataFrame,
y: pd.Series
) -> None:
"""验证X和y的数据有效性.
Args:
X: 特征DataFrame
y: 目标Series
Raises:
ValueError: 当数据验证失败时
"""
# 检查空数据
if X.empty:
raise InputValidationError("特征数据X为空")
if y.empty:
raise InputValidationError("目标变量y为空")
# 检查索引一致性
if not X.index.equals(y.index):
warnings.warn(
"X和y的索引不一致,将使用X的索引",
UserWarning
)
y.index = X.index
# 检查目标变量是否全为缺失值
if y.isna().all():
raise InputValidationError("目标变量y全部为缺失值")
# 检查特征是否全为缺失值
if X.isna().all().all():
raise InputValidationError("特征数据X全部为缺失值")
# 检查目标变量的类型(二分类问题的常见检查)
unique_values = y.dropna().unique()
if len(unique_values) < 2:
warnings.warn(
f"目标变量y只有{len(unique_values)}个唯一值: {unique_values},"
"这可能不是分类问题",
UserWarning
)
[文档]
def check_array_1d(
arr: ArrayLike,
name: str = 'array'
) -> pd.Series:
"""检查并转换为一维 ``Series``。
:param arr: 输入数组,``ndarray`` / ``Series`` / ``list`` / ``tuple``;
二维数组会被展平为一维
:param name: 数组名称,用于生成错误信息与 Series 名称,默认 ``'array'``
:return: 一维 ``Series``
:raises InputValidationError: 数组为空时
:raises InputTypeError: 类型不受支持时
**参考样例**
>>> s = check_array_1d([0, 1, 1, 0], name='y')
"""
series = _convert_to_series(arr, name=name)
if series.empty:
raise InputValidationError(f"{name}不能为空")
return series
[文档]
def get_feature_dtypes(
X: pd.DataFrame
) -> dict:
"""获取特征的数据类型分类(数值型 / 类别型)。
:param X: 特征 ``DataFrame``
:return: 字典,含三个键:
- ``'numeric'``:数值型特征名列表(int/uint/float 各精度)
- ``'categorical'``:非数值型特征名列表(object、category、bool 等)
- ``'all'``:全部特征名列表
**参考样例**
>>> dtypes = get_feature_dtypes(df)
>>> numeric_features = dtypes['numeric']
>>> categorical_features = dtypes['categorical']
"""
numeric_dtypes = ['int8', 'int16', 'int32', 'int64',
'uint8', 'uint16', 'uint32', 'uint64',
'float16', 'float32', 'float64']
numeric_features = []
categorical_features = []
for col in X.columns:
if X[col].dtype.name in numeric_dtypes:
numeric_features.append(col)
else:
categorical_features.append(col)
return {
'numeric': numeric_features,
'categorical': categorical_features,
'all': list(X.columns)
}
[文档]
def check_missing_values(
X: pd.DataFrame,
y: Optional[pd.Series] = None,
raise_error: bool = False
) -> dict:
"""检查缺失值情况。
:param X: 特征 ``DataFrame``
:param y: 目标 ``Series``(可选),提供时一并统计其缺失情况
:param raise_error: 存在缺失值时是否抛出异常,默认 False(仅统计不报错)
:return: 缺失值统计字典,含键 ``X_missing``(总缺失数)、
``X_missing_by_col``(各列缺失数)、``X_missing_ratio``(整体缺失率);
当 ``y`` 非 None 时另含 ``y_missing``、``y_missing_ratio``
:raises InputValidationError: ``raise_error=True`` 且 X 或 y 存在缺失值时
**参考样例**
>>> stats = check_missing_values(X, y)
>>> print(stats['X_missing_ratio'])
>>> check_missing_values(X, raise_error=True) # 有缺失则报错
"""
result = {
'X_missing': X.isna().sum().sum(),
'X_missing_by_col': X.isna().sum().to_dict(),
'X_missing_ratio': X.isna().sum().sum() / (X.shape[0] * X.shape[1]),
}
if y is not None:
result['y_missing'] = y.isna().sum()
result['y_missing_ratio'] = y.isna().sum() / len(y)
if raise_error and result['X_missing'] > 0:
raise InputValidationError(f"特征数据X存在{result['X_missing']}个缺失值")
if raise_error and y is not None and result['y_missing'] > 0:
raise InputValidationError(f"目标变量y存在{result['y_missing']}个缺失值")
return result