正态分布测试

Posted by Shallow Dreameron September 7, 2025

如何验证一组数据是否符合“σ(sigma)正态分布”规则(实用指南)

下面给出一套清晰的、可复用的步骤——既有直观可视化,也有定量检验,并附带 Python 实用函数。主要思路是同时检查经验规则(68–95–99.7%)*与*统计检验/图形诊断,因为单一方法容易误导。


1) 快速数值检查(均值 / 标准差 / z-score /经验比例)

计算样本均值 μ 和标准差 σ,然后统计数据落在区间 μ±1σμ±2σμ±3σ 的比例,和理论比例对比:

  • 理论(正态分布):
    • ±1σ ≈ 68.27%
    • ±2σ ≈ 95.45%
    • ±3σ ≈ 99.73%

判断参考(经验):若实测比例接近理论值(例如在 ±3% 或 ±5% 的容差内),说明与正态相符;若差异很大,则可能不是正态。


2) 可视化诊断(必做)

  • 直方图 + 拟合正态曲线:观察形状(对称性、尾部)。
  • Q–Q 图(Quantile-Quantile plot):若点接近对角线,说明接近正态;系统性偏离表明偏斜或厚/薄尾。
  • 箱线图 可以帮助查看异常值。

可视化通常比单个 p 值更有信息量。


3) 统计检验(定量)

  • Shapiro–Wilk 检验scipy.stats.shapiro):对小/中样本(例如 n < 5000)很常用。p > 0.05 通常表示不能拒绝正态性(即“看起来像正态”)。
  • Anderson–Darling 检验scipy.stats.anderson):给出更敏感的偏差检测,返回统计量并有显著性表格。
  • Kolmogorov–Smirnov 检验(对比正态):在用于比较总体和指定分布时要谨慎(参数需用样本估计时临界值改变)。
  • 注意:当样本量很大时(例如 n 很大),微小偏差会导致检验显著(拒绝正态性),因此需结合可视化与实际应用场景判断。

4) 其他指标

  • 偏度(skewness)峰度(kurtosis):正态分布的偏度≈0,峰度≈3(常用 excess kurtosis = kurtosis-3)。显著偏离表明不是正态。
  • 离群点检测:若少数极端值影响整体分布,考虑先处理或报告。

5) 实用 Python 函数(可直接运行)

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats

def check_sigma_distribution(arr, plot=True, tolerance_pct=5.0):
    """
    输入: 一维数组或 pandas Series
    输出: 一个字典报告 + 可选图形
    tolerance_pct: 用于经验规则比较的容差(百分比,例如5%)
    """
    x = np.asarray(arr).astype(float)
    x = x[~np.isnan(x)]
    n = x.size
    mu = x.mean()
    sigma = x.std(ddof=1)  # 样本标准差

    # empirical proportions
    within_1 = np.mean((x >= mu - sigma) & (x <= mu + sigma)) * 100
    within_2 = np.mean((x >= mu - 2*sigma) & (x <= mu + 2*sigma)) * 100
    within_3 = np.mean((x >= mu - 3*sigma) & (x <= mu + 3*sigma)) * 100

    empirical = {'±1σ': within_1, '±2σ': within_2, '±3σ': within_3}
    theoretical = {'±1σ': 68.27, '±2σ': 95.45, '±3σ': 99.73}
    diffs = {k: empirical[k] - theoretical[k] for k in empirical}

    # statistical tests
    sw_stat, sw_p = (None, None)
    if n >= 3 and n <= 5000:
        sw_stat, sw_p = stats.shapiro(x)
    ad_result = stats.anderson(x, dist='norm')  # returns statistic + critical values

    skew = stats.skew(x)
    kurt = stats.kurtosis(x, fisher=False)  # Pearson's definition (≈3 for normal)
    excess_kurtosis = kurt - 3.0

    report = {
        'n': n, 'mean': mu, 'std_sample': sigma,
        'empirical_pct': empirical, 'theoretical_pct': theoretical, 'diffs_pct': diffs,
        'skewness': skew, 'kurtosis': kurt, 'excess_kurtosis': excess_kurtosis,
        'shapiro': {'stat': sw_stat, 'p': sw_p} if sw_stat is not None else 'skipped (n out of range)',
        'anderson': {'stat': ad_result.statistic, 'critical_values': ad_result.critical_values, 'significance_level': ad_result.significance_level}
    }

    # simple rule of thumb decision (heuristic)
    within_tolerance = all(abs(diffs[k]) <= tolerance_pct for k in diffs)
    normality_hint = {
        'empirical_rule_ok': within_tolerance,
        'shapiro_pass': (sw_p is not None and sw_p > 0.05),
        'anderson_stat': ad_result.statistic  # lower is better
    }
    report['hint'] = normality_hint

    if plot:
        # histogram + normal pdf
        plt.figure(figsize=(10,4))
        plt.subplot(1,2,1)
        count, bins, _ = plt.hist(x, bins='auto', density=True, alpha=0.6)
        # plot normal pdf
        xs = np.linspace(x.min(), x.max(), 200)
        plt.plot(xs, stats.norm.pdf(xs, mu, sigma), linewidth=2)
        plt.title('Histogram & fitted normal')

        # Q-Q plot
        plt.subplot(1,2,2)
        stats.probplot(x, dist="norm", sparams=(mu, sigma), plot=plt)
        plt.title('Q-Q plot')
        plt.tight_layout()
        plt.show()

    return report

# 使用示例:
# data = pd.Series(...) 或 numpy array
# r = check_sigma_distribution(data)
# print(r)

6) 实用建议 / 决策流程

  1. 先用直方图与 Q–Q 图观察总体形状。
  2. 计算 ±1/2/3σ 的经验比例,看是否偏离太多。
  3. 根据样本大小用 Shapiro 或 AD 检验(并解读 p 值和统计量)。
  4. 如果轻微偏差但业务不敏感 —— 可以当作“近似正态”;若精确分布很关键(例如统计推断假设),则不要将其视为正态,考虑非参数方法或变换(对数、Box–Cox)。
  5. 对于明显偏斜或厚尾:考虑去极值、对数/根号变换或使用更稳健的分布建模(如 t 分布、对数正态等)。

7) 注意事项

  • 样本量影响检验:小样本检验力低(难以发现偏差);大样本会把微小偏差检测为显著。始终结合图形判断。
  • 独立同分布假设:σ 规则基于独立样本,若数据有时间序列相关性(自相关)或分组差异,需先处理依赖性/分层。
  • 缺失值/极端值:先审视并决定是否清洗或特别标注。

明白✅

如果你想用一个核心指标来判断数据是否近似正态分布,最常见的做法是使用 Shapiro–Wilk 检验的 p 值,因为它专门针对正态性。

规则很简单:

  • p > 0.05 → 不能拒绝正态性(数据看起来符合正态分布)
  • p ≤ 0.05 → 拒绝正态性(数据显著偏离正态分布)

