hscredit.core.encoders.catboost_encoder 源代码

"""CatBoost Encoder.

基于CatBoost算法的有序目标编码器,使用排序来防止目标泄漏。
"""

from typing import Optional, List, Dict, Union, Any
import numpy as np
import pandas as pd

from .base import BaseEncoder


[文档] class CatBoostEncoder(BaseEncoder): """CatBoost编码器. 使用有序目标统计(Ordered Target Statistics)方法, 通过随机排序和累积统计来防止过拟合和目标泄漏。 **参数** :param cols: 需要编码的列名列表。如果为None,则自动识别所有列(支持类别型和数值型) :param sigma: 添加的高斯噪声标准差,默认为None :param handle_unknown: 处理未知类别的方式,默认为'value' :param handle_missing: 处理缺失值的方式,默认为'value' :param drop_invariant: 是否删除方差为0的列,默认为False :param return_df: 是否返回DataFrame,默认为True :param random_state: 随机种子,用于可复现性,默认为None **属性** - mapping_: 目标编码映射字典,格式为 {col: {category: encoded_value}} - global_mean_: 全局目标均值 **参考样例** >>> from hscredit.core.encoders import CatBoostEncoder >>> encoder = CatBoostEncoder(cols=['category']) >>> X_encoded = encoder.fit_transform(X, y) >>> >>> # 添加噪声 >>> encoder = CatBoostEncoder(cols=['category'], sigma=0.05, random_state=42) >>> X_encoded = encoder.fit_transform(X, y) **注意** 与普通目标编码相比,本编码器对样本随机排序后只用"当前样本之前"的目标累积统计来编码 (ordered target statistics),从而显著降低目标泄漏;``random_state`` 决定排序, 影响结果可复现性。 **引用** Prokhorenkova, L. et al. (2018). *CatBoost: unbiased boosting with categorical features.* NeurIPS 2018. https://arxiv.org/abs/1706.09516 """ # global_mean_ 是 transform 时未知/缺失类别的填充值,须随映射一并序列化 _EXTRA_STATE_ATTRS = ["global_mean_"] def _get_category_cols(self, X: pd.DataFrame) -> List[str]: """自动识别需要编码的列。 CatBoostEncoder支持数值型和类别型列,因此返回所有列。 :param X: 输入数据 :return: 列名列表 """ return X.columns.tolist() def __init__( self, cols: Optional[List[str]] = None, sigma: Optional[float] = None, handle_unknown: str = "value", handle_missing: str = "value", drop_invariant: bool = False, return_df: bool = True, random_state: Optional[int] = None, target: Optional[str] = None, n_jobs: Optional[Union[int, float]] = -1, parallel_backend: Optional[str] = None, parallel_config: Optional[Dict[str, Any]] = None, ): """初始化CatBoost编码器。 :param cols: 需要编码的列名列表 :param sigma: 添加的高斯噪声标准差,默认为None :param handle_unknown: 处理未知类别的方式,默认为'value' :param handle_missing: 处理缺失值的方式,默认为'value' :param drop_invariant: 是否删除方差为0的列,默认为False :param return_df: 是否返回DataFrame,默认为True :param random_state: 随机种子,默认为None :param target: scorecardpipeline风格的目标列名。如果提供,fit时从X中提取该列作为y """ super().__init__( cols=cols, drop_invariant=drop_invariant, return_df=return_df, handle_unknown=handle_unknown, handle_missing=handle_missing, target=target, n_jobs=n_jobs, parallel_backend=parallel_backend, parallel_config=parallel_config, ) self.sigma = sigma self.random_state = random_state self.global_mean_: float = 0.0 def _fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): """拟合CatBoost编码器。 :param X: 输入数据,shape (n_samples, n_features) :param y: 目标变量 :raises ValueError: 当y为空时抛出 """ if y is None: raise ValueError("CatBoostEncoder是有监督编码器,必须提供目标变量y") y = pd.Series(y, name="target") global_mean = y.mean() self._fit_columns(X, y, shared_state={"global_mean_": global_mean}) self.global_mean_ = global_mean def _fit_column(self, column, values, y=None): df_temp = pd.DataFrame({"feature": values, "target": y.values}) category_stats = df_temp.groupby("feature")["target"].agg(["mean", "count"]) mapping = category_stats["mean"].to_dict() if self.handle_missing == "value": mapping[np.nan] = self.global_mean_ elif self.handle_missing == "return_nan": mapping[np.nan] = np.nan if self.handle_unknown == "value": mapping["__UNKNOWN__"] = self.global_mean_ elif self.handle_unknown == "return_nan": mapping["__UNKNOWN__"] = np.nan return {"mapping_": mapping} def _transform(self, X: pd.DataFrame, y: Optional[pd.Series] = None) -> pd.DataFrame: """转换数据。 :param X: 输入数据,shape (n_samples, n_features) :param y: 目标变量(可选),如果提供则使用有序统计 :return: 编码后的数据 """ contexts = {} if y is not None: rng = np.random.RandomState(self.random_state) for column in self.cols_: order = rng.permutation(len(X)) noise = rng.normal(0, self.sigma, len(X)) if self.sigma is not None else None contexts[column] = (order, noise) return self._transform_columns(X, y, contexts=contexts) def _transform_column(self, column, values, y=None, context=None): mapping = self.mapping_[column] if y is not None: order, noise = context result = self._transform_ordered(values, y, mapping, random_order=order) else: noise = None result = values.map(mapping) if self.handle_unknown == "value": result = result.fillna(self.global_mean_) elif self.handle_unknown == "error" and result.isna().any(): raise ValueError(f"列'{column}'包含未知类别") if noise is not None: result = result * (1 + noise) return result def _transform_ordered( self, x: pd.Series, y: pd.Series, mapping: Dict, rng: Optional[np.random.RandomState] = None, random_order: Optional[np.ndarray] = None, ) -> pd.Series: """使用有序统计进行转换(防止目标泄漏)。 :param x: 特征列 :param y: 目标变量 :param mapping: 编码映射 :param rng: 局部随机数发生器,None 时按 random_state 新建 :return: 编码后的序列 """ if not isinstance(y, pd.Series): y = pd.Series(y, index=x.index) n = len(x) if random_order is None: if rng is None: rng = np.random.RandomState(self.random_state) random_order = rng.permutation(n) result = pd.Series(index=x.index, dtype=float) result[:] = self.global_mean_ category_sums = {} category_counts = {} for idx in random_order: category = x.iloc[idx] if pd.isna(category): result.iloc[idx] = mapping.get(np.nan, self.global_mean_) continue if category in category_counts and category_counts[category] > 0: prior = self.global_mean_ posterior = category_sums[category] / category_counts[category] count = category_counts[category] a = 1.0 result.iloc[idx] = (count * posterior + a * prior) / (count + a) else: result.iloc[idx] = self.global_mean_ if category not in category_sums: category_sums[category] = 0 category_counts[category] = 0 category_sums[category] += y.iloc[idx] category_counts[category] += 1 return result