"""
@Author: yp.zuo
@Date: 2023-10-15 14:30:00
@Desc: 异常检测模块，集成多种异常检测算法
"""

import typing

from abc import ABC, abstractmethod
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Literal,
    Optional,
    Protocol,
    Tuple,
    TypedDict,
    Union,
    cast,
)

import numpy as np
from scipy import stats
from sklearn.covariance import EllipticEnvelope
from sklearn.decomposition import PCA
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor, NearestNeighbors
from sklearn.preprocessing import StandardScaler

# region Method-specific Argument Types


class _ZScoreArgs(TypedDict, total=False):
    threshold: float
    use_mahalanobis: bool
    aggregation: Literal['max', 'l2', 'l1', 'mean']


class _IQRArgs(TypedDict, total=False):
    multiplier: float
    aggregation: Literal['max', 'l2', 'l1', 'mean']


class _IsolationForestArgs(TypedDict, total=False):
    n_estimators: int
    max_samples: Union[int, float, str]
    contamination: Union[float, str]
    max_features: Union[int, float]
    random_state: Optional[int]


class _LOFArgs(TypedDict, total=False):
    n_neighbors: int
    algorithm: Literal['auto', 'ball_tree', 'kd_tree', 'brute']
    leaf_size: int
    metric: str
    p: int
    contamination: Union[float, str]
    novelty: bool


class _EllipticEnvelopeArgs(TypedDict, total=False):
    support_fraction: float
    contamination: float
    random_state: Optional[int]


class _PCAArgs(TypedDict, total=False):
    n_components: Union[int, float, None]
    threshold_percentile: float


class _KNNArgs(TypedDict, total=False):
    n_neighbors: int
    threshold_percentile: float


_MethodArgs = Union[
    _ZScoreArgs,
    _IQRArgs,
    _IsolationForestArgs,
    _LOFArgs,
    _EllipticEnvelopeArgs,
    _PCAArgs,
    _KNNArgs,
]

_MethodName = Literal[
    'z_score', 'iqr', 'lof', 'isolation_forest', 'pca', 'elliptic_envelope', 'knn'
]

# endregion

# region Outlier Detection Strategies (Internal Implementation)


class _OutlierStrategy(ABC):
    """Abstract base class for outlier detection strategies."""

    def __init__(self, **kwargs):
        self.params = kwargs
        self.model_ = None
        self.statistics_ = None
        self.threshold_ = None

    @abstractmethod
    def fit(self, X: np.ndarray) -> '_OutlierStrategy':
        """Fit the model or compute statistics."""
        pass

    @abstractmethod
    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """Predict outliers and scores."""
        pass

    def fit_predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        self.fit(X)
        return self.predict(X)


class _SklearnPredictProtocol(Protocol):
    def predict(self, X: np.ndarray) -> np.ndarray: ...


def _require_model_method(
    model: object,
    method_name: Literal['predict', 'score_samples', 'decision_function', 'mahalanobis'],
) -> Callable[[np.ndarray], np.ndarray]:
    method = getattr(model, method_name, None)
    if not callable(method):
        raise TypeError(f'Model does not implement {method_name}().')
    return cast(Callable[[np.ndarray], np.ndarray], method)


class _ZScoreStrategy(_OutlierStrategy):
    """Z-Score (including Mahalanobis distance) strategy."""

    def fit(self, X: np.ndarray) -> '_ZScoreStrategy':
        use_mahalanobis = self.params.get('use_mahalanobis', False) and X.shape[1] > 1
        if use_mahalanobis:
            self.statistics_ = {
                'mean': np.mean(X, axis=0),
                'cov_inv': np.linalg.pinv(np.cov(X.T)),
                'use_mahalanobis': True,
                'n_features': X.shape[1],
            }
        else:
            self.statistics_ = {
                'mean': np.mean(X, axis=0),
                'std': np.std(X, axis=0),
                'use_mahalanobis': False,
            }
        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        threshold = self.params.get('threshold')
        aggregation = self.params.get('aggregation', 'l2')

        if self.statistics_.get('use_mahalanobis'):
            mean, cov_inv = self.statistics_['mean'], self.statistics_['cov_inv']
            n_features = self.statistics_['n_features']

            # 维度自适应阈值：如果用户未指定阈值，使用卡方分布分位数
            if threshold is None:
                # q=0.997 对应约 3σ 的两侧尾概率
                threshold = stats.chi2.ppf(0.997, df=n_features)

            diff = X - mean
            scores = np.sqrt(np.sum(diff @ cov_inv * diff, axis=1))
        else:
            mean, std = self.statistics_['mean'], self.statistics_['std']
            if threshold is None:
                threshold = 3.0  # 标准 Z-score 阈值

            with np.errstate(divide='ignore', invalid='ignore'):
                z_scores = np.abs((X - mean) / np.where(std == 0, 1e-9, std))

            # 多维聚合策略
            if X.ndim == 1 or X.shape[1] == 1:
                scores = np.nan_to_num(z_scores).flatten()  # 确保单变量返回 1D
            else:
                z_scores = np.nan_to_num(z_scores)
                if aggregation == 'max':
                    scores = np.max(z_scores, axis=1)
                elif aggregation == 'l2':
                    scores = np.sqrt(np.mean(z_scores**2, axis=1))
                elif aggregation == 'l1':
                    scores = np.mean(z_scores, axis=1)
                elif aggregation == 'mean':
                    scores = np.mean(z_scores, axis=1)
                else:
                    raise ValueError(f'Unsupported aggregation method: {aggregation}')

        outliers = scores > threshold
        return outliers, scores