Python 代码示例

import numpy as np
from scipy import stats

def is_normal(data, alpha=0.05):
    """
    使用 Shapiro-Wilk 检验判断数据是否符合正态分布
    :param data: list 或 numpy array
    :param alpha: 显著性水平,默认0.05
    :return: (是否正态, p值)
    """
    data = np.asarray(data).astype(float)
    data = data[~np.isnan(data)]  # 去掉 NaN
    
    stat, p = stats.shapiro(data)
    return (p > alpha, p)

# 示例
data1 = np.random.normal(0, 1, 500)  # 正态分布
data2 = np.random.exponential(1, 500)  # 非正态分布

print("data1:", is_normal(data1))
print("data2:", is_normal(data2))

输出示例

data1: (True, 0.31)   # p=0.31 > 0.05 → 符合正态分布
data2: (False, 1.2e-12)  # p≈0 → 显著不符合正态分布

好的✅ 我给你写一个通用函数,可以通过参数选择不同的判断指标,返回 True/False 是否为正态分布,并附带 p 值或统计量,方便参考。


代码示例

import numpy as np
from scipy import stats

def is_normal(data, method="shapiro", alpha=0.05):
    """
    判断数据是否符合正态分布
    
    :param data: list 或 numpy array
    :param method: 检验方法,可选:
                   - "shapiro" (Shapiro-Wilk)
                   - "anderson" (Anderson-Darling)
                   - "kstest" (Kolmogorov-Smirnov, 与拟合的正态分布比较)
                   - "skewkurtosis" (基于偏度+峰度的Z检验)
    :param alpha: 显著性水平,默认 0.05
    :return: (是否正态, 统计信息)
    """
    data = np.asarray(data).astype(float)
    data = data[~np.isnan(data)]  # 去掉 NaN
    n = len(data)

    if method == "shapiro":
        stat, p = stats.shapiro(data)
        return (p > alpha, {"method": "Shapiro-Wilk", "stat": stat, "p": p})

    elif method == "anderson":
        result = stats.anderson(data, dist="norm")
        # 如果统计量 < 临界值,则认为近似正态
        is_norm = result.statistic < result.critical_values[list(result.significance_level).index(int(alpha*100))]
        return (is_norm, {"method": "Anderson-Darling", "stat": result.statistic,
                          "critical_values": result.critical_values,
                          "significance_level": result.significance_level})

    elif method == "kstest":
        mu, sigma = np.mean(data), np.std(data, ddof=1)
        stat, p = stats.kstest(data, "norm", args=(mu, sigma))
        return (p > alpha, {"method": "Kolmogorov-Smirnov", "stat": stat, "p": p})

    elif method == "skewkurtosis":
        # 偏度与峰度的Z检验
        skew, skew_p = stats.skewtest(data)
        kurt, kurt_p = stats.kurtosistest(data)
        is_norm = (skew_p > alpha) and (kurt_p > alpha)
        return (is_norm, {"method": "Skewness-Kurtosis",
                          "skew_stat": skew, "skew_p": skew_p,
                          "kurt_stat": kurt, "kurt_p": kurt_p})

    else:
        raise ValueError("method 参数必须是 'shapiro'、'anderson'、'kstest' 或 'skewkurtosis'")

# ---------------- 示例 ----------------
if __name__ == "__main__":
    np.random.seed(0)
    data_norm = np.random.normal(0, 1, 500)
    data_non_norm = np.random.exponential(1, 500)

    for method in ["shapiro", "anderson", "kstest", "skewkurtosis"]:
        print(f"方法: {method}")
        print("正态数据:", is_normal(data_norm, method))
        print("非正态数据:", is_normal(data_non_norm, method))
        print("------")

说明

  • shapiro:适合小中样本,常用默认方法。
  • anderson:返回多个显著性水平下的判断,更严格。
  • kstest:基于样本均值和方差拟合正态分布,再做 K-S 检验。
  • skewkurtosis:偏度+峰度联合检验,快速直观。

好的✅ 我帮你封装一个 综合判断函数,它会自动跑 Shapiro-Wilk、Anderson-Darling、K-S 检验、偏度-峰度检验 四种方法,然后给出每个方法的结果,并汇总一个“多数投票”的综合结论。


代码示例

import numpy as np
from scipy import stats

def check_normality(data, alpha=0.05):
    """
    综合判断数据是否符合正态分布
    
    :param data: list 或 numpy array
    :param alpha: 显著性水平,默认 0.05
    :return: dict,包含各方法结果 + 综合结论
    """
    data = np.asarray(data).astype(float)
    data = data[~np.isnan(data)]  # 去掉 NaN
    n = len(data)
    
    results = {}

    # 1. Shapiro-Wilk
    if 3 <= n <= 5000:  # 超过5000会报错
        stat, p = stats.shapiro(data)
        results["shapiro"] = {"is_normal": p > alpha, "stat": stat, "p": p}
    else:
        results["shapiro"] = {"is_normal": None, "stat": None, "p": None, "note": "样本量超出Shapiro适用范围"}

    # 2. Anderson-Darling
    ad_result = stats.anderson(data, dist="norm")
    crit_dict = dict(zip(ad_result.significance_level, ad_result.critical_values))
    crit_value = crit_dict.get(int(alpha * 100), ad_result.critical_values[2])  # 找最近的水平
    is_norm_ad = ad_result.statistic < crit_value
    results["anderson"] = {
        "is_normal": is_norm_ad,
        "stat": ad_result.statistic,
        "critical_value": crit_value
    }

    # 3. Kolmogorov-Smirnov
    mu, sigma = np.mean(data), np.std(data, ddof=1)
    stat, p = stats.kstest(data, "norm", args=(mu, sigma))
    results["kstest"] = {"is_normal": p > alpha, "stat": stat, "p": p}

    # 4. Skewness & Kurtosis 检验
    skew_stat, skew_p = stats.skewtest(data)
    kurt_stat, kurt_p = stats.kurtosistest(data)
    is_norm_sk = (skew_p > alpha) and (kurt_p > alpha)
    results["skewkurtosis"] = {
        "is_normal": is_norm_sk,
        "skew_stat": skew_stat, "skew_p": skew_p,
        "kurt_stat": kurt_stat, "kurt_p": kurt_p
    }

    # 综合结论(投票法,排除 None)
    votes = [res["is_normal"] for res in results.values() if res["is_normal"] is not None]
    if len(votes) > 0:
        final_decision = votes.count(True) >= votes.count(False)
    else:
        final_decision = None

    return {
        "methods": results,
        "final_decision": final_decision
    }


# ---------------- 示例 ----------------
if __name__ == "__main__":
    np.random.seed(0)
    data_norm = np.random.normal(0, 1, 500)
    data_non_norm = np.random.exponential(1, 500)

    print("正态数据结果:")
    print(check_normality(data_norm))

    print("\n非正态数据结果:")
    print(check_normality(data_non_norm))

