未记录事实核验 · 状态:现行内容
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适用于学习IQR计算、离散程度比较、分位数分层和异常候选筛查,代码示例按正文顺序复用导入、数据及函数,不是生产级数据质量评估或在线预警方案。围栏外观测仅是待复核候选,不等于错误数据或应删除的数据;评分及等级是人为规则演示,IQR/中位数的相对变异解释仅适用于中位数为正且远离零的适当指标,示例未覆盖空输入、缺失值和退化分布。有限样本及存在重复值时,分位数阈值不保证对应固定人数比例;k值建议也不构成业务效果保证。滚动窗口包含当前观测,适用于回顾性筛查;前29个观测已单列为未判定,异常候选率仅以有效窗口观测计算。
📏 四分位距(Interquartile Range, IQR)
abstract 📋 本节概览
- 🎯 学习目标:掌握四分位距的定义、计算和应用
- 📚 前置知识:分位数|分位数 | 离散程度指标|离散程度指标
- ⏱️ 预计用时:8分钟
🤔 什么是四分位距?
note 📖 **四分位距的定义**
四分位距(IQR)是第三四分位数(Q3)与第一四分位数(Q1)之间的差值,表示中间50%数据的分散程度。
公式:$IQR = Q_3 - Q_1$
生活例子:
- 班级成绩的四分位距告诉我们"中等学生"的成绩差距有多大
- 员工工资的四分位距反映"普通员工"间的薪资差异
- 房价的四分位距显示"主流房源"的价格跨度
📊 四分位距的计算
基本计算步骤
- 将数据从小到大排序
- 找到Q1(第25百分位数)
- 找到Q3(第75百分位数)
- 计算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()
🔍 四分位距的优缺点
✅ 优点
- 抗异常值:相较极差和标准差,通常对尾部极端值更稳健,但并非完全不受异常值或样本构成变化影响
- 直观易懂:表示中间50%数据的分散程度
- 适用性广:适用于各种分布形状
- 计算简单:只需要两个分位数
- 业务意义清晰:便于制定分层策略
❌ 缺点
- 信息损失:忽略了25%和75%以外的数据
- 效率较低:在正态分布下效率不如标准差
- 离散程度描述不完整:只反映中间部分的变异
- 不适合代数运算:无法像方差那样进行合并计算
🛠️ 实用技巧
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%}")
🔗 相关知识
#统计学 #四分位距 #离散程度 #异常值检测 #分位数 #基础