class _IQRStrategy(_OutlierStrategy):
    """IQR strategy (vectorized and optimized)."""

    def fit(self, X: np.ndarray) -> '_IQRStrategy':
        self.statistics_ = {
            'q1': np.percentile(X, 25, axis=0),
            'q3': np.percentile(X, 75, axis=0),
        }
        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        multiplier = self.params.get('multiplier', 1.5)
        aggregation = self.params.get('aggregation', 'max')  # IQR 传统上使用 max

        q1, q3 = self.statistics_['q1'], self.statistics_['q3']
        iqr = q3 - q1
        lower_bound = q1 - multiplier * iqr
        upper_bound = q3 + multiplier * iqr

        outliers_matrix = (X < lower_bound) | (X > upper_bound)

        with np.errstate(divide='ignore', invalid='ignore'):
            scores_matrix = np.maximum(lower_bound - X, X - upper_bound) / np.where(
                iqr == 0, 1e-9, iqr
            )
        scores_matrix = np.maximum(scores_matrix, 0)
        scores_matrix = np.nan_to_num(scores_matrix)

        # 处理单变量和多变量情况
        if X.ndim == 1 or X.shape[1] == 1:
            outliers = outliers_matrix.flatten()  # 确保单变量返回 1D
            scores = scores_matrix.flatten()
        else:
            # 多维聚合策略
            if aggregation == 'max':
                outliers = np.any(outliers_matrix, axis=1)
                scores = np.max(scores_matrix, axis=1)
            elif aggregation == 'l2':
                outliers = np.any(outliers_matrix, axis=1)  # 任一维异常即为异常
                scores = np.sqrt(np.mean(scores_matrix**2, axis=1))
            elif aggregation == 'l1':
                outliers = np.any(outliers_matrix, axis=1)
                scores = np.mean(scores_matrix, axis=1)
            elif aggregation == 'mean':
                outliers = np.any(outliers_matrix, axis=1)
                scores = np.mean(scores_matrix, axis=1)
            else:
                raise ValueError(f'Unsupported aggregation method: {aggregation}')

        return outliers, scores


class _SklearnStrategy(_OutlierStrategy):
    """Generic strategy for scikit-learn outlier detection models."""

    def __init__(self, type_name: str, model_class, **kwargs):
        super().__init__(**kwargs)
        self.type_name = type_name
        self.model_class = model_class
        self.model_ = None

    def fit(self, X: np.ndarray) -> '_SklearnStrategy':
        self.model_ = self.model_class(**self.params)

        # LOF(novelty=False) does not support fit, it only has fit_predict.
        # So we do nothing here for that specific case.
        if self.type_name == 'lof' and not self.params.get('novelty', True):
            pass  # Defer fitting to fit_predict
        else:
            self.model_.fit(X)

        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        # 特殊处理 LOF：当 novelty=False 时，使用 fit_predict
        if self.type_name == 'lof' and not self.params.get('novelty', True):
            raise ValueError(
                'LOF with novelty=False does not support predict(). '
                'Use fit_predict() instead for this configuration.'
            )
        else:
            # 其他情况使用 predict
            if self.model_ is None:
                raise ValueError('Model has not been initialized.')
            model = cast(_SklearnPredictProtocol, self.model_)
            predictions = model.predict(X)

        outliers = predictions == -1

        if self.model_ is None:
            raise ValueError('Model has not been initialized.')
        model = self.model_
        if hasattr(model, 'score_samples'):
            scores = -_require_model_method(model, 'score_samples')(X)
        elif hasattr(model, 'decision_function'):
            scores = -_require_model_method(model, 'decision_function')(X)
        elif hasattr(model, 'mahalanobis'):
            scores = _require_model_method(model, 'mahalanobis')(X)
        else:
            scores = (predictions * -1 + 1) / 2  # Fallback: 1 for outlier, 0 for inlier
        return outliers, scores

    def fit_predict(self, X):
        if self.model_ is None:
            self.model_ = self.model_class(**self.params)
        if self.type_name == 'lof' and not self.params.get('novelty', True):
            predictions = self.model_.fit_predict(X)
            outliers = predictions == -1
            # LOF(novelty=False) has negative_outlier_factor_ for scores
            scores = -self.model_.negative_outlier_factor_
            return outliers, scores
        else:
            # 对于所有其他 sklearn 模型，使用基类的默认行为
            return super().fit_predict(X)