输出示例(简化)

正态数据结果:
{
  'methods': {
    'shapiro': {'is_normal': True, 'stat': 0.998, 'p': 0.64},
    'anderson': {'is_normal': True, 'stat': 0.29, 'critical_value': 0.74},
    'kstest': {'is_normal': True, 'stat': 0.03, 'p': 0.56},
    'skewkurtosis': {'is_normal': True, 'skew_stat': 0.12, 'skew_p': 0.90, 'kurt_stat': 0.33, 'kurt_p': 0.74}
  },
  'final_decision': True
}

非正态数据结果:
{
  'methods': {
    'shapiro': {'is_normal': False, 'stat': 0.81, 'p': 1.2e-12},
    'anderson': {'is_normal': False, 'stat': 7.25, 'critical_value': 0.74},
    'kstest': {'is_normal': False, 'stat': 0.28, 'p': 1.3e-15},
    'skewkurtosis': {'is_normal': False, 'skew_stat': 12.1, 'skew_p': 0.0, 'kurt_stat': 8.3, 'kurt_p': 0.0}
  },
  'final_decision': False
}

这样你就能一行调用,自动跑 4 种方法,最后看 final_decision 就行。

👌 那我帮你把可视化整合进去:在综合检验时,可以画出

  1. 直方图 + 拟合正态曲线
  2. Q–Q 图(分位数-分位数图)

这样你既有数值判断(四种方法 + 投票结论),又有图形直观验证。


完整代码(带可视化)

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

def check_normality(data, alpha=0.05, plot=True):
    """
    综合判断数据是否符合正态分布,并可视化
    
    :param data: list 或 numpy array
    :param alpha: 显著性水平
    :param plot: 是否绘制图形
    :return: dict,包含各方法结果 + 综合结论
    """
    data = np.asarray(data).astype(float)
    data = data[~np.isnan(data)]  # 去掉 NaN
    n = len(data)
    
    results = {}

    # 1. Shapiro-Wilk
    if 3 <= n <= 5000:  # 超过5000会报错
        stat, p = stats.shapiro(data)
        results["shapiro"] = {"is_normal": p > alpha, "stat": stat, "p": p}
    else:
        results["shapiro"] = {"is_normal": None, "note": "样本量超出Shapiro适用范围"}

    # 2. Anderson-Darling
    ad_result = stats.anderson(data, dist="norm")
    crit_dict = dict(zip(ad_result.significance_level, ad_result.critical_values))
    crit_value = crit_dict.get(int(alpha * 100), ad_result.critical_values[2])  # 找最近水平
    is_norm_ad = ad_result.statistic < crit_value
    results["anderson"] = {
        "is_normal": is_norm_ad,
        "stat": ad_result.statistic,
        "critical_value": crit_value
    }

    # 3. Kolmogorov-Smirnov
    mu, sigma = np.mean(data), np.std(data, ddof=1)
    stat, p = stats.kstest(data, "norm", args=(mu, sigma))
    results["kstest"] = {"is_normal": p > alpha, "stat": stat, "p": p}

    # 4. Skewness & Kurtosis
    skew_stat, skew_p = stats.skewtest(data)
    kurt_stat, kurt_p = stats.kurtosistest(data)
    is_norm_sk = (skew_p > alpha) and (kurt_p > alpha)
    results["skewkurtosis"] = {
        "is_normal": is_norm_sk,
        "skew_stat": skew_stat, "skew_p": skew_p,
        "kurt_stat": kurt_stat, "kurt_p": kurt_p
    }

    # 综合结论(投票法,排除 None)
    votes = [res["is_normal"] for res in results.values() if res["is_normal"] is not None]
    if len(votes) > 0:
        final_decision = votes.count(True) >= votes.count(False)
    else:
        final_decision = None

    report = {"methods": results, "final_decision": final_decision}

    # ---------- 可视化 ----------
    if plot:
        fig, axes = plt.subplots(1, 2, figsize=(10, 4))

        # 直方图 + 正态拟合曲线
        axes[0].hist(data, bins="auto", density=True, alpha=0.6, color="skyblue", edgecolor="black")
        xs = np.linspace(data.min(), data.max(), 200)
        axes[0].plot(xs, stats.norm.pdf(xs, mu, sigma), 'r-', lw=2)
        axes[0].set_title("直方图 + 正态拟合曲线")

        # Q-Q 图
        stats.probplot(data, dist="norm", plot=axes[1])
        axes[1].set_title("Q-Q 图")

        plt.tight_layout()
        plt.show()

    return report


# ---------------- 示例 ----------------
if __name__ == "__main__":
    np.random.seed(0)
    data_norm = np.random.normal(0, 1, 500)   # 正态数据
    data_non_norm = np.random.exponential(1, 500)  # 非正态数据

    print("正态数据结果:")
    print(check_normality(data_norm, plot=True))

    print("\n非正态数据结果:")
    print(check_normality(data_non_norm, plot=True))

使用效果

  • 打印输出:每个方法的结果(是否正态、统计量、p 值等)+ 综合结论 final_decision
  • 绘图
    • 左边:数据直方图 + 拟合的正态曲线
    • 右边:Q-Q 图(点越贴近对角线,越符合正态)

👌 那我在上面的综合函数里加上 经验 68–95–99.7% 检查,也就是统计数据落在 μ±1σ、μ±2σ、μ±3σ 区间的比例,并和理论值做对比。

这样你就能同时看到:

  • 四种统计检验结果
  • 综合结论(投票)
  • Sigma Rule(经验法则)的比例情况
  • 可视化结果

完整代码(带 Sigma Rule 检查)

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

