github star gitee star atomgit star PyPI Downloads AI 编程 AI 交流群

大家好,我是正在实战各种AI项目的程序员晚枫。

欢迎来到统计分析基础部分!

今天学习描述性统计,这是用数字概括数据特征的方法。不需要复杂的数学,掌握几个关键指标,你就能快速理解任何数据集。


为什么需要描述性统计?

想象你面前有一组客户的年龄数据:

1
[25, 28, 32, 35, 38, 42, 45, 48, 52, 55, 58, 62, 65, 68, 72]

直接看这些数字很费劲。但如果告诉你:

  • 平均年龄:47.3岁
  • 中位数:48岁
  • 标准差:14.2岁

是不是立刻有了概念?这就是描述性统计的力量。


6大核心指标

1. 集中趋势(数据集中在哪)

均值(Mean)

平均值,所有数据的总和除以个数。

1
2
3
4
5
6
7
8
9
10
import numpy as np
import pandas as pd

data = [25, 28, 32, 35, 38, 42, 45, 48, 52, 55]
mean = np.mean(data)
print(f"均值: {mean:.1f}")

# 注意:均值对异常值敏感
outlier_data = [25, 28, 32, 35, 38, 42, 45, 48, 52, 1000] # 有个极端值
print(f"有异常值的均值: {np.mean(outlier_data):.1f}") # 被拉高了

中位数(Median)

排序后位于中间的值,不受异常值影响。

1
2
3
4
5
median = np.median(data)
print(f"中位数: {median}")

# 有异常值时更稳健
print(f"有异常值的中位数: {np.median(outlier_data)}") # 几乎不变

众数(Mode)

出现次数最多的值。

1
2
3
4
from scipy import stats

mode_result = stats.mode([1, 2, 2, 3, 3, 3, 4])
print(f"众数: {mode_result.mode[0]}, 出现次数: {mode_result.count[0]}")

什么时候用什么?

  • 数据对称分布 → 用均值
  • 有异常值或偏态分布 → 用中位数
  • 类别数据 → 用众数

2. 离散程度(数据有多分散)

极差(Range)

最大值减最小值。

1
2
range_val = np.max(data) - np.min(data)
print(f"极差: {range_val}")

方差(Variance)

每个数据与均值差的平方的平均。

1
2
variance = np.var(data, ddof=1)  # ddof=1表示样本方差
print(f"方差: {variance:.2f}")

标准差(Standard Deviation)

方差的平方根,与原始数据同单位。

1
2
3
4
5
6
7
std = np.std(data, ddof=1)
print(f"标准差: {std:.2f}")

# 经验法则(正态分布)
# 约68%的数据在均值±1个标准差内
# 约95%的数据在均值±2个标准差内
# 约99.7%的数据在均值±3个标准差内

变异系数(CV)

标准差除以均值,用于比较不同量纲数据的离散程度。

1
2
cv = std / mean
print(f"变异系数: {cv:.2%}")

3. 分布形态(数据长什么样)

百分位数(Percentile)

1
2
3
4
5
6
7
# 四分位数
q1 = np.percentile(data, 25) # 第一四分位数(25%分位)
q2 = np.percentile(data, 50) # 中位数(50%分位)
q3 = np.percentile(data, 75) # 第三四分位数(75%分位)

iqr = q3 - q1 # 四分位距
print(f"Q1: {q1}, Q2: {q2}, Q3: {q3}, IQR: {iqr}")

偏度(Skewness)

衡量分布的不对称性。

1
2
3
4
5
6
7
8
9
from scipy import stats

skewness = stats.skew(data)
print(f"偏度: {skewness:.2f}")

# 解释:
# > 0:右偏(长尾在右)
# < 0:左偏(长尾在左)
# ≈ 0:对称分布

峰度(Kurtosis)

衡量分布的尖锐程度。

1
2
3
4
5
6
7
kurtosis = stats.kurtosis(data)
print(f"峰度: {kurtosis:.2f}")

# 解释:
# > 0:比正态分布更尖(厚尾)
# < 0:比正态分布更平(薄尾)
# ≈ 0:接近正态分布

Pandas一键描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd

df = pd.DataFrame({
'年龄': [25, 28, 32, 35, 38, 42, 45, 48, 52, 55],
'收入': [5000, 6000, 7500, 8000, 9000, 10000, 12000, 15000, 18000, 20000]
})

# 一键生成描述性统计
print(df.describe())

# 包含更多指标
print(df.describe(percentiles=[0.05, 0.25, 0.5, 0.75, 0.95]))

# 单独列的详细统计
print(df['年龄'].describe())

实战:完整的数据画像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import pandas as pd
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

# 模拟用户数据
np.random.seed(42)
users = pd.DataFrame({
'年龄': np.random.normal(35, 10, 1000).astype(int),
'消费金额': np.random.lognormal(8, 0.5, 1000)
})

print("=== 用户数据画像 ===\n")

# 1. 基础统计
print("【基础统计】")
print(users.describe())