class _PCAReconstructionStrategy(_OutlierStrategy):
    """PCA reconstruction error strategy."""

    def fit(self, X: np.ndarray) -> '_PCAReconstructionStrategy':
        n_components = self.params.get('n_components', 0.95)
        self.model_ = PCA(n_components=n_components)
        X_transformed = self.model_.fit_transform(X)
        X_reconstructed = self.model_.inverse_transform(X_transformed)
        reconstruction_errors = np.sum((X - X_reconstructed) ** 2, axis=1)
        threshold_percentile = self.params.get('threshold_percentile', 95.0)
        self.threshold_ = np.percentile(reconstruction_errors, threshold_percentile)
        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        X_transformed = self.model_.transform(X)
        X_reconstructed = self.model_.inverse_transform(X_transformed)
        reconstruction_errors = np.sum((X - X_reconstructed) ** 2, axis=1)
        outliers = reconstruction_errors > self.threshold_
        return outliers, reconstruction_errors


class _KNNDistanceStrategy(_OutlierStrategy):
    """KNN distance strategy."""

    def fit(self, X: np.ndarray) -> '_KNNDistanceStrategy':
        n_neighbors = self.params.get('n_neighbors', 5)
        self.n_neighbors_ = max(1, n_neighbors)

        self.model_ = NearestNeighbors(n_neighbors=n_neighbors).fit(X)
        self.n_neighbors_ = n_neighbors

        # 计算训练阈值时排除自身
        distances, _ = self.model_.kneighbors(X, n_neighbors=self.n_neighbors_ + 1)
        knn_distances_excl_self = distances[:, -1]

        threshold_percentile = float(self.params.get('threshold_percentile', 95.0))
        self.threshold_ = np.percentile(knn_distances_excl_self, threshold_percentile)

        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        k = self.n_neighbors_
        n_query = k + 1 if k > 0 else 1
        distances, _ = self.model_.kneighbors(X, n_neighbors=n_query)

        # 识别测试点中的训练点 (自匹配)
        mask_self = np.isclose(distances[:, 0], 0.0)
        # 初始化分数数组
        knn_distances = np.zeros(X.shape[0])
        # 对于非自匹配点，其k-th邻居在倒数第二列 (index -2)
        # distances has shape (n_samples, k+1)
        if np.any(~mask_self):
            knn_distances[~mask_self] = distances[~mask_self, -2]

        # 对于自匹配点，其k-th“其他”邻居在最后一列 (index -1)
        if np.any(mask_self):
            knn_distances[mask_self] = distances[mask_self, -1]

        outliers = knn_distances > self.threshold_
        return outliers, knn_distances


# 默认参数配置
DEFAULT_Z_SCORE_ARGS = {'threshold': None, 'aggregation': 'l2'}  # 使用自适应阈值和L2聚合
DEFAULT_IQR_ARGS = {'multiplier': 1.5, 'aggregation': 'max'}
DEFAULT_PCA_ARGS = {'n_components': 0.95, 'threshold_percentile': 99.0}
DEFAULT_KNN_ARGS = {'n_neighbors': 5, 'threshold_percentile': 95.0}  # 更保守的默认值
DEFAULT_LOF_ARGS: _LOFArgs = {
    'n_neighbors': 5,  # 对小样本更友好
    'algorithm': 'auto',
    'novelty': False,
    # 'leaf_size': 30,
    # 'metric': 'minkowski',
    # 'p': 2,
    'contamination': 'auto',
}
DEFAULT_ISOLATION_FOREST_ARGS = {
    'n_estimators': 100,
    'contamination': 'auto',
    'random_state': None,
}
DEFAULT_ELLIPTIC_ENVELOPE_ARGS = {'contamination': 0.1, 'random_state': None}