def check_normality(data, alpha=0.05, plot=True):
    """
    综合判断数据是否符合正态分布(多种方法 + Sigma Rule + 可视化)
    
    :param data: list 或 numpy array
    :param alpha: 显著性水平
    :param plot: 是否绘制图形
    :return: dict,包含各方法结果 + Sigma Rule + 综合结论
    """
    data = np.asarray(data).astype(float)
    data = data[~np.isnan(data)]  # 去掉 NaN
    n = len(data)
    
    results = {}

    # 1. Shapiro-Wilk
    if 3 <= n <= 5000:  # 超过5000会报错
        stat, p = stats.shapiro(data)
        results["shapiro"] = {"is_normal": p > alpha, "stat": stat, "p": p}
    else:
        results["shapiro"] = {"is_normal": None, "note": "样本量超出Shapiro适用范围"}

    # 2. Anderson-Darling
    ad_result = stats.anderson(data, dist="norm")
    crit_dict = dict(zip(ad_result.significance_level, ad_result.critical_values))
    crit_value = crit_dict.get(int(alpha * 100), ad_result.critical_values[2])  # 找最近水平
    is_norm_ad = ad_result.statistic < crit_value
    results["anderson"] = {
        "is_normal": is_norm_ad,
        "stat": ad_result.statistic,
        "critical_value": crit_value
    }

    # 3. Kolmogorov-Smirnov
    mu, sigma = np.mean(data), np.std(data, ddof=1)
    stat, p = stats.kstest(data, "norm", args=(mu, sigma))
    results["kstest"] = {"is_normal": p > alpha, "stat": stat, "p": p}

    # 4. Skewness & Kurtosis
    skew_stat, skew_p = stats.skewtest(data)
    kurt_stat, kurt_p = stats.kurtosistest(data)
    is_norm_sk = (skew_p > alpha) and (kurt_p > alpha)
    results["skewkurtosis"] = {
        "is_normal": is_norm_sk,
        "skew_stat": skew_stat, "skew_p": skew_p,
        "kurt_stat": kurt_stat, "kurt_p": kurt_p
    }

    # 5. Sigma Rule 检查
    within_1 = np.mean((data >= mu - sigma) & (data <= mu + sigma)) * 100
    within_2 = np.mean((data >= mu - 2*sigma) & (data <= mu + 2*sigma)) * 100
    within_3 = np.mean((data >= mu - 3*sigma) & (data <= mu + 3*sigma)) * 100

    sigma_rule = {
        "empirical": {"±1σ": within_1, "±2σ": within_2, "±3σ": within_3},
        "theoretical": {"±1σ": 68.27, "±2σ": 95.45, "±3σ": 99.73}
    }

    # 综合结论(投票法,排除 None)
    votes = [res["is_normal"] for res in results.values() if res["is_normal"] is not None]
    if len(votes) > 0:
        final_decision = votes.count(True) >= votes.count(False)
    else:
        final_decision = None

    report = {"methods": results, "sigma_rule": sigma_rule, "final_decision": final_decision}

    # ---------- 可视化 ----------
    if plot:
        fig, axes = plt.subplots(1, 2, figsize=(10, 4))

        # 直方图 + 正态拟合曲线
        axes[0].hist(data, bins="auto", density=True, alpha=0.6, color="skyblue", edgecolor="black")
        xs = np.linspace(data.min(), data.max(), 200)
        axes[0].plot(xs, stats.norm.pdf(xs, mu, sigma), 'r-', lw=2)
        axes[0].set_title("直方图 + 正态拟合曲线")

        # Q-Q 图
        stats.probplot(data, dist="norm", plot=axes[1])
        axes[1].set_title("Q-Q 图")

        plt.tight_layout()
        plt.show()

    return report


# ---------------- 示例 ----------------
if __name__ == "__main__":
    np.random.seed(0)
    data_norm = np.random.normal(0, 1, 500)   # 正态数据
    data_non_norm = np.random.exponential(1, 500)  # 非正态数据

    print("正态数据结果:")
    print(check_normality(data_norm, plot=True))

    print("\n非正态数据结果:")
    print(check_normality(data_non_norm, plot=True))

输出示例(部分)

"final_decision": True,
"sigma_rule": {
  "empirical": {"±1σ": 67.8, "±2σ": 95.0, "±3σ": 99.4},
  "theoretical": {"±1σ": 68.27, "±2σ": 95.45, "±3σ": 99.73}
}

这样就能看到经验比例和理论值的对比。


你说的情况其实很常见,我来帮你拆解一下:


1. 为什么会出现 “检验不通过,但σ法则很接近”

  • 统计检验太严格
    • Shapiro-Wilk、Anderson-Darling 等检验在样本量大时非常敏感,哪怕数据稍微偏一点点正态分布,就会拒绝正态性。
    • 这时即使数据“看起来很正态”,也会被判定为不正态。
  • σ法则更宽松
    • σ 法则(68–95–99.7 规则)本质是经验分布特征:
      • ±1σ 包含 ≈68% 数据
      • ±2σ 包含 ≈95% 数据
      • ±3σ 包含 ≈99.7% 数据
    • 如果你的数据接近这个分布,说明它近似正态分布,即使严格检验不通过。

2. 怎么处理这种冲突?

你可以建立一个 “宽松 + 严格”双重判定 机制:

  • 严格判定:用统计检验(p > 0.05 才算正态)。
  • 宽松判定:用 σ 法则误差 ≤ 阈值(例如允许误差 5%)。
  • 综合结论
    • 如果统计检验不通过,但 σ 法则误差很小 → 近似正态
    • 如果两者都不通过 → 非正态
    • 如果两者都通过 → 正态

3. 示例代码(改进版)

import numpy as np
from scipy import stats

def sigma_rule_check(data, tol=0.05):
    mean, std = np.mean(data), np.std(data)
    counts = [
        np.mean((data > mean - k*std) & (data < mean + k*std))
        for k in [1, 2, 3]
    ]
    expected = [0.68, 0.95, 0.997]
    diffs = [abs(c - e) for c, e in zip(counts, expected)]
    return all(d < tol for d in diffs), counts, diffs

def check_normality(data, alpha=0.05, tol=0.05):
    data = np.asarray(data).astype(float)
    data = data[~np.isnan(data)]  # 去掉 NaN

    # 严格:Shapiro-Wilk
    _, p = stats.shapiro(data)
    strict = p > alpha

    # 宽松:σ法则
    loose, counts, diffs = sigma_rule_check(data, tol=tol)

    # 综合结论
    if strict:
        conclusion = "正态分布"
    elif loose:
        conclusion = "近似正态分布"
    else:
        conclusion = "非正态分布"

    return {
        "p_value": p,
        "sigma_counts": counts,
        "sigma_diffs": diffs,
        "strict": strict,
        "loose": loose,
        "conclusion": conclusion
    }

# 示例
data1 = np.random.normal(0, 1, 1000)
data2 = np.random.exponential(1, 1000)

print(check_normality(data1))
print(check_normality(data2))

4. 输出示例

{'p_value': 0.01, 'sigma_counts': [0.682, 0.953, 0.998],
 'sigma_diffs': [0.002, 0.003, 0.001],
 'strict': False, 'loose': True,
 'conclusion': '近似正态分布'}

意思是:统计检验很严格地拒绝了正态,但 σ 法则几乎完全符合 → 可判定为“近似正态”


很好 — 既然你的数据来自训练过程,存在测量误差和极端值(outliers),我们需要一套有步骤、可复用的流程来处理这类“近似正态但被异常值干扰”的情况。下面给出原则 + 实战代码(可直接运行),并在每一步给出为什么这么做以及何时选用的建议。

思路(原则性)

  1. 先不轻易删数据:训练数据可能包含有意义的极端值(真实信号),盲目删除会损失信息。
  2. 检测——定性判断——处理——复检:检测异常 → 判断异常是否为噪声/测量误差 → 选择处理策略(修正/截尾/变换/稳健建模)→ 重新检验正态性与下游影响。
  3. 可复现、可回退:所有处理应可回退(记录被修改或截尾数据)。
  4. 在建模时使用稳健方法:若无法保证正态性,可选择对异常不敏感的模型或分布(如 Student’s t、鲁棒回归等)。

