hscredit.core.encoders.cardinality_encoder 源代码

"""Cardinality Reducer (高基数降维编码器).

将高基数类别特征合并为低基数,保留频次最高的 top-N 类别,
其余合并为统一的 other 标签。支持缺失值和特殊值单独成类,支持逆编码。
"""

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

from .base import BaseEncoder


[文档] class CardinalityEncoder(BaseEncoder): """高基数降维编码器. 将类别型特征从高基数(如100个枚举值)降维为低基数(如最多10类)。 按训练集中各类别的频次排序,保留前 ``max_categories - 1`` 个类别, 其余合并为统一的 ``other_label`` 标签。 缺失值和特殊值不占 ``max_categories`` 配额,始终独立成类。 **参数** :param cols: 需要编码的列名列表。如果为None,则自动识别所有类别型列 :param max_categories: 每列最大类别数(含 other 标签,不含缺失及特殊值),默认为10 :param other_label: 合并后的标签名称,默认为 'other',支持自定义 :param special_values: 需要独立成类的特殊值列表(如 [-999, 'unknown']),默认为None :param handle_unknown: 处理未知类别的方式,默认为 'other' - 'other': 映射为 other_label - 'error': 抛出错误 - 'return_nan': 返回 NaN :param handle_missing: 处理缺失值的方式,默认为 'value' - 'value': 缺失值保持原样(NaN),独立成类 - 'error': 抛出错误 - 'return_nan': 返回 NaN :param drop_invariant: 是否删除方差为0的列,默认为False :param return_df: 是否返回DataFrame,默认为True **属性** - mapping\_: 编码映射字典,格式为 ``{col: {原始类别: 编码后类别}}`` - top_categories\_: 各列保留的高频类别列表,格式为 ``{col: [类别列表]}`` - category_counts\_: 各列训练集类别频次统计 **参考样例** >>> from hscredit.core.encoders import CardinalityEncoder >>> encoder = CardinalityEncoder(cols=['city'], max_categories=10) >>> X_encoded = encoder.fit_transform(X) >>> >>> # 自定义标签 >>> encoder = CardinalityEncoder( ... cols=['city'], max_categories=5, other_label='其他城市' ... ) >>> X_encoded = encoder.fit_transform(X) >>> >>> # 特殊值独立成类 >>> encoder = CardinalityEncoder( ... cols=['score_level'], ... max_categories=8, ... special_values=[-999, 'missing'], ... ) >>> X_encoded = encoder.fit_transform(X) >>> >>> # 逆编码 >>> X_original = encoder.inverse_transform(X_encoded) **注意** 本编码器只做"高基数→低基数"的类别合并(频次 top-N 保留、其余归入 ``other_label``), 输出仍为类别值,通常作为 WOE/Target/OneHot 等编码的前置步骤,用于抑制长尾稀有类别带来的 过拟合与不稳定。合并依据为训练集频次,故为无监督步骤。 **引用** 高基数类别的稀有类别合并(rare-category grouping)是类别特征工程的常用预处理,参见 Micci-Barreca, D. (2001). *A preprocessing scheme for high-cardinality categorical attributes.* ACM SIGKDD Explorations. https://doi.org/10.1145/507533.507538 """ # top_categories_ 供 get_top_categories/get_summary 使用,随映射一并序列化 _EXTRA_STATE_ATTRS = ["top_categories_"] def __init__( self, cols: Optional[List[str]] = None, max_categories: int = 10, other_label: Any = "other", special_values: Optional[List[Any]] = None, handle_unknown: str = "other", handle_missing: str = "value", drop_invariant: bool = False, return_df: bool = True, target: Optional[str] = None, n_jobs: Optional[Union[int, float]] = -1, parallel_backend: Optional[str] = None, parallel_config: Optional[Dict[str, Any]] = None, ): """初始化高基数降维编码器。 :param cols: 需要编码的列名列表 :param max_categories: 每列最大类别数(含 other,不含缺失及特殊值),默认为10 :param other_label: 合并后的标签名称,默认为 'other' :param special_values: 需要独立成类的特殊值列表,默认为None :param handle_unknown: 处理未知类别的方式,默认为 'other' :param handle_missing: 处理缺失值的方式,默认为 'value' :param drop_invariant: 是否删除方差为0的列,默认为False :param return_df: 是否返回DataFrame,默认为True :param target: scorecardpipeline风格的目标列名。本编码器不使用此参数,仅为API一致性保留 """ 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.max_categories = max_categories self.other_label = other_label self.special_values = special_values self.top_categories_: Dict[str, list] = {} self.category_counts_: Dict[str, pd.Series] = {} self.special_counts_: Dict[str, Dict[Any, int]] = {} def _fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): """拟合高基数降维编码器。 :param X: 输入数据,shape (n_samples, n_features) :param y: 目标变量(可选),本编码器不需要 """ self._fit_columns( X, y, state_attrs=("mapping_", "top_categories_", "category_counts_", "special_counts_"), ) def _fit_column(self, column, values, y=None): special_set = set(self.special_values or []) series = values # 排除缺失值和特殊值后统计频次 mask_special = series.isin(special_set) if special_set else pd.Series(False, index=series.index) mask_valid = series.notna() & ~mask_special counts = series[mask_valid].value_counts() # 记录实际出现的特殊值及其频次 special_counts = {sv: int((series == sv).sum()) for sv in special_set if (series == sv).any()} # 保留前 max_categories - 1 个(给 other 留一个位置) keep_n = max(self.max_categories - 1, 0) top_cats = counts.head(keep_n).index.tolist() # 构建映射:top 类别 → 自身,其余 → other_label mapping = {} for cat in counts.index: mapping[cat] = cat if cat in top_cats else self.other_label # 特殊值 → 自身(不受 max_categories 限制) for sv in special_set: mapping[sv] = sv # 未知类别处理 if self.handle_unknown == "other": mapping["__UNKNOWN__"] = self.other_label elif self.handle_unknown == "return_nan": mapping["__UNKNOWN__"] = np.nan return { "mapping_": mapping, "top_categories_": top_cats, "category_counts_": counts, "special_counts_": special_counts, } def _transform(self, X: pd.DataFrame, y: Optional[pd.Series] = None) -> pd.DataFrame: """转换数据。 :param X: 输入数据,shape (n_samples, n_features) :param y: 目标变量(可选),本编码器不需要 :return: 编码后的数据 """ return self._transform_columns(X, y) def _transform_column(self, column, values, y=None, context=None): mapping = self.mapping_[column] original = values # 标记缺失 is_na = original.isna() # 映射:先用 map,未命中的为 NaN mapped = original.map(mapping) # 对未命中且非缺失的值,按 handle_unknown 策略处理 unmapped = mapped.isna() & ~is_na if unmapped.any(): if self.handle_unknown == "other": mapped[unmapped] = self.other_label elif self.handle_unknown == "error": bad = original[unmapped].unique().tolist() raise ValueError(f"列 '{column}' 包含未知类别: {bad}") # 'return_nan' 不需要额外处理,已经是 NaN # 缺失值保持 if self.handle_missing == "error" and is_na.any(): raise ValueError(f"列 '{column}' 包含缺失值") return mapped
[文档] def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame: """逆编码。 将编码后的数据还原为原始类别值。 **注意**: 被合并为 other_label 的类别无法恢复为原始值,逆编码后仍为 other_label。 :param X: 编码后的数据 :return: 逆编码后的数据 """ X = self._check_input(X).copy() for col in self.cols_: if col not in self.mapping_ or col not in X.columns: continue mapping = self.mapping_[col] # 构建逆映射:只对保留类别和特殊值有效 inverse_mapping = {} for orig, encoded in mapping.items(): if orig == "__UNKNOWN__": continue # 保留类别和特殊值映射回自身;other_label 保持不变 if encoded != self.other_label: inverse_mapping[encoded] = orig X[col] = X[col].map(lambda v: inverse_mapping.get(v, v)) return X
[文档] def get_top_categories(self, col: Optional[str] = None) -> Union[list, Dict[str, list]]: """获取各列保留的高频类别。 :param col: 列名,如果为None则返回所有列 :return: 类别列表或字典 """ if col is not None: return self.top_categories_.get(col, []) return self.top_categories_
[文档] def get_summary(self) -> pd.DataFrame: """获取各列的降维摘要。 :return: DataFrame,含列名/原始类别数/保留类别数/合并类别数/特殊值数 """ rows = [] for col in self.cols_: counts = self.category_counts_.get(col, pd.Series(dtype=int)) top_cats = self.top_categories_.get(col, []) n_special = len(self.special_counts_.get(col, {})) rows.append( { "列名": col, "原始类别数": len(counts), "保留类别数": len(top_cats), "合并为other": max(len(counts) - len(top_cats), 0), "特殊值数": n_special, "other标签": self.other_label, } ) return pd.DataFrame(rows)