strategy_map = {
    'z_score': (_ZScoreStrategy, DEFAULT_Z_SCORE_ARGS),
    'iqr': (_IQRStrategy, DEFAULT_IQR_ARGS),
    'pca': (_PCAReconstructionStrategy, DEFAULT_PCA_ARGS),
    'knn': (_KNNDistanceStrategy, DEFAULT_KNN_ARGS),
    'lof': (_SklearnStrategy, DEFAULT_LOF_ARGS),
    'isolation_forest': (_SklearnStrategy, DEFAULT_ISOLATION_FOREST_ARGS),
    'elliptic_envelope': (_SklearnStrategy, DEFAULT_ELLIPTIC_ENVELOPE_ARGS),
}


def _get_strategy(method: str, method_args: typing.Mapping[str, Any]) -> _OutlierStrategy:
    """Strategy factory function."""
    method_lower = method.lower()

    # 1. 定义策略类和默认参数
    if method_lower not in strategy_map:
        raise ValueError(f'Unsupported method: {method}')

    strategy_class, default_params = strategy_map[method_lower]

    # 2. 合并参数：用户传入的参数会覆盖默认值
    final_params = default_params.copy()
    final_params.update(method_args)

    # 3. 为 scikit-learn 策略准备参数
    if strategy_class is _SklearnStrategy:
        model_map = {
            'lof': LocalOutlierFactor,
            'isolation_forest': IsolationForest,
            'elliptic_envelope': EllipticEnvelope,
        }
        # 将模型类和合并后的参数一起传入
        return _SklearnStrategy(
            type_name=method_lower, model_class=model_map[method_lower], **final_params
        )
    else:
        # 对于非 sklearn 策略，直接传入合并后的参数
        return strategy_class(**final_params)


# endregion


