未记录事实核验 · 状态:现行内容

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用边界与维护说明

本文适用于学习IQR计算、离散程度比较、分位数分层和异常候选筛查,代码示例按正文顺序复用导入、数据及函数,不是生产级数据质量评估或在线预警方案。围栏外观测仅是待复核候选,不等于错误数据或应删除的数据;评分及等级是人为规则演示,IQR/中位数的相对变异解释仅适用于中位数为正且远离零的适当指标,示例未覆盖空输入、缺失值和退化分布。有限样本及存在重复值时,分位数阈值不保证对应固定人数比例;k值建议也不构成业务效果保证。滚动窗口包含当前观测,适用于回顾性筛查;前29个观测已单列为未判定,异常候选率仅以有效窗口观测计算。

📏 四分位距(Interquartile Range, IQR)

abstract 📋 本节概览

  • 🎯 学习目标:掌握四分位距的定义、计算和应用
  • 📚 前置知识:分位数|分位数 | 离散程度指标|离散程度指标
  • ⏱️ 预计用时:8分钟

🤔 什么是四分位距?

note 📖 **四分位距的定义**

四分位距(IQR)是第三四分位数(Q3)与第一四分位数(Q1)之间的差值,表示中间50%数据的分散程度。

公式:$IQR = Q_3 - Q_1$

生活例子:

  • 班级成绩的四分位距告诉我们"中等学生"的成绩差距有多大
  • 员工工资的四分位距反映"普通员工"间的薪资差异
  • 房价的四分位距显示"主流房源"的价格跨度

📊 四分位距的计算

基本计算步骤

  1. 将数据从小到大排序
  2. 找到Q1(第25百分位数)
  3. 找到Q3(第75百分位数)
  4. 计算IQR = Q3 - Q1
import numpy as np
import matplotlib.pyplot as plt

# 示例数据:学生考试成绩
scores = [65, 70, 72, 75, 78, 80, 82, 85, 88, 90, 92, 95, 98]

# 计算四分位数
Q1 = np.percentile(scores, 25)
Q2 = np.percentile(scores, 50)  # 中位数
Q3 = np.percentile(scores, 75)

# 计算四分位距
IQR = Q3 - Q1

print(f"数据: {scores}")
print(f"Q1 (第一四分位数): {Q1}")
print(f"Q2 (中位数): {Q2}")
print(f"Q3 (第三四分位数): {Q3}")
print(f"IQR (四分位距): {IQR}")

print(f"\n数据分布解释:")
print(f"第25百分位数: {Q1}")
print(f"第50百分位数(中位数): {Q2}")
print(f"第75百分位数: {Q3}")
print(f"第25至第75百分位数的阈值跨度为{IQR}分;有限样本中落在该区间的人数比例不一定恰好为50%")

💻 Python实现方法

方法1:使用NumPy

import numpy as np

def calculate_iqr_numpy(data):
    """使用NumPy计算四分位距"""
    Q1 = np.percentile(data, 25)
    Q3 = np.percentile(data, 75)
    IQR = Q3 - Q1
    return Q1, Q3, IQR

# 示例数据
data = [10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]
Q1, Q3, IQR = calculate_iqr_numpy(data)

print(f"NumPy方法:")
print(f"Q1: {Q1}, Q3: {Q3}, IQR: {IQR}")

方法2:使用Pandas

import pandas as pd

def calculate_iqr_pandas(data):
    """使用Pandas计算四分位距"""
    series = pd.Series(data)
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    return Q1, Q3, IQR

# 使用describe()方法快速查看
series = pd.Series(data)
desc = series.describe()
print(f"\nPandas describe()结果:")
print(desc)

Q1_pd, Q3_pd, IQR_pd = calculate_iqr_pandas(data)
print(f"\nPandas方法:")
print(f"Q1: {Q1_pd}, Q3: {Q3_pd}, IQR: {IQR_pd}")

方法3:使用scipy.stats

from scipy import stats