常用策略(优先级与何时用)

  • 检测异常:IQR(适合右偏/左偏都能检测),Z-score(对较小异常有效),MAD/robust-z(对大量异常更稳健)。
  • 处理异常
    • 修正/裁剪(cap/winsorize)——当异常是测量错误或明显离群点时。
    • 删除(trim)——只在确定为坏数据时,并记录。
    • 变换(log, sqrt, Box-Cox, Yeo-Johnson)——当数据呈右偏或乘法噪声。
    • 使用稳健统计量(中位数、MAD)/稳健模型(RANSAC、Huber)或假设更厚尾分布(t分布)。
  • 重新检验:重复正态性检验与 sigma-rule 检查,并评估对下游任务(模型性能、指标)的影响。

可直接运行的 Python 实战代码(完整、带注释)

import numpy as np
import pandas as pd
from scipy import stats
from sklearn.preprocessing import PowerTransformer
from statsmodels.robust.scale import mad as statsmodels_mad
from typing import Tuple, Dict

# ----------------- 检测异常 -----------------
def detect_outliers_iqr(x: np.ndarray, k=1.5) -> np.ndarray:
    """返回布尔数组,True 表示该点为异常(基于 IQR)"""
    q1, q3 = np.nanpercentile(x, [25, 75])
    iqr = q3 - q1
    lower, upper = q1 - k * iqr, q3 + k * iqr
    return (x < lower) | (x > upper)

def detect_outliers_zscore(x: np.ndarray, z_thresh=3.0) -> np.ndarray:
    """基于标准 z-score 的异常检测(对极端值敏感)"""
    z = (x - np.nanmean(x)) / (np.nanstd(x, ddof=1))
    return np.abs(z) > z_thresh

def detect_outliers_robust_z(x: np.ndarray, z_thresh=3.5) -> np.ndarray:
    """
    基于中位数和 MAD 的 robust z-score(对异常更稳健)
    公式:robust_z = 0.6745*(x - median)/MAD
    """
    med = np.nanmedian(x)
    mad = statsmodels_mad(x, center=med)  # 返回MAD
    if mad == 0:
        # fallback: use std
        mad = np.nanstd(x, ddof=1)
        if mad == 0:
            return np.zeros_like(x, dtype=bool)
    robust_z = 0.6745 * (x - med) / mad
    return np.abs(robust_z) > z_thresh

# ----------------- 处理方法 -----------------
def winsorize_series(x: np.ndarray, lower_pct=0.01, upper_pct=0.99) -> np.ndarray:
    """截尾(winzorize): 将低于lower_pct分位数的值替换为该分位值,高于upper_pct分位数类似处理"""
    low = np.nanpercentile(x, lower_pct * 100)
    high = np.nanpercentile(x, upper_pct * 100)
    x2 = x.copy()
    x2[x2 < low] = low
    x2[x2 > high] = high
    return x2

def trim_series(x: np.ndarray, lower_pct=0.01, upper_pct=0.99) -> np.ndarray:
    """截断(删除)边界外数据;返回掩码与保留数据索引"""
    low = np.nanpercentile(x, lower_pct * 100)
    high = np.nanpercentile(x, upper_pct * 100)
    mask = (x >= low) & (x <= high)
    return mask

def apply_transformations(x: np.ndarray, method: str):
    """变换:'log','boxcox','yeojohnson'(后两者需要正值/可兼容)"""
    x = np.asarray(x).astype(float)
    if method == "log":
        # 为避免 log(<=0),先平移最小值
        shift = 0
        minv = np.nanmin(x)
        if minv <= 0:
            shift = 1 - minv
        return np.log(x + shift), {"shift": shift}
    elif method == "boxcox":
        # Box-Cox 要求正值
        if np.nanmin(x) <= 0:
            raise ValueError("Box-Cox requires positive data")
        transformed, lmbda = stats.boxcox(x)
        return transformed, {"lambda": lmbda}
    elif method == "yeojohnson":
        # Yeo-Johnson 可处理非正数据
        pt = PowerTransformer(method="yeo-johnson", standardize=False)
        xt = pt.fit_transform(x.reshape(-1, 1)).ravel()
        return xt, {"pt": pt}
    else:
        raise ValueError("Unknown transform")

# ----------------- 正态性检验(复用之前的概念) -----------------
def shapiro_p(x: np.ndarray) -> float:
    """返回 Shapiro-Wilk 的 p 值(若样本太大则返回 np.nan)"""
    n = np.sum(~np.isnan(x))
    if n < 3:
        return np.nan
    if n > 5000:
        # Shapiro 在 scipy 中对大样本可能失去适用性
        return np.nan
    stat, p = stats.shapiro(x)
    return p

def sigma_rule_percentages(x: np.ndarray) -> Dict[str, float]:
    mu = np.nanmean(x); sigma = np.nanstd(x, ddof=1)
    within1 = np.mean((x >= mu - sigma) & (x <= mu + sigma)) * 100
    within2 = np.mean((x >= mu - 2*sigma) & (x <= mu + 2*sigma)) * 100
    within3 = np.mean((x >= mu - 3*sigma) & (x <= mu + 3*sigma)) * 100
    return {"±1σ": within1, "±2σ": within2, "±3σ": within3}