class OutlierDetector:
    """多算法异常检测器

    一个封装了多种常见异常检测算法的统一接口，包括基于统计、
    距离和模型的方法。遵循 sklearn 风格的 fit-predict 模式。

    支持的算法:
    - `z_score`: 基于标准差的统计方法（支持马氏距离和多种聚合策略）
    - `iqr`: 四分位距方法（支持多种聚合策略）
    - `lof`: 局部异常因子
    - `isolation_forest`: 孤立森林
    - `pca`: 主成分分析重构误差
    - `elliptic_envelope`: 椭圆包络法
    - `knn`: K近邻距离法

    Z-Score 方法特性:
    - 维度自适应阈值：马氏距离时自动使用卡方分布分位数
    - 多维聚合策略：支持 'max', 'l2', 'l1', 'mean' 等聚合方式
    - 形状一致性：单变量时确保返回 1D 数组

    Examples
    --------
    >>> detector = OutlierDetector('isolation_forest', method_args={'contamination': 0.1})
    >>> detector.fit(X_train)
    >>> outliers, scores = detector.predict(X_test)
    """

    def __init__(
        self,
        method: _MethodName = 'lof',
        method_args: Optional[_MethodArgs] = None,
        normalize: bool | Literal['auto'] = 'auto',
    ):
        """初始化异常检测器

        Parameters
        ----------
        - `method` : `_MethodName`, 检测方法.
        - `method_args` : `_MethodArgs`, optional
            - 传递给所选检测算法的参数字典, by default None.
        - `normalize` : `bool | Literal['auto']`, optional
            - 是否对数据进行标准化, 默认为 'auto'。
            - 'auto' 模式下，会自动为需要标准化的算法启用。
        """
        self.method = method.lower()
        self.method_args = method_args or {}

        self.is_fitted = False

        DEFAULT_NEED_NORMALIZE = {
            'lof',
            'knn',
            'isolation_forest',
            'pca',
            'elliptic_envelope',
        }
        if normalize == 'auto':
            self.normalize = self.method in DEFAULT_NEED_NORMALIZE
        else:
            self.normalize = normalize

        self.scaler = StandardScaler() if self.normalize else None
        self.strategy_ = _get_strategy(self.method, self.method_args)

    def fit(self, X: np.ndarray) -> 'OutlierDetector':
        """训练异常检测器

        Parameters
        ----------
        - `X` : `np.ndarray`
            - 训练数据，形状为 (n_samples, n_features).

        Returns
        -------
        - `OutlierDetector`
            - 返回自身以支持链式调用。
        """
        X = np.asarray(X)
        if X.ndim == 1:
            X = X.reshape(-1, 1)

        # 对小样本进行参数自适应调整
        n_samples = X.shape[0]
        adjusted_args = self._adapt_parameters_for_sample_size(n_samples)

        # 重新创建策略以使用调整后的参数
        self.strategy_ = _get_strategy(self.method, adjusted_args)

        X_processed = np.asarray(self.scaler.fit_transform(X) if self.scaler else X)
        self.strategy_.fit(X_processed)
        self.is_fitted = True
        return self

    def _adapt_parameters_for_sample_size(self, n_samples: int) -> dict:
        """根据样本量自适应调整参数"""
        # 1) 先合并默认 + 用户参数
        _, default_params = strategy_map[self.method]
        merged = default_params.copy()
        merged.update(self.method_args)

        # 2) 再做样本感知裁剪
        if self.method in ['lof', 'knn']:
            k = merged.get('n_neighbors', 5)
            merged['n_neighbors'] = max(1, min(k, n_samples - 1))
        return merged

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """使用已训练模型检测异常值

        Parameters
        ----------
        - `X` : `np.ndarray`
            - 待检测数据，形状为 (n_samples, n_features).

        Returns
        -------
        - `Tuple[np.ndarray, np.ndarray]`
            - `outlier_mask`: 布尔数组，True表示异常值。
            - `outlier_scores`: 每个样本的异常分数。

        Raises
        ------
        - `ValueError`
            - 当检测器尚未训练时抛出。
        """
        if not self.is_fitted:
            raise ValueError('检测器尚未训练，请先调用fit()方法')

        X = np.asarray(X)
        if X.ndim == 1:
            X = X.reshape(-1, 1)

        X_processed = np.asarray(self.scaler.transform(X) if self.scaler else X)
        return self.strategy_.predict(X_processed)

    def fit_predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """
        在同一数据集上拟合模型并进行异常检测。

        Parameters
        ----------
        - `X` : `np.ndarray`
            - 输入数据，形状为 (n_samples, n_features).

        Returns
        -------
        - `Tuple[np.ndarray, np.ndarray]`
            - `outlier_mask`: 布尔数组，True表示异常值。
            - `outlier_scores`: 每个样本的异常分数。
        """
        X = np.asarray(X)
        if X.ndim == 1:
            X = X.reshape(-1, 1)

        # 同样需要参数自适应和数据标准化
        n_samples = X.shape[0]
        adjusted_args = self._adapt_parameters_for_sample_size(n_samples)
        self.strategy_ = _get_strategy(self.method, adjusted_args)

        # 注意：对于 fit_predict，标准化需要用 fit_transform
        X_processed = np.asarray(self.scaler.fit_transform(X) if self.scaler else X)

        # is_fitted 状态应该在之后设置
        result = self.strategy_.fit_predict(X_processed)
        self.is_fitted = True
        return result