def calculate_iqr_scipy(data):
    """使用scipy.stats计算四分位距"""
    IQR = stats.iqr(data)
    Q1 = np.percentile(data, 25)
    Q3 = np.percentile(data, 75)
    return Q1, Q3, IQR

Q1_sp, Q3_sp, IQR_sp = calculate_iqr_scipy(data)
print(f"\nSciPy方法:")
print(f"Q1: {Q1_sp}, Q3: {Q3_sp}, IQR: {IQR_sp}")

# 直接使用iqr函数
IQR_direct = stats.iqr(data)
print(f"直接计算IQR: {IQR_direct}")

📈 四分位距的特性

1. 抗异常值特性

import numpy as np
import matplotlib.pyplot as plt

# 正常数据
normal_data = [20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40]

# 含异常值数据
outlier_data = [20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 200]  # 200是异常值

def compare_dispersion_measures(data, label):
    """比较不同离散程度指标"""
    std = np.std(data, ddof=1)
    IQR = stats.iqr(data)
    range_val = np.max(data) - np.min(data)
    
    print(f"{label}:")
    print(f"  标准差: {std:.2f}")
    print(f"  四分位距: {IQR:.2f}")
    print(f"  极差: {range_val:.2f}")
    return std, IQR, range_val

print("抗异常值能力对比:")
std1, iqr1, range1 = compare_dispersion_measures(normal_data, "正常数据")
std2, iqr2, range2 = compare_dispersion_measures(outlier_data, "含异常值数据")

print(f"\n异常值的影响:")
print(f"标准差变化: {((std2-std1)/std1*100):+.1f}%")
print(f"四分位距变化: {((iqr2-iqr1)/iqr1*100):+.1f}%")
print(f"极差变化: {((range2-range1)/range1*100):+.1f}%")

# 可视化对比
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

# 箱线图对比
ax1.boxplot([normal_data, outlier_data], labels=['正常数据', '含异常值'])
ax1.set_title('箱线图对比')
ax1.set_ylabel('数值')

# 指标变化对比
measures = ['标准差', '四分位距', '极差']
changes = [((std2-std1)/std1*100), ((iqr2-iqr1)/iqr1*100), ((range2-range1)/range1*100)]
colors = ['red' if x > 50 else 'orange' if x > 10 else 'green' for x in changes]

ax2.bar(measures, changes, color=colors)
ax2.set_title('异常值对离散程度指标的影响')
ax2.set_ylabel('变化百分比 (%)')
ax2.axhline(y=0, color='black', linestyle='-', alpha=0.3)

for i, v in enumerate(changes):
    ax2.text(i, v + 5, f'{v:+.1f}%', ha='center', va='bottom')

plt.tight_layout()
plt.show()

2. 不同分布的四分位距

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

# 生成不同分布的数据
np.random.seed(42)
distributions = {
    '正态分布': np.random.normal(50, 10, 1000),
    '均匀分布': np.random.uniform(30, 70, 1000),
    '指数分布': np.random.exponential(15, 1000) + 20,
    '偏态分布': stats.skewnorm.rvs(a=5, loc=40, scale=10, size=1000)
}

fig, axes = plt.subplots(2, 2, figsize=(12, 10))
axes = axes.flatten()

for i, (name, data) in enumerate(distributions.items()):
 # 计算统计量
    Q1 = np.percentile(data, 25)
    Q2 = np.percentile(data, 50)
    Q3 = np.percentile(data, 75)
    IQR = Q3 - Q1
    std = np.std(data, ddof=1)
    
 # 绘制直方图和四分位数线
    axes[i].hist(data, bins=30, alpha=0.7, density=True)
    axes[i].axvline(Q1, color='red', linestyle='--', label=f'Q1={Q1:.1f}')
    axes[i].axvline(Q2, color='green', linestyle='-', label=f'Q2={Q2:.1f}')
    axes[i].axvline(Q3, color='red', linestyle='--', label=f'Q3={Q3:.1f}')
    
    axes[i].set_title(f'{name}\nIQR={IQR:.1f}, σ={std:.1f}')
    axes[i].legend()

plt.tight_layout()
plt.show()