# ----------------- 综合处理流程函数 -----------------
def robust_normalize_pipeline(
    x: np.ndarray,
    outlier_method: str = "iqr",
    outlier_action: str = "winsorize",  # 'winsorize','trim','mark'
    transform_try: list = ["yeojohnson", "log"],  # try in order
    win_lower=0.01, win_upper=0.99,
    z_thresh=3.5
) -> Dict:
    """
    一个端到端流程:
    1) 检测异常(iqr/robust_z/zscore)
    2) 根据 outlier_action 处理(winsorize/trim/mark)
    3) 尝试变换(若需要)并评估正态性 (Shapiro + sigma_rule)
    4) 返回处理前后诊断与处理结果
    """
    orig = np.asarray(x).astype(float)
    x0 = orig.copy()
    mask_nan = np.isnan(x0)
    n_total = x0.size

    # 1) 检测
    if outlier_method == "iqr":
        out_mask = detect_outliers_iqr(x0)
    elif outlier_method == "zscore":
        out_mask = detect_outliers_zscore(x0, z_thresh=z_thresh)
    elif outlier_method == "robust_z":
        out_mask = detect_outliers_robust_z(x0, z_thresh=z_thresh)
    else:
        raise ValueError("Unknown outlier_method")

    out_mask = out_mask & (~mask_nan)  # 不把 NaN 当作异常
    n_outliers = int(np.nansum(out_mask))

    # 2) 处理
    x_processed = x0.copy()
    removed_idx = None
    if outlier_action == "winsorize":
        x_processed = winsorize_series(x_processed, lower_pct=win_lower, upper_pct=win_upper)
    elif outlier_action == "trim":
        mask_keep = trim_series(x_processed, lower_pct=win_lower, upper_pct=win_upper)
        removed_idx = np.where(~mask_keep)[0].tolist()
        x_processed = x_processed[mask_keep]
    elif outlier_action == "mark":
        # 不修改数据,只标记,由下游模型决定
        pass
    else:
        raise ValueError("Unknown outlier_action")

    # 3) 诊断函数
    def diag(xarr):
        p = shapiro_p(xarr)
        sigma = sigma_rule_percentages(xarr)
        skew = stats.skew(xarr); kurt = stats.kurtosis(xarr, fisher=False)
        return {"n": np.sum(~np.isnan(xarr)), "shapiro_p": p, "sigma": sigma, "skew": skew, "kurtosis": kurt}

    before_diag = diag(x0[~mask_nan])
    after_diag = diag(x_processed[~np.isnan(x_processed)])

    # 4) 尝试变换(如果 after_diag 未满足)
    transform_info = []
    best = {"method": None, "diag": after_diag, "data": x_processed}
    # 判断是否需要变换:shapiro_p 非空但小于0.05 或 sigma 与理论差距大
    def sigma_diff_ok(sigma_dict, tol_pct=5.0):
        # 比较 empirical(%) vs theoretical percentages
        thr = {"±1σ": 68.27, "±2σ": 95.45, "±3σ": 99.73}
        diffs = [abs(sigma_dict[k] - thr[k]) for k in thr]
        return all(d <= tol_pct for d in diffs)

    need_transform = True
    if (after_diag["shapiro_p"] is not None and after_diag["shapiro_p"] > 0.05) and sigma_diff_ok(after_diag["sigma"]):
        need_transform = False

    if need_transform:
        for method in transform_try:
            try:
                xt, info = apply_transformations(x_processed if outlier_action != "trim" else x_processed, method)
            except Exception as e:
                transform_info.append({"method": method, "error": str(e)})
                continue
            d = diag(xt)
            transform_info.append({"method": method, "diag": d, "info": info})
            # 选取第一个满足 shapiro_p>0.05 或者 sigma_rule 接近的变换结果作为最佳
            if (d["shapiro_p"] is not None and d["shapiro_p"] > 0.05) or sigma_diff_ok(d["sigma"]):
                best = {"method": method, "diag": d, "data": xt}
                break

    result = {
        "n_total": int(n_total),
        "n_outliers_detected": n_outliers,
        "outlier_indices": np.where(out_mask)[0].tolist(),
        "before_diag": before_diag,
        "after_diag": after_diag,
        "outlier_action": outlier_action,
        "transforms_tested": transform_info,
        "best": best,
        "removed_indices": removed_idx
    }
    return result

# ----------------- 使用示例 -----------------
if __name__ == "__main__":
    np.random.seed(42)
    # 构造示例:正态噪声 + 一些极端值
    data = np.concatenate([np.random.normal(0, 1, 2000), np.array([10, 12, -9, 15])])
    res = robust_normalize_pipeline(
        data,
        outlier_method="robust_z",
        outlier_action="winsorize",
        transform_try=["yeojohnson", "log"]
    )
    import pprint; pprint.pprint(res)

使用建议(对训练数据的具体建议)

  1. 优先尝试 winsorize(截尾):训练数据中少量极端值常常是噪声,用截尾(例如 1%/99%)能稳健改善统计性质而不删数据。
  2. 保留原始列:在数据集中保留原始未处理的列,以及处理后列(比如 feature, feature_winsorized, feature_yeojohnson),以便比较对模型效果的影响。
  3. 若极端值来自真实信号(不是噪声):考虑使用稳健模型或厚尾分布(Student-t),或对损失函数做鲁棒化(Huber、Quantile loss)。
  4. 评估下游影响:任何变换都应以提升下游模型表现或满足方法假设为目标。做 A/B(有/无处理)比较模型性能。
  5. 自动化并记录:把检测、处理、参数(如截尾分位点、变换 lambda)记录到管道日志,方便复现与回退。

好——给你一个极简、可复制、实用的函数:只返回“这次数据是否可以认为是正态分布(布尔)”,并同时给出短小的诊断理由。设计原则是:

  • 对训练数据常见的少量极端值采用可选截尾(winsorize)来降低噪声影响(默认开启)。
  • 当样本量很大且 Shapiro 不适用时,用偏度/峰度检验替代。
  • 同时用 σ 经验法则差异 做“宽松判定”——如果严格检验没过但 σ 差异小,则判为“近似正态”。

函数返回:

  • is_normal(bool)最终结论
  • reason(字符串)简短说明为什么是 / 不是正态
  • 以及关键中间值(p 值、sigma_diff 等)方便调参。

复制运行即可:

import numpy as np
from scipy import stats