class EnsembleOutlierDetector:
    """集成异常检测器

    通过结合多种不同算法的结果来提高异常检测的鲁棒性和准确性。

    支持的投票策略:
    - `majority`: 多数投票，超过一半的检测器认为异常则判定为异常。
    - `unanimous`: 一致投票，所有检测器都认为异常才判定为异常。
    - `score_threshold`: 基于集成分数的阈值判断。
    - `weighted`: 加权投票，允许为不同检测器设置权重。
    """

    def __init__(
        self,
        methods: typing.Mapping[_MethodName, Optional[_MethodArgs]],
        voting_strategy: Literal[
            'majority', 'unanimous', 'score_threshold', 'weighted'
        ] = 'majority',
        detector_weights: Optional[typing.Mapping[str, float]] = None,
        score_threshold_percentile: float = 90.0,
    ):
        """初始化集成检测器

        Parameters
        ----------
        - `methods` : `Dict[_MethodName, Optional[_MethodArgs]]`
            - 检测方法及其参数的字典。
        - `voting_strategy` : `str`, optional
            - 投票策略, by default 'majority'.
        - `detector_weights` : `Dict[str, float]`, optional
            - 各检测器的权重，仅在 'weighted' 策略下有效。
        - `score_threshold_percentile` : `float`, optional
            - 分数阈值百分位数，仅在 'score_threshold' 和 'weighted' 策略下有效。
        """
        self.methods = methods
        self.voting_strategy = voting_strategy
        self.detector_weights = detector_weights or {}
        self.score_threshold_percentile = score_threshold_percentile
        self.detectors: List[OutlierDetector] = []
        self.is_fitted = False

        self._validate_and_prepare_weights()

    def _validate_and_prepare_weights(self):
        valid_strategies = {'majority', 'unanimous', 'score_threshold', 'weighted'}
        if self.voting_strategy not in valid_strategies:
            raise ValueError(f'不支持的投票策略: {self.voting_strategy}')

        if self.voting_strategy == 'weighted':
            if not self.detector_weights:
                self.detector_weights = {method: 1.0 for method in self.methods.keys()}

            missing_weights = set(self.methods.keys()) - set(self.detector_weights.keys())  # pyright: ignore[reportOperatorIssue]
            if missing_weights:
                raise ValueError(f'缺少以下方法的权重: {missing_weights}')

            total_weight = sum(self.detector_weights.values())
            if total_weight > 0:
                self.detector_weights = {
                    k: v / total_weight for k, v in self.detector_weights.items()
                }

    def fit(self, X: np.ndarray) -> 'EnsembleOutlierDetector':
        """训练所有检测器"""
        self.detectors = [
            OutlierDetector(method=method, method_args=params).fit(X)
            for method, params in self.methods.items()
        ]
        self.is_fitted = True
        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """集成检测"""
        if not self.is_fitted:
            raise ValueError('集成检测器尚未训练')

        predictions = [detector.predict(X) for detector in self.detectors]
        all_outliers = np.array([p[0] for p in predictions]).T
        all_scores = np.array([p[1] for p in predictions]).T

        return self._apply_voting_strategy(all_outliers, all_scores)

    def fit_predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """在同一数据集上拟合并预测"""
        self.detectors = [
            OutlierDetector(method=method, method_args=params)
            for method, params in self.methods.items()
        ]
        self.is_fitted = True
        predictions = [detector.fit_predict(X) for detector in self.detectors]
        all_outliers = np.array([p[0] for p in predictions]).T
        all_scores = np.array([p[1] for p in predictions]).T

        return self._apply_voting_strategy(all_outliers, all_scores)

    def _apply_voting_strategy(self, all_outliers, all_scores):
        # 标准化分数以进行公平比较
        with np.errstate(divide='ignore', invalid='ignore'):
            normalized_scores = stats.zscore(all_scores, axis=0, nan_policy='omit')
            normalized_scores = np.nan_to_num(normalized_scores)

        if self.voting_strategy == 'majority':
            ensemble_outliers = np.sum(all_outliers, axis=1) > len(self.methods) // 2
            ensemble_scores = np.nanmean(normalized_scores, axis=1)
        elif self.voting_strategy == 'unanimous':
            ensemble_outliers = np.all(all_outliers, axis=1)
            ensemble_scores = np.nanmean(normalized_scores, axis=1)
        elif self.voting_strategy == 'score_threshold':
            ensemble_scores = np.nanmean(normalized_scores, axis=1)
            threshold = np.nanpercentile(ensemble_scores, self.score_threshold_percentile)
            ensemble_outliers = ensemble_scores > threshold
        elif self.voting_strategy == 'weighted':
            weights = np.array([self.detector_weights[m] for m in self.methods.keys()])
            ensemble_scores = np.sum(normalized_scores * weights, axis=1)
            threshold = np.nanpercentile(ensemble_scores, self.score_threshold_percentile)
            ensemble_outliers = ensemble_scores > threshold
        else:
            raise ValueError(f'不支持的投票策略: {self.voting_strategy}')

        return ensemble_outliers, ensemble_scores


def detect_outliers(
    X: np.ndarray, method: _MethodName = 'lof', method_args: Optional[_MethodArgs] = None
) -> Tuple[np.ndarray, np.ndarray]:
    """简化的异常检测接口

    一个便利函数，用于快速执行“fit”和“predict”两个步骤。
    注意：此函数在同一数据上进行训练和预测，主要用于探索性分析。

    Parameters
    ----------
    - `X` : `np.ndarray`
        - 输入数据，形状为 (n_samples, n_features).
    - `method` : `_MethodName`, optional
        - 检测方法, by default 'lof'.
    - `method_args` : `_MethodArgs`, optional
        - 传递给所选方法的参数字典, by default None.

    Returns
    -------
    - `Tuple[np.ndarray, np.ndarray]`
        - `outliers`: 异常值掩码，True表示异常值。
        - `scores`: 异常分数，值越大越可能是异常值。
    """
    detector = OutlierDetector(method=method, method_args=method_args)
    return detector.fit_predict(X)