# 打印各分布的IQR/σ比值
print("不同分布的IQR与标准差比值:")
for name, data in distributions.items():
    IQR = stats.iqr(data)
    std = np.std(data, ddof=1)
    ratio = IQR / std
    print(f"{name}: IQR/σ = {ratio:.2f}")

🎯 四分位距的应用

1. 异常值检测(IQR法则)

def detect_outliers_iqr(data, k=1.5):
    """
    使用IQR方法检测异常值
    k=1.5: 温和异常值
    k=3.0: 极端异常值
    """
    Q1 = np.percentile(data, 25)
    Q3 = np.percentile(data, 75)
    IQR = Q3 - Q1
    
 # 定义异常值边界
    lower_bound = Q1 - k * IQR
    upper_bound = Q3 + k * IQR
    
 # 识别异常值
    outliers = []
    normal_data = []
    
    for value in data:
        if value < lower_bound or value > upper_bound:
            outliers.append(value)
        else:
            normal_data.append(value)
    
    return {
        'outliers': outliers,
        'normal_data': normal_data,
        'lower_bound': lower_bound,
        'upper_bound': upper_bound,
        'Q1': Q1,
        'Q3': Q3,
        'IQR': IQR
    }

# 示例:员工工资数据
salaries = [3000, 3200, 3500, 3800, 4000, 4200, 4500, 4800, 
           5000, 5200, 5500, 8000, 12000, 25000]

# 检测异常值
result = detect_outliers_iqr(salaries, k=1.5)

print(f"工资异常值检测结果:")
print(f"Q1: {result['Q1']:,.0f}元")
print(f"Q3: {result['Q3']:,.0f}元")
print(f"IQR: {result['IQR']:,.0f}元")
print(f"正常范围: [{result['lower_bound']:,.0f}, {result['upper_bound']:,.0f}]元")
print(f"异常值: {[f'{x:,.0f}' for x in result['outliers']]}元")
print(f"异常值占比: {len(result['outliers'])/len(salaries)*100:.1f}%")

# 可视化
plt.figure(figsize=(12, 6))

# 箱线图
plt.subplot(1, 2, 1)
plt.boxplot(salaries, vert=True)
plt.ylabel('工资 (元)')
plt.title('工资分布箱线图')

# 散点图标注异常值
plt.subplot(1, 2, 2)
normal_indices = [i for i, x in enumerate(salaries) if x in result['normal_data']]
outlier_indices = [i for i, x in enumerate(salaries) if x in result['outliers']]

plt.scatter(normal_indices, [salaries[i] for i in normal_indices], 
           color='blue', label='正常值', alpha=0.7)
plt.scatter(outlier_indices, [salaries[i] for i in outlier_indices], 
           color='red', label='异常值', s=100)

plt.axhline(result['lower_bound'], color='orange', linestyle='--', alpha=0.7, label='下边界')
plt.axhline(result['upper_bound'], color='orange', linestyle='--', alpha=0.7, label='上边界')
plt.axhline(result['Q1'], color='green', linestyle=':', alpha=0.7, label='Q1')
plt.axhline(result['Q3'], color='green', linestyle=':', alpha=0.7, label='Q3')

plt.xlabel('员工编号')
plt.ylabel('工资 (元)')
plt.title('异常值检测结果')
plt.legend()

plt.tight_layout()
plt.show()

2. 数据质量评估