def simple_normal_check(data,
                        alpha=0.05,
                        sigma_tol_pct=5.0,
                        winsorize_pct=(0.01, 0.99),
                        do_winsorize=True):
    """
    返回一个简洁判断:数据是否可认为正态分布(True/False)及简短原因。
    - data: 1D array-like
    - alpha: 显著性水平(用于统计检验)
    - sigma_tol_pct: sigma rule 与理论值允许的误差(百分比点,例如5.0 表示 ±5%)
    - winsorize_pct: (low, high) 截尾分位点(仅在 do_winsorize=True 时生效)
    - do_winsorize: 是否先对极端值做 winsorize(通常建议 True)
    """
    x = np.asarray(data).astype(float)
    x = x[~np.isnan(x)]
    n = x.size
    if n < 3:
        return {"is_normal": False, "reason": "样本量太小(<3)", "n": n}

    # 可选 winsorize(默认开启,减轻少量极端值影响)
    if do_winsorize:
        low_q = np.nanpercentile(x, winsorize_pct[0]*100)
        high_q = np.nanpercentile(x, winsorize_pct[1]*100)
        x_proc = x.copy()
        x_proc[x_proc < low_q] = low_q
        x_proc[x_proc > high_q] = high_q
    else:
        x_proc = x

    mu = np.mean(x_proc)
    sigma = np.std(x_proc, ddof=1)

    # Sigma rule empirical百分比(以百分比点表示)
    within_1 = np.mean((x_proc >= mu - sigma) & (x_proc <= mu + sigma)) * 100
    within_2 = np.mean((x_proc >= mu - 2*sigma) & (x_proc <= mu + 2*sigma)) * 100
    within_3 = np.mean((x_proc >= mu - 3*sigma) & (x_proc <= mu + 3*sigma)) * 100
    thr = {"±1σ": 68.27, "±2σ": 95.45, "±3σ": 99.73}
    diffs = {
        "±1σ": abs(within_1 - thr["±1σ"]),
        "±2σ": abs(within_2 - thr["±2σ"]),
        "±3σ": abs(within_3 - thr["±3σ"])
    }
    sigma_ok = all(d <= sigma_tol_pct for d in diffs.values())

    # 统计检验:优先 Shapiro(样本量 <= 5000),否则用偏度/峰度检验联合判断
    shapiro_p = None
    skew_p = None
    kurt_p = None
    strict_ok = None

    try:
        if 3 <= n <= 5000:
            _, shapiro_p = stats.shapiro(x_proc)
            strict_ok = (shapiro_p > alpha)
        else:
            # 当样本很大时,shapiro容易拒绝:用 skew/kurt tests
            skew_stat, skew_p = stats.skewtest(x_proc)
            kurt_stat, kurt_p = stats.kurtosistest(x_proc)
            strict_ok = (skew_p > alpha) and (kurt_p > alpha)
    except Exception as e:
        # 若任何检验出错,设为 None 并继续用 sigma_rule 决定
        strict_ok = None

    # 综合决策规则(极简)
    # 1) 如果严格检验通过 -> 正态
    # 2) 否则,如果严格检验未通过但 sigma_rule 接近 -> 近似正态(也认为通过)
    # 3) 否则 -> 非正态
    if strict_ok is True:
        is_norm = True
        reason = f"严格检验通过 (shapiro_p={shapiro_p:.4g})" if shapiro_p is not None else "严格检验通过"
    elif strict_ok is False and sigma_ok:
        is_norm = True
        reason = (f"严格检验未通过 (p={shapiro_p:.4g} 或 skew/kurt p),"
                  f" 但 sigma-rule 接近 (diffs={ {k:round(v,2) for k,v in diffs.items()} }) -> 视为近似正态")
    elif strict_ok is None and sigma_ok:
        is_norm = True
        reason = f"检验失败或不适用,但 sigma-rule 接近 -> 视为近似正态 (diffs={ {k:round(v,2) for k,v in diffs.items()} })"
    else:
        is_norm = False
        if strict_ok is False:
            reason = (f"严格检验不通过 (p={shapiro_p:.4g} 或 skew/kurt p),"
                      f"sigma diffs={ {k:round(v,2) for k,v in diffs.items()} } 超出容差 {sigma_tol_pct}%")
        else:
            reason = f"sigma diffs={ {k:round(v,2) for k,v in diffs.items()} } 超出容差 {sigma_tol_pct}%"

    return {
        "is_normal": is_norm,
        "reason": reason,
        "n": n,
        "shapiro_p": shapiro_p,
        "skew_p": skew_p,
        "kurt_p": kurt_p,
        "sigma_empirical_pct": {"±1σ": within_1, "±2σ": within_2, "±3σ": within_3},
        "sigma_diffs_pct": diffs
    }

# ---------------- 使用示例 ----------------
if __name__ == "__main__":
    import numpy as np
    np.random.seed(0)
    data = np.concatenate([np.random.normal(0,1,2000), [10,12,-9,15]])  # 有少量极端值
    res = simple_normal_check(data, alpha=0.05, sigma_tol_pct=5.0, do_winsorize=True)
    print(res)

推荐的默认设置说明

  • do_winsorize=True:对训练数据通常有好处(1%/99% 截尾)——能消除少量噪声点对统计检验的强烈影响。
  • sigma_tol_pct=5.0:允许 ±1σ/±2σ/±3σ 与理论值偏差在 5 个百分点 内就认为“接近”。你可以根据需要放宽到 8–10 或收紧到 3。
  • 当样本 > 5000 时,Shapiro 通常不适用(会因微小偏差拒绝),故改用偏度/峰度联合检验。

好的 ✅ 我帮你整理一个 极简版,满足你提的要求:

  • 不修改数据do_winsorize = False)。
  • 只返回 True/False,不返回额外信息。
  • 容差用相对误差,并且根据样本量 n 自适应调整(样本越大,要求越严格)。

代码示例

import numpy as np
from scipy import stats

def simple_normal_check(data, alpha=0.05):
    """
    极简正态性检测
    1. 使用 Shapiro-Wilk 检验
    2. 使用 sigma rule(68-95-99.7 经验法则)
    两者只要有一个通过,就认为近似正态
    """
    data = np.asarray(data).astype(float)
    data = data[~np.isnan(data)]  # 去除 NaN
    n = len(data)
    if n < 10:
        return False  # 样本太小无法判断

    # --- 方法1: Shapiro-Wilk ---
    _, p_value = stats.shapiro(data)
    if p_value > alpha:
        return True

    # --- 方法2: sigma rule ---
    mean, std = np.mean(data), np.std(data, ddof=1)
    if std == 0:
        return False

    within_1sigma = np.mean((data >= mean - std) & (data <= mean + std))
    within_2sigma = np.mean((data >= mean - 2*std) & (data <= mean + 2*std))
    within_3sigma = np.mean((data >= mean - 3*std) & (data <= mean + 3*std))

    # 期望比例
    expected = [0.68, 0.95, 0.997]
    observed = [within_1sigma, within_2sigma, within_3sigma]

    # 自适应容差(样本越大,容差越小)
    tol = 1 / np.sqrt(n)  # 例如 n=100 → 0.1 容差,n=1000 → 0.03
    if all(abs(o - e) <= tol for o, e in zip(observed, expected)):
        return True

    return False


# --- 示例 ---
np.random.seed(42)
normal_data = np.random.normal(0, 1, 500)
exp_data = np.random.exponential(1, 500)

print("Normal data:", simple_normal_check(normal_data))  # True
print("Exponential data:", simple_normal_check(exp_data))  # False

核心逻辑

  1. 先用 Shapiro-Wilk 检验(严格统计学标准)。
  2. 如果失败,再用 sigma rule + 自适应容差 兜底。
    • 样本量小 → 容差放宽
    • 样本量大 → 容差收紧

好嘞 ✅ 我给你加一个 mode 参数,支持三种模式:

  • “strict” → 只用 Shapiro-Wilk(更统计学严谨,容错率低)。
  • “loose” → 只用 sigma rule(更宽松,适合有噪声的训练数据)。
  • “auto”(默认) → 先尝试 Shapiro-Wilk,不通过时再看 sigma rule。

改进后的代码

import numpy as np
from scipy import stats

