"""特征描述统计工具.
提供特征描述统计功能。
"""
from typing import Any, Dict, List, Optional, Union
import numpy as np
import pandas as pd
from .parallel import ParallelWorkload, parallel_execute
def _group_feature_describe_worker(task):
"""计算一个分组内的单字段统计,兼容 object/category 非向量化路径。"""
group_key, feature, series, kwargs = task
result = feature_describe(series, **kwargs)
result.index = pd.MultiIndex.from_product([[feature], result.index])
return group_key, feature, result
[文档]
def feature_describe(data: pd.DataFrame, feature: Optional[str] = None, percentiles: Optional[List[float]] = None, missing=None, cardinality: Optional[int] = None) -> pd.Series:
"""特征描述统计(数值型与类别型自动区分)。
对数值型特征输出 样本数/非空数/查得率/最小值/平均值/各分位数/最大值;
对类别型特征(或唯一值数 ≤ ``cardinality``)输出 样本数/非空数/查得率
及各取值占比。
:param data: 输入数据,``DataFrame``(需配合 ``feature`` 指定列)或单列 ``Series``
:param feature: 特征列名,仅当 ``data`` 为 DataFrame 时需要;
为 None 时把 ``data`` 整体当作 Series 处理
:param percentiles: 分位数列表(0~1 之间),默认
``[0.01, 0.02, 0.03, 0.05, 0.1, 0.2, ..., 0.95, 0.97, 0.98, 0.99]``
:param missing: 缺失值标记(标量或列表),统计前会被替换为 ``np.nan``;默认 None 不替换
:param cardinality: 基数阈值(正整数),唯一值数 ≤ 该值时强制按类别型统计;
默认 None 表示仅按 dtype 判断
:return: 描述统计结果 ``Series``(``name`` 为特征名)
:raises ValueError: ``feature`` 不在 ``data`` 列中,或 ``cardinality`` < 1 时
**参考样例**
>>> feature_describe(df, feature='age') # 指定列
>>> feature_describe(df['age']) # 直接传 Series
>>> feature_describe(df, feature='city', cardinality=20) # 强制按类别统计
>>> feature_describe(df, feature='income', missing=-999) # -999 视为缺失
"""
if feature and feature not in data.columns:
raise ValueError(f"特征 {feature} 不在数据列中")
if cardinality and cardinality < 1:
raise ValueError("cardinality 必须大于 1")
if percentiles is None:
percentiles = [0.01, 0.02, 0.03, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.97, 0.98, 0.99]
if feature:
series = data[feature]
else:
series = data.copy()
if missing:
series = series.replace(missing, np.nan)
# 类别型特征处理
if (cardinality and series.nunique() <= cardinality) or not pd.api.types.is_numeric_dtype(series):
describe = {
"样本数": len(series),
"非空数": len(series) - series.isnull().sum(),
"查得率": 1 - series.isnull().mean(),
}
value_counts = series.replace(np.nan, "缺失值").value_counts(dropna=False)
describe.update((value_counts / len(series)).to_dict())
return pd.Series(describe, name=feature)
else:
# 数值型特征处理
describe = {
"样本数": len(series),
"非空数": len(series) - series.isnull().sum(),
"查得率": 1 - series.isnull().mean(),
"最小值": series.min(),
"平均值": series.mean(),
"最大值": series.max(),
}
quantile = series.quantile(percentiles)
quantile.index = [f"{int(i * 100)}%" for i in percentiles]
describe.update(quantile.to_dict())
# 按固定顺序排列
order = ["样本数", "非空数", "查得率", "最小值", "平均值"] + [f"{int(i * 100)}%" for i in percentiles] + ["最大值"]
return pd.Series(describe, name=feature).reindex(order)
[文档]
def groupby_feature_describe(data: pd.DataFrame, by=None, n_jobs: int = -1, parallel_backend: Optional[str] = None, parallel_config: Optional[Dict[str, Any]] = None, **kwargs) -> pd.DataFrame:
"""按分组进行特征描述统计。
:param data: 数据DataFrame
:param by: 分组字段或字段列表
:param n_jobs: 并行任务数,-1 根据数据规模自动选择
:param parallel_backend: joblib 后端;默认使用避免复制字符串列的线程后端
:param parallel_config: joblib 扩展配置
:param kwargs: 传递给feature_describe的其他参数
:return: 描述统计结果DataFrame
**参考样例**
>>> groupby_feature_describe(df, by='gender')
>>> groupby_feature_describe(df, by=['gender', 'age_group'])
"""
if not isinstance(by, (tuple, list, np.ndarray)):
by = [by]
feature_columns = [column for column in data.columns if column not in by]
grouped = data.groupby(by=by)
task_count = grouped.ngroups * len(feature_columns)
def iter_tasks():
for group_key, group in grouped:
if isinstance(group_key, tuple) and len(group_key) == 1:
group_key = group_key[0]
for feature in feature_columns:
yield group_key, feature, group[feature], kwargs
results = parallel_execute(
_group_feature_describe_worker,
iter_tasks(),
n_jobs=n_jobs,
parallel_backend=parallel_backend,
parallel_config=parallel_config,
default_backend="threading",
workload=ParallelWorkload(
task_count=task_count,
rows=len(data),
columns=len(feature_columns),
data_bytes=int(data.memory_usage(deep=True).sum()),
cost_per_item=3.0,
capability="thread_safe",
releases_gil=True,
operation="分组字段描述统计",
),
)
grouped_results = {}
for group_key, _, result in results:
grouped_results.setdefault(group_key, []).append(result)
describe = pd.DataFrame()
for group_key, feature_results in grouped_results.items():
describe[group_key] = pd.concat(feature_results)
if len(by) > 1:
describe.columns = pd.MultiIndex.from_tuples(describe.columns)
describe.index.names = ["特征名称", "统计指标"]
return describe