def data_quality_assessment(data, label="数据"):
    """基于IQR、偏度和人为扣分阈值的分布评分演示,不是通用数据质量标准;分布偏斜或超出IQR围栏不等于数据错误,IQR/中位数的变异解释仅适用于中位数为正且远离零的适当指标。"""
    Q1 = np.percentile(data, 25)
    Q2 = np.percentile(data, 50)
    Q3 = np.percentile(data, 75)
    IQR = Q3 - Q1
    
 # 检测异常值
    outlier_result = detect_outliers_iqr(data)
    outlier_rate = len(outlier_result['outliers']) / len(data)
    
 # 数据分布评估
    mean = np.mean(data)
    skewness = stats.skew(data)
    
 # 数据质量评分
    quality_score = 100
    
 # 异常值扣分
    if outlier_rate > 0.1:  # 超过10%异常值
        quality_score -= 30
    elif outlier_rate > 0.05:  # 5%-10%异常值
        quality_score -= 20
    elif outlier_rate > 0.02:  # 2%-5%异常值
        quality_score -= 10
    
 # 偏度扣分
    if abs(skewness) > 2:  # 严重偏斜
        quality_score -= 20
    elif abs(skewness) > 1:  # 中度偏斜
        quality_score -= 10
    
 # 稳定性评估(基于IQR相对变异)
    cv_iqr = IQR / Q2 if Q2 != 0 else float('inf')
    if cv_iqr > 1:  # 高变异
        quality_score -= 15
    elif cv_iqr > 0.5:  # 中等变异
        quality_score -= 5
    
    print(f"{label}质量评估报告:")
    print(f"{'='*40}")
    print(f"样本量: {len(data)}")
    print(f"四分位数: Q1={Q1:.2f}, Q2={Q2:.2f}, Q3={Q3:.2f}")
    print(f"四分位距: {IQR:.2f}")
    print(f"异常值数量: {len(outlier_result['outliers'])} ({outlier_rate:.1%})")
    print(f"偏度: {skewness:.2f}")
    print(f"IQR变异系数: {cv_iqr:.2f}")
    print(f"示例规则评分(不代表实际数据质量): {quality_score}/100")
    
 # 质量等级
    if quality_score >= 90:
        grade = "优秀"
    elif quality_score >= 80:
        grade = "良好"
    elif quality_score >= 70:
        grade = "一般"
    else:
        grade = "较差"
    
    print(f"示例规则等级(不代表实际数据质量): {grade}")
    print(f"{'='*40}")
    
    return {
        'quality_score': quality_score,
        'grade': grade,
        'outlier_rate': outlier_rate,
        'skewness': skewness,
        'cv_iqr': cv_iqr
    }

# 评估不同质量的数据集
np.random.seed(42)

# 正态分布模拟数据(假设在示例评分规则下得分较高,并非已确认高质量)
high_quality_data = np.random.normal(100, 10, 1000)

# 正态主体与高值混合的模拟数据(假设在示例评分规则下得分居中,并非已确认中等质量)
medium_quality_data = np.concatenate([
    np.random.normal(100, 15, 950),
    np.random.uniform(150, 200, 50)  # 添加一些异常值
])

# 指数分布与高值混合的模拟数据(假设在示例评分规则下得分较低,并非已确认低质量)
low_quality_data = np.concatenate([
    np.random.exponential(20, 800),
    np.random.uniform(200, 300, 200)  # 大量异常值
])

# 评估三个数据集
data_sets = [
    (high_quality_data, "正态分布模拟数据集"),
    (medium_quality_data, "正态主体与高值混合模拟数据集"),
    (low_quality_data, "指数分布与高值混合模拟数据集")
]

results = []
for data, label in data_sets:
    result = data_quality_assessment(data, label)
    results.append((label, result))

3. 业务分析应用

# 模拟电商网站的用户行为数据
np.random.seed(42)

# 生成用户数据
user_data = {
    '用户ID': range(1, 1001),
    '月消费金额': np.random.lognormal(mean=6, sigma=0.8, size=1000),
    '月访问次数': np.random.poisson(lam=15, size=1000),
    '平均停留时长': np.random.exponential(scale=10, size=1000),
    '购买商品数量': np.random.negative_binomial(n=3, p=0.3, size=1000)
}

import pandas as pd
df = pd.DataFrame(user_data)