def simple_normal_check(data, alpha=0.05, mode="auto"):
    """
    极简正态性检测
    mode:
        - "strict" : 只用 Shapiro-Wilk
        - "loose"  : 只用 sigma rule
        - "auto"   : 先 Shapiro-Wilk,失败再 sigma rule
    返回 True/False
    """
    data = np.asarray(data).astype(float)
    data = data[~np.isnan(data)]  # 去除 NaN
    n = len(data)
    if n < 10:
        return False  # 样本太小无法判断

    # --- 方法1: Shapiro-Wilk ---
    def check_shapiro(x):
        _, p_value = stats.shapiro(x)
        return p_value > alpha

    # --- 方法2: sigma rule ---
    def check_sigma(x):
        mean, std = np.mean(x), np.std(x, ddof=1)
        if std == 0:
            return False

        within_1sigma = np.mean((x >= mean - std) & (x <= mean + std))
        within_2sigma = np.mean((x >= mean - 2*std) & (x <= mean + 2*std))
        within_3sigma = np.mean((x >= mean - 3*std) & (x <= mean + 3*std))

        expected = [0.68, 0.95, 0.997]
        observed = [within_1sigma, within_2sigma, within_3sigma]

        # 自适应容差(样本越大越严格)
        tol = 1 / np.sqrt(n)
        return all(abs(o - e) <= tol for o, e in zip(observed, expected))

    # --- 模式选择 ---
    if mode == "strict":
        return check_shapiro(data)
    elif mode == "loose":
        return check_sigma(data)
    elif mode == "auto":
        return check_shapiro(data) or check_sigma(data)
    else:
        raise ValueError("mode must be 'strict', 'loose', or 'auto'")


# --- 示例 ---
np.random.seed(42)
normal_data = np.random.normal(0, 1, 500)
exp_data = np.random.exponential(1, 500)

print("Normal data (strict):", simple_normal_check(normal_data, mode="strict"))
print("Normal data (loose):", simple_normal_check(normal_data, mode="loose"))
print("Exponential data (auto):", simple_normal_check(exp_data, mode="auto"))

示例输出

Normal data (strict): True
Normal data (loose): True
Exponential data (auto): False

👌我理解了,你需要一个更稳健的自适应容差,避免以下两个问题:

  1. 数据量太少 → 容差太大,导致几乎任何分布都“通过”。
  2. 数据量很大但带有异常值 → 容差太小,导致明明近似正态却判定失败。

改进方案

我建议综合以下因素来设定容差:

  1. 基础误差base_tol):保证即便数据量很小,也有一个最小容差(避免“过严”)。
  2. 样本量影响1/√n):样本量越大,容差越小(标准统计学依据)。
  3. 数据分布稳定性(用偏度 skew 和峰度 kurtosis):
    • 如果 |skew| < 1|kurtosis| < 3,说明数据没有明显偏态/厚尾 → 放宽容差。
    • 如果严重偏态或厚尾 → 收紧容差。

最终容差公式:

tol=base_tol+cn×adjtol = base_tol + \frac{c}{\sqrt{n}} \times adj

  • base_tol = 0.03 (保证小样本也能判断)
  • c = 1.0(缩放因子,可调)
  • adj = 0.5~2.0(根据偏度/峰度调整)

改进后的代码

import numpy as np
from scipy import stats

def simple_normal_check(data, alpha=0.05, mode="auto"):
    """
    极简正态性检测(增强自适应容差版)
    mode:
        - "strict" : 只用 Shapiro-Wilk
        - "loose"  : 只用 sigma rule
        - "auto"   : 先 Shapiro-Wilk,失败再 sigma rule
    """
    data = np.asarray(data).astype(float)
    data = data[~np.isnan(data)]
    n = len(data)
    if n < 10:
        return False  # 样本太小无法判断

    # --- 方法1: Shapiro-Wilk ---
    def check_shapiro(x):
        _, p_value = stats.shapiro(x)
        return p_value > alpha

    # --- 方法2: sigma rule ---
    def check_sigma(x):
        mean, std = np.mean(x), np.std(x, ddof=1)
        if std == 0:
            return False

        within_1sigma = np.mean((x >= mean - std) & (x <= mean + std))
        within_2sigma = np.mean((x >= mean - 2*std) & (x <= mean + 2*std))
        within_3sigma = np.mean((x >= mean - 3*std) & (x <= mean + 3*std))

        expected = [0.68, 0.95, 0.997]
        observed = [within_1sigma, within_2sigma, within_3sigma]

        # --- 自适应容差 ---
        skew = stats.skew(x)
        kurt = stats.kurtosis(x)
        adj = 1.0
        if abs(skew) < 1 and abs(kurt) < 3:
            adj = 1.5  # 数据较稳定 → 放宽
        elif abs(skew) > 2 or abs(kurt) > 7:
            adj = 0.7  # 明显偏态/厚尾 → 收紧

        base_tol = 0.03
        tol = base_tol + (1.0 / np.sqrt(n)) * adj

        return all(abs(o - e) <= tol for o, e in zip(observed, expected))

    # --- 模式选择 ---
    if mode == "strict":
        return check_shapiro(data)
    elif mode == "loose":
        return check_sigma(data)
    elif mode == "auto":
        return check_shapiro(data) or check_sigma(data)
    else:
        raise ValueError("mode must be 'strict', 'loose', or 'auto'")

改进点

  1. 小样本时,至少保留 ±3% 容差,避免过于严格。
  2. 大样本时,容差仍然收紧,但不会无限接近 0。
  3. 数据偏态/厚尾时,容差自动调整(偏离越严重,要求越严格)。

👌 那我帮你写一个完整的 测试脚本,分别用不同分布的数据来跑你现在的 simple_normal_check,看看在 strict / loose / auto 三种模式下的表现。


测试脚本

import numpy as np
from scipy import stats

# 假设 simple_normal_check 已经定义在这里(直接用你上一个版本的函数)

def test_normality_checker():
    np.random.seed(42)

    datasets = {
        "正态分布 (N(0,1))": np.random.normal(0, 1, 500),
        "轻微偏态 (对数正态)": np.random.lognormal(0, 0.2, 500),  # 偏态轻
        "强偏态 (指数分布)": np.random.exponential(1, 500),       # 偏态重
        "厚尾分布 (t df=2)": stats.t(df=2).rvs(500),             # heavy tail
        "均匀分布 (U[0,1])": np.random.uniform(0, 1, 500)        # 非正态
    }

    modes = ["strict", "loose", "auto"]

    for name, data in datasets.items():
        print(f"\n{name}:")
        for mode in modes:
            result = simple_normal_check(data, mode=mode)
            print(f"  {mode:<6}{result}")

if __name__ == "__main__":
    test_normality_checker()

预期结果解释

  • 正态分布 (N(0,1))
    • strict → True(Shapiro 检验通过)
    • loose → True(sigma rule 通过)
    • auto → True
  • 轻微偏态 (对数正态)
    • strict → False(Shapiro 很敏感)
    • loose → True(sigma rule 容忍度高)
    • auto → True(兜底成功)
  • 强偏态 (指数分布)
    • strict → False
    • loose → False(sigma rule 也偏差大)
    • auto → False
  • 厚尾分布 (t 分布,自由度2)
    • strict → False
    • loose → False(容差收紧,判定失败)
    • auto → False
  • 均匀分布
    • strict → False
    • loose → False
    • auto → False