# 2. 自定义报告
def data_profile(series, name):
print(f"\n【{name}分析】")
print(f" 样本量: {len(series)}")
print(f" 均值: {series.mean():.2f}")
print(f" 中位数: {series.median():.2f}")
print(f" 标准差: {series.std():.2f}")
print(f" 变异系数: {(series.std()/series.mean()):.2%}")
print(f" 偏度: {stats.skew(series):.2f}")
print(f" 峰度: {stats.kurtosis(series):.2f}")
print(f" 最小值: {series.min():.2f}")
print(f" 最大值: {series.max():.2f}")
print(f" 极差: {series.max()-series.min():.2f}")

data_profile(users['年龄'], '年龄')
data_profile(users['消费金额'], '消费金额')

# 3. 可视化
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 年龄分布
axes[0, 0].hist(users['年龄'], bins=30, edgecolor='black', alpha=0.7)
axes[0, 0].axvline(users['年龄'].mean(), color='red', linestyle='--', label=f'均值={users["年龄"].mean():.1f}')
axes[0, 0].axvline(users['年龄'].median(), color='green', linestyle='--', label=f'中位数={users["年龄"].median():.1f}')
axes[0, 0].set_title('年龄分布')
axes[0, 0].legend()

# 消费金额分布(对数刻度)
axes[0, 1].hist(np.log(users['消费金额']), bins=30, edgecolor='black', alpha=0.7, color='orange')
axes[0, 1].set_title('消费金额分布(对数)')

# 箱线图
axes[1, 0].boxplot([users['年龄'], users['消费金额']/100], labels=['年龄', '消费金额/100'])
axes[1, 0].set_title('箱线图对比')

# QQ图(检验正态性)
stats.probplot(users['年龄'], dist="norm", plot=axes[1, 1])
axes[1, 1].set_title('QQ图(年龄)')

plt.tight_layout()
plt.savefig('data_profile.png', dpi=300)
plt.show()

性能对比:Pandas vs NumPy vs 手动计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
import numpy as np
import time

data = np.random.randn(1000000)
s = pd.Series(data)

# Pandas
%timeit s.describe() # 约5ms

# NumPy
%timeit np.mean(data), np.std(data), np.median(data) # 约3ms

# 手动循环
%timeit sum(data)/len(data) # 约100ms

进阶用法

分组描述统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 一行代码完成分组统计
df.groupby('category').describe()

# 自定义统计指标
def custom_stats(x):
return pd.Series({
'mean': x.mean(),
'median': x.median(),
'std': x.std(),
'cv': x.std() / x.mean() if x.mean() != 0 else np.nan, # 变异系数
'skew': x.skew(), # 偏度
'kurt': x.kurt(), # 峰度
'iqr': x.quantile(0.75) - x.quantile(0.25) # 四分位距
})

df.groupby('category')['value'].apply(custom_stats)

相关性分析

1
2
3
4
5
6
7
8
9
# 皮尔逊相关系数(线性关系)
df.corr()

# 斯皮尔曼相关系数(单调关系,对异常值鲁棒)
df.corr(method='spearman')

# 可视化相关性
import seaborn as sns
sns.heatmap(df.corr(), annot=True, cmap='RdYlBu_r', center=0)

避坑指南

❌ 坑1:均值被异常值带偏

1
2
3
4
5
6
7
data = [1, 2, 3, 4, 5, 100]  # 100是异常值
print(f"均值: {np.mean(data):.1f}") # 19.2,被100拉高
print(f"中位数: {np.median(data):.1f}") # 3.5,不受影响

# 用截尾均值(去掉最高最低5%再求平均)
from scipy import stats
print(f"截尾均值: {stats.trim_mean(data, 0.1):.1f}") # 3.5

❌ 坑2:相关≠因果

1
2
3
4
5
# 高相关性不代表因果关系
# 比如:冰淇淋销量和溺水人数高度相关
# 真实原因:夏天天气热 → 游泳多 + 冰淇淋多

# 检验因果需要:A/B测试、工具变量、断点回归等

实战案例:分析房产价格数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import pandas as pd
import numpy as np

np.random.seed(42)
n = 5000
df = pd.DataFrame({
'area': np.random.uniform(50, 200, n).round(1),
'price': np.random.uniform(50, 500, n).round(2),
'rooms': np.random.choice([1, 2, 3, 4, 5], n, p=[0.05, 0.15, 0.4, 0.3, 0.1]),
'floor': np.random.randint(1, 35, n),
'year': np.random.randint(1990, 2025, n),
'district': np.random.choice(['浦东', '静安', '徐汇', '黄浦', '长宁'], n)
})

# 单价
df['unit_price'] = (df['price'] * 10000 / df['area']).round(0)

# 1. 总体描述统计
print("=== 房价描述统计 ===")
print(df[['area', 'unit_price', 'rooms', 'floor', 'year']].describe().round(1))