# 对每个指标进行四分位距分析
def analyze_business_metrics(df):
    """业务指标的四分位距分析"""
    metrics = ['月消费金额', '月访问次数', '平均停留时长', '购买商品数量']
    
    analysis_results = {}
    
    for metric in metrics:
        data = df[metric]
        
 # 基础统计量
        Q1 = data.quantile(0.25)
        Q2 = data.quantile(0.50)
        Q3 = data.quantile(0.75)
        IQR = Q3 - Q1
        
 # 用户分层
        low_users = len(data[data <= Q1])
        medium_users = len(data[(data > Q1) & (data <= Q3)])
        high_users = len(data[data > Q3])
        
 # 异常值检测
        outliers = detect_outliers_iqr(data.values)
        
        analysis_results[metric] = {
            'Q1': Q1, 'Q2': Q2, 'Q3': Q3, 'IQR': IQR,
            'low_users': low_users,
            'medium_users': medium_users,
            'high_users': high_users,
            'outliers_count': len(outliers['outliers'])
        }
        
        print(f"\n{metric}分析:")
        print(f"  Q1: {Q1:.2f} | Q2: {Q2:.2f} | Q3: {Q3:.2f} | IQR: {IQR:.2f}")
        print(f"  用户分层: 低值用户{low_users}人 | 中等用户{medium_users}人 | 高值用户{high_users}人")
        print(f"  异常用户: {len(outliers['outliers'])}人 ({len(outliers['outliers'])/len(data)*100:.1f}%)")
        
 # 业务建议
        if metric == '月消费金额':
            print(f"  💡 建议: 重点关注Q3以上用户(高消费用户),针对Q1以下用户制定激活策略")
        elif metric == '月访问次数':
            print(f"  💡 建议: 优化用户体验以提升中低频访问用户的活跃度")
        elif metric == '平均停留时长':
            print(f"  💡 建议: 分析高停留时长用户的行为特征,优化页面内容")
        else:
            print(f"  💡 建议: 分析高购买用户的偏好,推荐给其他用户群体")
    
    return analysis_results

# 执行分析
business_results = analyze_business_metrics(df)

# 可视化业务指标分布
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))
axes = [ax1, ax2, ax3, ax4]
metrics = ['月消费金额', '月访问次数', '平均停留时长', '购买商品数量']

for i, metric in enumerate(metrics):
    data = df[metric]
    
 # 箱线图
    box_plot = axes[i].boxplot(data, patch_artist=True)
    box_plot['boxes'][0].set_facecolor('lightblue')
    
 # 标注四分位数
    Q1, Q2, Q3 = data.quantile([0.25, 0.5, 0.75])
    axes[i].text(1.1, Q1, f'Q1: {Q1:.1f}', verticalalignment='center')
    axes[i].text(1.1, Q2, f'Q2: {Q2:.1f}', verticalalignment='center')
    axes[i].text(1.1, Q3, f'Q3: {Q3:.1f}', verticalalignment='center')
    
    axes[i].set_title(f'{metric}分布')
    axes[i].set_ylabel(metric)

plt.tight_layout()
plt.show()

🔍 四分位距的优缺点

✅ 优点

  1. 抗异常值:相较极差和标准差,通常对尾部极端值更稳健,但并非完全不受异常值或样本构成变化影响
  2. 直观易懂:表示中间50%数据的分散程度
  3. 适用性广:适用于各种分布形状
  4. 计算简单:只需要两个分位数
  5. 业务意义清晰:便于制定分层策略

❌ 缺点

  1. 信息损失:忽略了25%和75%以外的数据
  2. 效率较低:在正态分布下效率不如标准差
  3. 离散程度描述不完整:只反映中间部分的变异
  4. 不适合代数运算:无法像方差那样进行合并计算

🛠️ 实用技巧

1. 选择合适的k值

# 生成测试数据
np.random.seed(42)
test_data = np.concatenate([
    np.random.normal(50, 10, 900),  # 正常数据
    np.random.uniform(100, 120, 50),  # 温和异常值
    np.random.uniform(150, 200, 50)   # 极端异常值
])

k_values = [1.0, 1.5, 2.0, 2.5, 3.0]
print("不同k值的异常值检测效果:")
print("k值\t异常值数量\t异常值率\t主要检测范围")
print("-" * 50)

