Skip to content

异常值处理

IQR 与 Z-score 异常值检测与处理。

核心代码

py
def handle_outliers(
    *,
    df: pd.DataFrame,
    params: dict[str, Any],
    column_descriptions: dict[str, str],
) -> tuple[pd.DataFrame, dict[str, Any], list[str], dict[str, str]]:
    """异常值检测、删除、缩尾和标记。"""
    columns = require_numeric_columns(df, normalize_string_list(params.get('columns')), label=_('异常值变量'))
    method = str(params.get('method') or 'iqr').strip().lower()
    action = str(params.get('action') or 'winsorize').strip().lower()
    result_df = df.copy()
    bounds: dict[str, dict[str, float | None]] = {}
    combined_mask = pd.Series(False, index=result_df.index)

    boxplot_stats: dict[str, dict[str, float | None]] = {}

    for column in columns:
        series = pd.to_numeric(result_df[column], errors='coerce')
        lower, upper = _outlier_bounds(series, method=method, params=params)
        column_mask = pd.Series(False, index=result_df.index)
        if lower is not None:
            column_mask = column_mask | (series < lower)
        if upper is not None:
            column_mask = column_mask | (series > upper)
        combined_mask = combined_mask | column_mask
        bounds[column] = {
            'lower': None if lower is None or np.isnan(lower) else float(lower),
            'upper': None if upper is None or np.isnan(upper) else float(upper),
            'outlier_rows': int(column_mask.sum()),
        }
        boxplot_stats[column] = _build_boxplot_stats(
            series=series,
            lower=lower,
            upper=upper,
            outlier_rows=int(column_mask.sum()),
        )

        if action == 'winsorize':
            if lower is not None:
                result_df.loc[series < lower, column] = lower
            if upper is not None:
                result_df.loc[series > upper, column] = upper
        elif action == 'indicator':
            indicator_name = ensure_new_column_absent(result_df, f'{column}_outlier')
            result_df[indicator_name] = column_mask.astype(int)
        elif action == 'remove':
            continue
        else:
            raise DataProcessingValidationError(_('异常值操作仅支持 remove、winsorize、indicator'))

    if action == 'remove':
        result_df = result_df.loc[~combined_mask].reset_index(drop=True)

    details = {
        'action': action,
        'method': method,
        'columns': columns,
        'bounds': bounds,
        'boxplot_stats': boxplot_stats,
        'affected_rows': int(combined_mask.sum()),
    }
    warnings = []
    if int(combined_mask.sum()) == 0:
        warnings.append(_('未检测到异常值'))
    return result_df, details, warnings, column_descriptions

Released under the AGPL-3.0 License.