# 2. 各区域统计
print("\n=== 各区域单价统计 ===")
district_stats = df.groupby('district')['unit_price'].agg(['mean', 'median', 'std', 'count'])
print(district_stats.round(0))

# 3. 相关性
print("\n=== 相关性矩阵 ===")
print(df[['area', 'unit_price', 'rooms', 'floor', 'year']].corr().round(2))

描述性统计速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pandas as pd
import numpy as np

# 一行代码获取所有统计量
df.describe()
df.describe(include='all') # 包括非数值列

# 单独计算
df['col'].mean() # 均值
df['col'].median() # 中位数
df['col'].mode() # 众数
df['col'].std() # 标准差
df['col'].var() # 方差
df['col'].min() # 最小值
df['col'].max() # 最大值
df['col'].quantile(0.25) # Q1
df['col'].quantile(0.75) # Q3
df['col'].skew() # 偏度
df['col'].kurt() # 峰度
df['col'].mad() # 平均绝对偏差
df['col'].sem() # 标准误差

# 分组描述统计
df.groupby('category')['value'].describe()

# 多列同时统计
df.agg({'A': ['mean', 'std'], 'B': ['median', 'min', 'max']})

可视化描述统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import seaborn as sns
import matplotlib.pyplot as plt

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

# 直方图 + 核密度
sns.histplot(df['value'], kde=True, ax=axes[0,0])

# 箱线图
sns.boxplot(data=df, x='category', y='value', ax=axes[0,1])

# 小提琴图(箱线图+分布)
sns.violinplot(data=df, x='category', y='value', ax=axes[1,0])

# 相关性热力图
sns.heatmap(df.corr(), annot=True, cmap='RdYlBu_r', ax=axes[1,1])

plt.tight_layout()

描述性统计实战模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import pandas as pd
import numpy as np

def data_profile(df, target_col=None):
'''一键数据画像报告'''
print("=" * 60)
print("数据画像报告")
print("=" * 60)

# 1. 基本信息k
print(f"
📊 基本信息")
print(f" 数据量: {len(df):,} 行, {len(df.columns)} 列")
print(f" 缺失率: {df.isnull().mean().mean()*100:.1f}%")
print(f" 重复率: {df.duplicated().mean()*100:.1f}%")

# 2. 数值列统计
numeric_cols = df.select_dtypes(include=[np.number]).columns
if len(numeric_cols) > 0:
print(f"
📈 数值列统计 ({len(numeric_cols)}列)")
for col in numeric_cols[:5]: # 最多展示5列
print(f" {col}:")
print(f" 均值={df[col].mean():.2f}, 中位数={df[col].median():.2f}")
print(f" 标准差={df[col].std():.2f}, 偏度={df[col].skew():.2f}")

# 3. 分类列统计
cat_cols = df.select_dtypes(include=['object', 'category']).columns
if len(cat_cols) > 0:
print(f"
📋 分类列统计 ({len(cat_cols)}列)")
for col in cat_cols[:5]:
print(f" {col}: {df[col].nunique()}个唯一值, 最常见={df[col].mode()[0]}")

# 4. 目标变量分析
if target_col and target_col in df.columns:
print(f"
🎯 目标变量: {target_col}")
print(df[target_col].describe())

# 使用
data_profile(df, target_col='salary')

统计报告模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 生成统计报告摘要
def stat_report(df, group_col, value_col):
'''按分组生成统计报告'''
report = df.groupby(group_col)[value_col].agg([
('样本量', 'count'),
('均值', 'mean'),
('中位数', 'median'),
('标准差', 'std'),
('最小值', 'min'),
('最大值', 'max'),
('Q1', lambda x: x.quantile(0.25)),
('Q3', lambda x: x.quantile(0.75)),
]).round(2)

report['变异系数'] = (report['标准差'] / report['均值'] * 100).round(1)
report['IQR'] = report['Q3'] - report['Q1']

return report

# 使用
result = stat_report(df, 'city', 'salary')
print(result)

下节预告

下一课我们将学习假设检验入门,学会用数据验证猜想。

👉 继续阅读:假设检验入门-用数据验证你的猜想


💬 加入学习交流群

扫码加入Python学习交流群,和数千名同学一起进步:

👉 点击加入交流群

群里不定期分享:

  • 数据分析实战案例
  • Python学习资料
  • 求职面试经验
  • 行业最新动态

推荐:AI Python数据分析实战营

🎁 限时福利:送《利用Python进行数据分析》实体书

👉 点击了解详情


课程导航

上一篇: 交互式可视化-pyecharts制作动态图表

下一篇: 假设检验入门-用数据验证你的猜想


PS:描述性统计是数据分析的基础。记住这6个指标:均值、中位数、标准差、分位数、偏度、峰度。



📚 推荐教材

主教材《Excel+Python 飞速搞定数据分析与处理(图灵出品)》

💬 联系我

平台账号/链接
微信扫码加好友
微博@程序员晚枫
知乎@程序员晚枫
抖音@程序员晚枫
小红书@程序员晚枫
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!