for k in k_values:
    result = detect_outliers_iqr(test_data, k=k)
    outlier_rate = len(result['outliers']) / len(test_data)
    
    if k == 1.5:
        detection_range = "常规异常值"
    elif k >= 3.0:
        detection_range = "极端异常值"
    else:
        detection_range = "中等异常值"
    
    print(f"{k}\t{len(result['outliers'])}\t\t{outlier_rate:.1%}\t\t{detection_range}")

print("\n💡 建议:")
print("k=1.5: 适合一般异常值检测,比较敏感")
print("k=2.0: 平衡敏感性和稳定性")
print("k=3.0: 只检测极端异常值,比较保守")

2. 时间序列中的滚动IQR

# 生成时间序列数据
dates = pd.date_range('2023-01-01', periods=365, freq='D')
np.random.seed(42)

# 基础趋势 + 季节性 + 噪声 + 偶发异常
trend = np.linspace(100, 120, 365)
seasonal = 10 * np.sin(2 * np.pi * np.arange(365) / 365.25 * 4)  # 季节性
noise = np.random.normal(0, 3, 365)

# 添加一些异常值
outlier_indices = np.random.choice(365, 20, replace=False)
outlier_values = trend + seasonal + noise
for idx in outlier_indices:
    outlier_values[idx] += np.random.choice([-20, 20]) * np.random.uniform(1, 2)

ts_data = pd.DataFrame({
    'date': dates,
    'value': outlier_values
})

# 计算滚动IQR
window = 30  # 30天滚动窗口
ts_data['rolling_Q1'] = ts_data['value'].rolling(window).quantile(0.25)
ts_data['rolling_Q3'] = ts_data['value'].rolling(window).quantile(0.75)
ts_data['rolling_IQR'] = ts_data['rolling_Q3'] - ts_data['rolling_Q1']

# 基于滚动IQR的异常检测
ts_data['lower_bound'] = ts_data['rolling_Q1'] - 1.5 * ts_data['rolling_IQR']
ts_data['upper_bound'] = ts_data['rolling_Q3'] + 1.5 * ts_data['rolling_IQR']
ts_data['has_window'] = ts_data['lower_bound'].notna() & ts_data['upper_bound'].notna()
ts_data['is_outlier'] = ts_data['has_window'] & (
    (ts_data['value'] < ts_data['lower_bound']) |
    (ts_data['value'] > ts_data['upper_bound'])
)

# 可视化
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10))

# 时间序列和异常值
normal_data = ts_data[ts_data['has_window'] & ~ts_data['is_outlier']]
outlier_data = ts_data[ts_data['is_outlier']]
unassessed_data = ts_data[~ts_data['has_window']]

ax1.plot(normal_data['date'], normal_data['value'], 'b-', alpha=0.7, label='围栏内观测')
ax1.scatter(unassessed_data['date'], unassessed_data['value'], color='gray', s=20, label='窗口不足,未判定')
ax1.scatter(outlier_data['date'], outlier_data['value'], color='red', s=50, label='异常值', zorder=5)
ax1.fill_between(ts_data['date'], ts_data['lower_bound'], ts_data['upper_bound'], 
                alpha=0.2, color='gray', label='正常范围')
ax1.set_title('基于滚动IQR的时间序列异常检测')
ax1.set_ylabel('数值')
ax1.legend()

# 滚动IQR变化
ax2.plot(ts_data['date'], ts_data['rolling_IQR'], 'g-', linewidth=2)
ax2.set_title(f'{window}天滚动IQR变化')
ax2.set_xlabel('日期')
ax2.set_ylabel('IQR')

plt.tight_layout()
plt.show()

print(f"滚动IQR异常检测结果:")
print(f"检测到异常值: {ts_data['is_outlier'].sum()}个")
print(f"有效窗口观测数: {ts_data['has_window'].sum()}个")
print(f"窗口不足、未判定观测数: {(~ts_data['has_window']).sum()}个")
print(f"有效窗口内异常候选率: {ts_data.loc[ts_data['has_window'], 'is_outlier'].mean():.1%}")

🔗 相关知识

#统计学 #四分位距 #离散程度 #异常值检测 #分位数 #基础