描述性统计与相关性分析
数据分析专题 · 用数字描述数据特征与关系
专题:Python数据分析系统学习
关键词:数据分析, 描述性统计, 相关性, Pearson, Spearman, 协方差, 偏度, 峰度, 热力图
一、引言
数据分析和数据科学的第一步,永远是了解数据。在拿到一个数据集之后,我们首先需要回答几个基本问题:数据的分布是怎样的?数据的集中趋势在哪里?数据的离散程度如何?变量之间是否存在关联?这些问题正是描述性统计与相关性分析所要回答的核心问题。
描述性统计(Descriptive Statistics)是用数字和图表来概括数据特征的方法,它是任何数据分析工作的起点。相关性分析(Correlation Analysis)则用于衡量两个或多个变量之间关系的强度和方向。本文将系统性地介绍这两大主题,从理论基础到Python实现,覆盖常用工具和进阶应用,并提供丰富的代码示例。
前置知识:建议读者具备基础的Python编程知识,熟悉NumPy和Pandas库的基本操作。本文使用Python 3.10+和常见的数据科学库。
二、描述性统计基础
描述性统计的核心目标是使用少量关键数字来概括一个数据集的主要特征。这可以分为三个维度:集中趋势、离散程度和分布形态。
2.1 集中趋势统计量
集中趋势描述的是数据向中心值集中的程度,以下是最常用的三个指标:
均值(Mean)
均值是所有数据之和除以数据个数,是最常用的集中趋势度量。但均值对异常值非常敏感,当数据中存在极端值时,均值会被"拉偏"而失去代表性。
中位数(Median)
中位数是将数据从小到大排序后处于中间位置的值。当数据个数为奇数时取中间值,为偶数时取中间两个值的平均数。中位数的优势在于不受极端值影响,因此对偏态分布的数据,中位数比均值更具代表性。
众数(Mode)
众数是数据中出现频率最高的值。对于分类变量,众数是唯一可行的集中趋势度量。一个数据集可能有一个众数(单峰)、多个众数(多峰)或没有众数。
import numpy as np
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt
import seaborn as sns
# 示例数据:某班级30名学生的数学成绩
scores = [67, 72, 85, 90, 78, 88, 92, 65, 73, 81,
55, 76, 89, 94, 70, 68, 83, 77, 86, 91,
60, 75, 84, 79, 95, 71, 69, 87, 82, 66]
# 集中趋势计算
mean_val = np.mean(scores) # 均值
median_val = np.median(scores) # 中位数
mode_val = stats.mode(scores, keepdims=True) # 众数
print(f"均值 (Mean): {mean_val:.2f}")
print(f"中位数 (Median): {median_val:.2f}")
print(f"众数 (Mode): {mode_val.mode[0]}, 出现次数: {mode_val.count[0]}")
# 输出:
# 均值 (Mean): 77.23
# 中位数 (Median): 77.50
# 众数 (Mode): 72, 出现次数: 2
使用场景对比:当数据呈对称分布时,均值约等于中位数,两者都可以使用。当数据呈偏态分布(如收入数据)时,中位数更可靠。对于分类数据(如"最喜欢的编程语言"),应使用众数。
2.2 离散程度统计量
离散程度度量的是数据值的分散情况,它回答了"数据点之间差异有多大"的问题。
标准差与方差(Standard Deviation & Variance)
方差是每个数据点与均值之差的平方的平均值,标准差是方差的正平方根。标准差与原始数据具有相同的量纲,因此更常被使用。标准差越大,说明数据越分散。
极差(Range)
极差是最大值与最小值之差,是最简单的离散度量,但极易受异常值影响。
四分位数与四分位距(Interquartile Range, IQR)
四分位数将数据分为四个等份:Q1(25%分位数)、Q2(中位数,50%分位数)、Q3(75%分位数)。四分位距 IQR = Q3 - Q1,它衡量了中间50%数据的分散程度,不受极端值影响。箱线图正是基于四分位数绘制的。
# 离散程度计算
std_val = np.std(scores, ddof=1) # 样本标准差 (ddof=1 使用无偏估计)
var_val = np.var(scores, ddof=1) # 样本方差
range_val = np.max(scores) - np.min(scores) # 极差
q1 = np.percentile(scores, 25) # 下四分位数 Q1
q2 = np.percentile(scores, 50) # 中位数 Q2
q3 = np.percentile(scores, 75) # 上四分位数 Q3
iqr = q3 - q1 # 四分位距
print(f"标准差: {std_val:.2f}")
print(f"方差: {var_val:.2f}")
print(f"极差: {range_val}")
print(f"Q1={q1}, Q2(中位数)={q2}, Q3={q3}")
print(f"四分位距 (IQR): {iqr}")
# 输出:
# 标准差: 10.23
# 方差: 104.67
# 极差: 40
# Q1=69.0, Q2(中位数)=77.5, Q3=86.5
# 四分位距 (IQR): 17.5
2.3 分布形态统计量
分布形态描述的是数据分布的对称性和尾部厚度。
偏度(Skewness)
偏度衡量数据分布的不对称程度。偏度为0表示对称分布(如正态分布);偏度大于0为正偏(右偏),尾部向右延伸;偏度小于0为负偏(左偏),尾部向左延伸。一般认为偏度的绝对值大于1时,偏态较为明显。
峰度(Kurtosis)
峰度衡量数据分布尾部的厚度。正态分布的峰度为3(Fisher峰度为0)。峰度大于3(Fisher峰度大于0)表示分布具有厚尾特征,极端值较多;峰度小于3(Fisher峰度小于0)表示分布尾部较薄。
# 偏度和峰度计算
skew_val = stats.skew(scores, bias=False) # 样本偏度 (无偏估计)
kurt_val = stats.kurtosis(scores, bias=False) # Fisher峰度 (正态分布为0)
print(f"偏度 (Skewness): {skew_val:.3f}")
print(f"峰度 (Kurtosis, Fisher): {kurt_val:.3f}")
# 解释偏度
if abs(skew_val) < 0.5:
skew_desc = "近似对称分布"
elif skew_val > 0:
skew_desc = "右偏分布 (正偏),均值大于中位数"
else:
skew_desc = "左偏分布 (负偏),均值小于中位数"
print(f"分布形态: {skew_desc}")
# 输出:
# 偏度 (Skewness): -0.124
# 峰度 (Kurtosis, Fisher): -0.875
# 分布形态: 近似对称分布
2.4 describe() 与 tabulate 一键汇总
在Python的Pandas库中,describe()方法可以一次性计算常见的描述性统计量,是数据探索中最常用的工具之一。
# 使用Pandas的describe()一键汇总
df = pd.DataFrame({'数学': scores,
'语文': [70, 78, 82, 91, 75, 85, 88, 62, 70, 79,
58, 72, 86, 90, 68, 65, 80, 74, 83, 88,
63, 71, 81, 76, 92, 69, 66, 84, 79, 64],
'英语': [65, 80, 79, 93, 72, 86, 90, 60, 75, 83,
52, 77, 87, 95, 66, 69, 82, 73, 85, 91,
61, 74, 84, 78, 96, 67, 63, 88, 76, 62]})
print("=== describe() 输出 ===")
print(df.describe())
print("\n=== 包含百分位数的完整输出 ===")
print(df.describe(percentiles=[.05, .25, .5, .75, .95]))
# 使用 tabulate 格式输出 (pandas 内置)
from pandas import option_context
with option_context('display.max_columns', 10, 'display.width', 100):
summary = df.describe().T
summary['range'] = summary['max'] - summary['min']
summary['CV'] = summary['std'] / summary['mean'] # 变异系数
print("\n=== 扩展描述统计 ===")
print(summary.round(2))
变异系数(Coefficient of Variation, CV):CV = 标准差 / 均值。这是一个无量纲的离散度量,适用于比较不同量纲或均值差异较大的数据集的离散程度。例如,比较"某股票日收益率"和"某股票日交易量"的离散程度时,标准差不可直接比较,但CV可以。
三、分组描述统计
在实际数据分析中,我们经常需要按某个分组变量来计算描述性统计量,以便进行组间比较。Pandas的groupby机制配合describe()或agg()可以完美胜任这一任务。
3.1 groupby + describe()
将分组与describe()结合,可以快速获得不同组别的描述性统计汇总。
# 创建含分组的数据
np.random.seed(42)
df_group = pd.DataFrame({
'班级': np.repeat(['A班', 'B班', 'C班'], 30),
'数学': np.concatenate([
np.random.normal(80, 8, 30), # A班: 均值80, 标准差8
np.random.normal(72, 12, 30), # B班: 均值72, 标准差12
np.random.normal(85, 5, 30) # C班: 均值85, 标准差5
]).round(1)
})
# 按班级分组并计算描述性统计
grouped_stats = df_group.groupby('班级')['数学'].describe()
print(grouped_stats)
# 等价的多列分组
print("\n=== 分组描述统计(带百分位数)===")
grouped_detailed = df_group.groupby('班级')['数学'].describe(
percentiles=[.1, .25, .5, .75, .9]
)
print(grouped_detailed)
3.2 agg() 多指标聚合
agg()方法提供了更大的灵活性,允许我们为不同列指定不同的聚合函数,也可以自定义聚合函数。
# 使用 agg() 进行多指标聚合
agg_result = df_group.groupby('班级')['数学'].agg([
('样本量', 'count'),
('均值', 'mean'),
('标准差', 'std'),
('最小值', 'min'),
('25%分位', lambda x: x.quantile(0.25)),
('中位数', 'median'),
('75%分位', lambda x: x.quantile(0.75)),
('最大值', 'max'),
('偏度', lambda x: stats.skew(x, bias=False)),
('峰度', lambda x: stats.kurtosis(x, bias=False)),
])
print(agg_result.round(2))
# 自定义聚合函数 - 计算均值的95%置信区间
def mean_ci(x):
from scipy.stats import t
n = len(x)
se = np.std(x, ddof=1) / np.sqrt(n)
t_val = t.ppf(0.975, n - 1) # 95%置信区间对应的t值
mean = np.mean(x)
return pd.Series({
'mean': mean,
'ci_lower': mean - t_val * se,
'ci_upper': mean + t_val * se,
'se': se
})
print("\n=== 各组均值及95%置信区间 ===")
ci_result = df_group.groupby('班级')['数学'].apply(mean_ci).unstack()
print(ci_result.round(2))
agg() vs apply() 的区别:agg()适用于对每列分别应用聚合函数,返回的是标量值组成的DataFrame;而apply()更加灵活,可以返回任意形状的结果。在分组统计分析中,agg()更常用于常规汇总,apply()适用于返回Series或DataFrame的自定义函数。
四、相关系数分析
相关系数用于量化两个变量之间的线性关系的强度和方向。在数据分析中,它是探索变量间关系最基础也最重要的工具。
4.1 Pearson 相关系数
Pearson相关系数是最常用的相关系数,它度量了两个变量之间的线性相关程度。其值在[-1, 1]之间:1为完全正相关,-1为完全负相关,0为无线性相关。计算公式为两个变量的协方差除以它们标准差的乘积。
使用Pearson相关系数的前提假设:两个变量均服从近似正态分布;变量间具有线性关系;数据中不存在明显的异常值。
# 生成示例数据:身高与体重
np.random.seed(123)
heights = np.random.normal(170, 10, 100) # 身高(cm)
weights = 0.7 * heights + np.random.normal(0, 5, 100) # 体重(kg) 与身高呈线性关系
shoes = np.random.normal(40, 2, 100) # 鞋码 (与身高弱相关)
iq = np.random.normal(100, 15, 100) # IQ (与身高无关)
df_corr = pd.DataFrame({
'身高': heights,
'体重': weights,
'鞋码': shoes,
'IQ': iq
})
# Pearson相关系数
r_height_weight, p_value = stats.pearsonr(heights, weights)
print(f"身高与体重的Pearson相关系数: r = {r_height_weight:.3f}")
print(f"p-value = {p_value:.6f}")
print("结论: 相关关系" if p_value < 0.05 else "结论: 无显著相关")
# 计算相关系数矩阵 (Pandas)
corr_matrix = df_corr.corr(method='pearson')
print("\n=== Pearson相关系数矩阵 ===")
print(corr_matrix.round(3))
4.2 Spearman 秩相关系数
Spearman相关系数是一种非参数的相关度量,它基于数据的排序(秩)而非原始数值进行计算。Spearman相关系数对异常值不敏感,且能够检测单调(而不一定是线性)的相关关系。当数据不满足正态分布假设,或者变量之间存在非线性但单调的关系时,Spearman是比Pearson更合适的选择。
# Spearman秩相关系数
r_spearman, p_spearman = stats.spearmanr(heights, weights)
print(f"Spearman秩相关系数: ρ = {r_spearman:.3f}")
print(f"p-value = {p_spearman:.6f}")
# 对比:非线性单调关系的情况
x = np.linspace(0, 5, 50)
y_nonlinear = x**2 + np.random.normal(0, 0.5, 50) # 二次关系 (单调递增)
r_pearson_nl, _ = stats.pearsonr(x, y_nonlinear)
r_spearman_nl, _ = stats.spearmanr(x, y_nonlinear)
print(f"\n非线性单调关系对比:")
print(f" Pearson r = {r_pearson_nl:.3f} (低估了非线性关系的强度)")
print(f" Spearman ρ = {r_spearman_nl:.3f} (更能反映单调关系强度)")
4.3 Kendall 相关系数
Kendall Tau相关系数是另一种基于秩的非参数相关度量。它通过比较所有数据点对的一致性来评估相关性,计算的是"和谐对"与"不和谐对"的数量之差占总对数的比例。Kendall系数对异常值比Spearman更稳健,且在小样本情况下表现更好。但它的计算复杂度较高(O(n^2)),对于大数据集可能较慢。
# Kendall Tau相关系数
r_kendall, p_kendall = stats.kendalltau(heights, weights)
print(f"Kendall Tau相关系数: τ = {r_kendall:.3f}")
print(f"p-value = {p_kendall:.6f}")
# 三种方法对比
print("\n=== 三种相关系数对比 ===")
methods = ['pearson', 'spearman', 'kendall']
for method in methods:
cmat = df_corr.corr(method=method)
print(f"\n{method.upper()} 相关矩阵:")
print(cmat.round(3))
4.4 corr() 与 corrwith()
Pandas除了提供corr()方法计算相关系数矩阵外,还提供了corrwith()方法,用于计算DataFrame中每列与一个指定的Series或DataFrame之间的相关系数。这在特征选择中非常有用,例如计算所有特征与目标变量之间的相关性。
# corrwith() 示例:计算所有特征与目标变量的相关性
# 假设体重是我们的目标变量
target = '体重'
corr_with_target = df_corr.corrwith(df_corr[target])
print(f"所有特征与'{target}'的相关系数:")
print(corr_with_target.sort_values(ascending=False).round(3))
# 在特征选择中过滤高相关特征
threshold = 0.3
strong_features = corr_with_target[abs(corr_with_target) > threshold]
print(f"\n与'{target}'相关性绝对值 > {threshold} 的特征:")
print(strong_features.drop(target).index.tolist())
4.5 热力图可视化
热力图(Heatmap)是可视化相关系数矩阵最经典的方式。通过颜色深浅直观地展示变量之间相关性的强弱和方向。Seaborn库提供了非常便捷的热力图绘制接口。
# 使用Seaborn绘制相关系数热力图
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体 (可选, 根据环境调整)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 1. 标准热力图 - 显示数值
sns.heatmap(corr_matrix, annot=True, fmt='.3f', cmap='RdBu_r',
center=0, vmin=-1, vmax=1,
square=True, linewidths=.5,
ax=axes[0])
axes[0].set_title('Pearson相关系数热力图', fontsize=14)
# 2. 掩码上三角 (避免重复信息)
mask = np.triu(np.ones_like(corr_matrix, dtype=bool))
sns.heatmap(corr_matrix, mask=mask, annot=True, fmt='.3f',
cmap='RdBu_r', center=0, vmin=-1, vmax=1,
square=True, linewidths=.5, cbar_kws={"shrink": .8},
ax=axes[1])
axes[1].set_title('下三角热力图 (避免冗余)', fontsize=14)
plt.tight_layout()
plt.show()
# 聚类热力图 - 按相关性聚类
sns.clustermap(corr_matrix, annot=True, fmt='.3f',
cmap='RdBu_r', center=0,
vmin=-1, vmax=1,
linewidths=.5, figsize=(8, 6))
plt.title('聚类热力图 - 自动分组相似变量', fontsize=14)
plt.show()
热力图解读要点:颜色从深红到深蓝表示相关系数从+1到-1。数值越接近±1,颜色越深,表示相关性越强。聚类热力图可以根据变量的相关性模式自动进行分组,帮助发现隐藏在数据中的结构。在实际报告中,建议将相关系数的显著性水平(p值)也标注出来,避免过度解读微弱的相关关系。
五、协方差矩阵
协方差是衡量两个变量共同变化程度的统计量。协方差的正负表示两个变量的变化方向是否一致,但其数值大小不易直接解释(受量纲影响)。协方差矩阵则是将所有变量两两之间的协方差组织成矩阵形式。
5.1 协方差矩阵的计算与解读
# 计算协方差矩阵
cov_matrix = df_corr.cov()
print("=== 协方差矩阵 ===")
print(cov_matrix.round(2))
# 协方差矩阵与相关系数矩阵的关系
# 相关系数 = 协方差 / (标准差1 * 标准差2)
print("\n=== 从协方差矩阵计算相关系数 ===")
stds = np.sqrt(np.diag(cov_matrix))
corr_from_cov = cov_matrix.values / np.outer(stds, stds)
corr_from_cov_df = pd.DataFrame(corr_from_cov, index=cov_matrix.index, columns=cov_matrix.columns)
print(corr_from_cov_df.round(3))
5.2 特征值与特征向量
协方差矩阵的特征值和特征向量揭示了数据的主要变异方向。特征值表示对应方向上的方差大小,特征向量表示变异的方向。这正是主成分分析(PCA)的核心数学基础。
# 协方差矩阵的特征分解
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
print("=== 协方差矩阵特征分解 ===")
print("特征值 (各主成分解释的方差):")
for i, val in enumerate(sorted(eigenvalues, reverse=True)):
print(f" PC{i+1}: {val:.2f}")
# 计算方差解释比例
total_var = eigenvalues.sum()
explained_ratio = sorted(eigenvalues, reverse=True) / total_var
print("\n方差解释比例:")
for i, ratio in enumerate(explained_ratio):
cumulative = explained_ratio[:i+1].sum()
print(f" PC{i+1}: {ratio:.2%} (累计: {cumulative:.2%})")
5.3 协方差矩阵与PCA的关联
协方差矩阵是主成分分析(PCA)的基础。PCA通过对协方差矩阵进行特征分解,找到数据变异最大的方向,从而实现降维。具体来说:第一主成分对应最大特征值的特征向量方向,表示数据变异最大的方向;第二主成分对应第二大特征值的特征向量方向,且与第一主成分正交,以此类推。
# 使用PCA对协方差矩阵进行降维
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# 标准化数据 (PCA前必需)
scaler = StandardScaler()
df_scaled = pd.DataFrame(
scaler.fit_transform(df_corr),
columns=df_corr.columns
)
# 执行PCA
pca = PCA()
pca.fit(df_scaled)
print("=== PCA结果 ===")
print("各主成分的方差解释比例:")
for i, ratio in enumerate(pca.explained_variance_ratio_):
print(f" PC{i+1}: {ratio:.2%}")
print("\n主成分载荷矩阵 (特征向量):")
loadings = pd.DataFrame(
pca.components_.T,
index=df_corr.columns,
columns=[f'PC{i+1}' for i in range(pca.n_components_)]
)
print(loadings.round(3))
# 选择前2个主成分进行降维 (可视化用)
pca_2d = PCA(n_components=2)
scores_2d = pca_2d.fit_transform(df_scaled)
print(f"\n前2个主成分累计解释方差: {pca_2d.explained_variance_ratio_.sum():.2%}")
协方差矩阵 vs 相关系数矩阵:协方差矩阵保留了变量的尺度信息,对量纲敏感;相关系数矩阵是标准化的协方差矩阵。在PCA中,如果变量量纲差异较大,应使用标准化后的数据计算协方差矩阵(等价于相关系数矩阵),否则量纲大的变量会主导主成分方向。
六、相关性的注意事项与陷阱
相关性分析虽然强大,但也充满了陷阱和误区。以下是几个必须牢记的关键注意事项。
6.1 相关不等于因果(Correlation ≠ Causation)
这是统计学中最基本也最重要的原则。两个变量之间存在相关关系,并不代表一个变量导致了另一个变量的变化。可能的原因包括:存在第三个未观测到的变量同时影响了两个变量(混杂变量);纯属巧合;或者因果关系方向与直觉相反(反向因果)。
经典案例:冰淇淋销量与溺水人数呈正相关。但这并不意味着吃冰淇淋会导致溺水,而是因为夏季高温天气让更多人同时吃冰淇淋和游泳,从而同时推高了这两个指标。天气(混杂变量)才是真正的原因。
6.2 辛普森悖论(Simpson's Paradox)
辛普森悖论是指在分组数据中存在的趋势,在合并数据后消失甚至反转的奇特现象。这通常是因为忽略了重要的分组变量(混杂变量)所导致的。
# 辛普森悖论的经典示例:
# 两个医院的手术成功率。整体上看,A医院成功率更高,但分科室看却相反
data_simpson = pd.DataFrame({
'医院': ['A', 'A', 'B', 'B'],
'科室': ['心脏手术', '眼科手术', '心脏手术', '眼科手术'],
'成功数': [78, 2, 10, 81],
'总例数': [100, 2, 10, 100]
})
data_simpson['成功率'] = data_simpson['成功数'] / data_simpson['总例数']
print("=== 辛普森悖论示例 ===")
print(data_simpson)
# 整体成功率
overall_a = data_simpson[data_simpson['医院']=='A']
overall_b = data_simpson[data_simpson['医院']=='B']
rate_a = overall_a['成功数'].sum() / overall_a['总例数'].sum()
rate_b = overall_b['成功数'].sum() / overall_b['总例数'].sum()
print(f"\n整体成功率: A医院 = {rate_a:.1%}, B医院 = {rate_b:.1%}")
print("结论: A医院整体成功率更高")
print("分科室看: 心脏手术B医院更高(10/10=100% vs 78/100=78%),眼科手术B医院更高(81/100=81% vs 2/2=100%)")
print("悖论: 整体和分组结论完全相反!原因是样本量分布不均")
6.3 虚假相关(Spurious Correlation)
当两个变量在时间上呈现相似的趋势时,容易给人造成"相关"的错觉,但这种相关性完全是虚假的。最典型的是时间序列数据中的"趋势相关"——两个毫无关系的指标,仅仅因为都随时间增长,就会显示出很强的相关性。
# 虚假相关示例:
years = np.arange(2000, 2024)
# 某地区芝士消费量 (随年份增长)
cheese = 10 + years * 0.5 + np.random.normal(0, 2, len(years))
# 某地区因床单缠绕导致的死亡人数 (随年份增长)
deaths = 5 + years * 0.3 + np.random.normal(0, 1.5, len(years))
r_spurious, p_spurious = stats.pearsonr(cheese, deaths)
print(f"芝士消费与床单缠绕死亡的Pearson相关: r = {r_spurious:.3f}")
print(f"p-value = {p_spurious:.4f}")
print("看似显著相关, 实为虚假相关 - 两者都只是随时间增长的趋势!")
# 解决方案: 使用差分或去趋势后的数据再次计算相关
cheese_diff = np.diff(cheese) # 一阶差分
deaths_diff = np.diff(deaths)
r_detrended, p_detrended = stats.pearsonr(cheese_diff, deaths_diff)
print(f"\n去趋势后: r = {r_detrended:.3f}, p = {p_detrended:.4f}")
print("虚假相关消失!" if p_detrended > 0.05 else "可能仍有真实相关")
避免误读相关性的四项原则:(1)始终怀疑"相关即因果"的论断,寻找可能的混杂变量;(2)检查数据是否存在分组结构,避免辛普森悖论;(3)对时间序列数据,先做去趋势处理再计算相关性;(4)使用领域知识判断相关性的实际意义,统计显著不等于实际显著。
七、分类变量相关性
当两个变量都是分类变量时,Pearson等相关系数不再适用。此时需要专门的关联性度量方法。
7.1 列联表与卡方检验
卡方检验(Chi-square test)用于判断两个分类变量是否独立。它通过比较观察频数与期望频数的差异来评估变量之间的关联性。
# 卡方检验:性别与是否购买某产品是否有关
import pandas as pd
from scipy.stats import chi2_contingency
# 模拟数据
np.random.seed(456)
n = 500
genders = np.random.choice(['男', '女'], n, p=[0.5, 0.5])
# 假设女性购买率更高
purchase_probs = np.where(genders == '女', 0.6, 0.4)
purchased = np.random.binomial(1, purchase_probs)
df_cat = pd.DataFrame({'性别': genders, '是否购买': purchased})
# 构建列联表
contingency_table = pd.crosstab(df_cat['性别'], df_cat['是否购买'],
margins=True, margins_name='合计')
print("=== 列联表 ===")
print(contingency_table)
# 卡方检验
chi2, p_val, dof, expected = chi2_contingency(pd.crosstab(df_cat['性别'], df_cat['是否购买']))
print(f"\n卡方检验结果:")
print(f" 卡方统计量: {chi2:.3f}")
print(f" 自由度: {dof}")
print(f" p值: {p_val:.4f}")
print(f" 结论: {'性别与购买行为相关' if p_val < 0.05 else '性别与购买行为独立'}")
7.2 Cramer's V 系数
Cramer's V是基于卡方统计量计算的关联度指标,其值在0到1之间。与卡方检验不同,Cramer's V提供了关联强度的量化度量,不受样本量的影响。值越接近1,表示关联越强。
# Cramer's V 计算
def cramers_v(confusion_matrix):
"""计算Cramer's V系数"""
chi2, _, _, _ = chi2_contingency(confusion_matrix)
n = confusion_matrix.sum().sum()
min_dim = min(confusion_matrix.shape) - 1
return np.sqrt(chi2 / (n * min_dim))
# 计算Cramer's V
cm = pd.crosstab(df_cat['性别'], df_cat['是否购买'])
cv = cramers_v(cm)
print(f"Cramer's V = {cv:.3f}")
# Cramer's V 解读
def interpret_cramer_v(v):
if v < 0.1: return "微弱关联"
elif v < 0.3: return "弱关联"
elif v < 0.5: return "中等关联"
else: return "强关联"
print(f"关联强度: {interpret_cramer_v(cv)}")
# 多分类变量示例
print("\n=== 多分类变量Cramer's V矩阵 ===")
# 创建更多分类变量
df_multi_cat = pd.DataFrame({
'教育水平': np.random.choice(['高中', '本科', '研究生'], n, p=[0.3, 0.5, 0.2]),
'地区': np.random.choice(['东部', '中部', '西部'], n, p=[0.4, 0.35, 0.25]),
'产品偏好': np.random.choice(['A类', 'B类', 'C类'], n),
'是否复购': np.random.choice(['是', '否'], n, p=[0.6, 0.4])
})
categorical_cols = df_multi_cat.columns
cramers_matrix = pd.DataFrame(np.zeros((len(categorical_cols), len(categorical_cols))),
index=categorical_cols, columns=categorical_cols)
for col1 in categorical_cols:
for col2 in categorical_cols:
cm_temp = pd.crosstab(df_multi_cat[col1], df_multi_cat[col2])
cramers_matrix.loc[col1, col2] = cramers_v(cm_temp)
print(cramers_matrix.round(3))
八、数据分布形态检验
很多统计方法(如Pearson相关、t检验、ANOVA)都假设数据服从正态分布。因此,在应用这些方法之前,检验数据的分布形态是非常必要的。
8.1 正态分布检验方法
判断一组数据是否服从正态分布,既可以通过图形方法(Q-Q图、直方图、密度图),也可以通过统计检验(Shapiro-Wilk检验、D'Agostino-Pearson检验、Kolmogorov-Smirnov检验)。
# 正态分布检验综合示例
from scipy.stats import shapiro, normaltest, kstest, probplot
import statsmodels.api as sm
# 生成不同分布的数据
np.random.seed(789)
normal_data = np.random.normal(100, 15, 200) # 正态分布
skewed_data = np.random.exponential(50, 200) # 偏态分布
uniform_data = np.random.uniform(0, 100, 200) # 均匀分布
# Shapiro-Wilk检验 (最常用, 适合中小样本)
def normality_test(data, name, test='shapiro'):
if test == 'shapiro':
stat, p = shapiro(data)
test_name = "Shapiro-Wilk"
elif test == 'dagostino':
stat, p = normaltest(data)
test_name = "D'Agostino-Pearson"
else:
stat, p = kstest(data, 'norm', args=(np.mean(data), np.std(data, ddof=1)))
test_name = "Kolmogorov-Smirnov"
conclusion = "服从正态分布" if p > 0.05 else "不服从正态分布"
print(f"{test_name}检验 ({name}): 统计量={stat:.4f}, p={p:.4f} → {conclusion}")
for name, data in [("正态数据", normal_data),
("偏态数据", skewed_data),
("均匀数据", uniform_data)]:
normality_test(data, name, 'shapiro')
normality_test(data, name, 'dagostino')
print()
8.2 Q-Q图(Quantile-Quantile Plot)
Q-Q图是检验分布形态最直观的图形工具。它将数据的实际分位数与理论正态分布的分位数进行对比。如果数据点大致落在45度参考线上,说明数据服从正态分布。点偏离参考线则提示分布偏态或厚尾。
# Q-Q图绘制
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
# 直方图 + 密度曲线
for ax, data, title in zip(axes[0],
[normal_data, skewed_data, uniform_data],
['正态分布数据', '偏态分布(指数)', '均匀分布']):
ax.hist(data, bins=20, density=True, alpha=0.7, color='steelblue', edgecolor='white')
from scipy.stats import gaussian_kde
kde = gaussian_kde(data)
x_range = np.linspace(data.min(), data.max(), 200)
ax.plot(x_range, kde(x_range), 'r-', linewidth=2)
ax.axvline(np.mean(data), color='g', linestyle='--', label=f'均值={np.mean(data):.1f}')
ax.axvline(np.median(data), color='orange', linestyle=':', label=f'中位数={np.median(data):.1f}')
ax.set_title(title, fontsize=12)
ax.legend(fontsize=9)
# Q-Q图
for ax, data, title in zip(axes[1],
[normal_data, skewed_data, uniform_data],
['Q-Q图: 正态数据', 'Q-Q图: 偏态数据', 'Q-Q图: 均匀数据']):
probplot(data, dist='norm', plot=ax)
ax.get_lines()[0].set_markerfacecolor('steelblue')
ax.get_lines()[0].set_markeredgecolor('steelblue')
ax.get_lines()[1].set_color('red')
ax.set_title(title, fontsize=12)
ax.set_xlabel('理论分位数')
ax.set_ylabel('实际分位数')
plt.tight_layout()
plt.show()
Q-Q图解读指南:(1)点大致在直线上 -> 服从正态分布;(2)两端点偏离直线(S形)-> 尾部比正态更薄或更厚;(3)点呈弧形(上凸或下凹)-> 分布偏态;(4)注意:Shapiro-Wilk检验在小样本(n<50)时可能不够灵敏,大样本(n>5000)时又可能过于灵敏,此时应结合Q-Q图综合判断。实际数据分析中,只要数据"近似"正态即可,不必苛求完美正态。
8.3 Shapiro-Wilk 检验详解
Shapiro-Wilk检验是目前公认最有效的正态性检验方法之一,尤其适用于中小样本(n < 5000)。其原假设H0为"数据服从正态分布"。当p值小于显著性水平(通常为0.05)时,拒绝原假设,认为数据不服从正态分布。
# Shapiro-Wilk 检验详细示例
def shapiro_report(data, name, alpha=0.05):
stat, p = shapiro(data)
n = len(data)
print(f"=== {name} (n={n}) ===")
print(f" Shapiro-Wilk W统计量: {stat:.4f}")
print(f" p值: {p:.4f}")
if p > alpha:
print(f" ✅ 无法拒绝H0 (p={p:.4f} > {alpha})")
print(f" 结论: 数据{name}服从正态分布 (或无法证伪)")
else:
print(f" ❌ 拒绝H0 (p={p:.4f} < {alpha})")
print(f" 结论: 数据{name}不服从正态分布")
# 补充:偏度和峰度信息
skew = stats.skew(data, bias=False)
kurt = stats.kurtosis(data, bias=False)
print(f" 偏度={skew:.3f}, 峰度(Fisher)={kurt:.3f}")
print()
# 对不同样本量进行检验
np.random.seed(111)
for n in [10, 30, 100, 1000]:
sample = np.random.normal(50, 10, n)
shapiro_report(sample, "正态分布样本")
# 产生轻微污染的正态分布 (含5%异常值)
contaminated = sample.copy()
n_outliers = max(1, int(n * 0.05))
outlier_indices = np.random.choice(n, n_outliers, replace=False)
contaminated[outlier_indices] = np.random.uniform(100, 200, n_outliers)
shapiro_report(contaminated, "含5%异常值")
print("-" * 40)
九、总结与实践建议
方法选择决策流程:
- 数据概览:先用
df.describe()和df.info()快速了解数据的基本情况,检查缺失值和数据类型。
- 分组比较:需要分组分析时,使用
groupby() + describe()或agg()进行聚合统计。
- 连续变量间相关性:首选Pearson,若数据非正态或存在异常值则选择Spearman;Kendall适用于小样本或对异常值极度敏感的场景。
- 分类变量间关联:使用卡方检验判断独立性,Cramer's V量化关联强度。
- 混合类型变量:二分类与连续变量可使用点二列相关系数(Point-Biserial Correlation),有序分类与连续变量可使用Spearman。
- 可视化配合:热力图展示相关系数矩阵,散点图矩阵(pairplot)直观展示变量间关系,Q-Q图检验正态性。
- 多维数据分析:协方差矩阵配合PCA进行降维分析,理解数据的潜在结构。
警惕陷阱:分析中时刻牢记"相关不等于因果"。发现有趣的相关性时,先思考是否存在混杂变量、分组效应或时间趋势导致的虚假相关。只有在严格的实验设计(如随机对照试验)下,才能做出因果推断。观察性数据中的相关性只能作为进一步研究的线索,不能作为因果证据。
| 统计方法 | 适用场景 | 数据类型 | Python实现 |
| 均值/中位数/众数 | 集中趋势描述 | 连续/分类 | np.mean, np.median, stats.mode |
| 标准差/方差/IQR | 离散程度描述 | 连续 | np.std, np.var, np.percentile |
| 偏度/峰度 | 分布形态描述 | 连续 | stats.skew, stats.kurtosis |
| describe() | 一键汇总 | 连续 | df.describe() |
| GroupBy + agg() | 分组描述统计 | 混合 | df.groupby().agg() |
| Pearson相关 | 线性关系 | 连续+连续 | df.corr('pearson') |
| Spearman相关 | 单调关系 | 连续+连续 | df.corr('spearman') |
| Kendall相关 | 稳健非参数相关 | 连续+连续 | df.corr('kendall') |
| 热力图 | 相关矩阵可视化 | 连续 | sns.heatmap() |
| 协方差矩阵 | 多维变异分析 | 连续 | df.cov() |
| PCA | 降维/特征提取 | 连续 | sklearn.decomposition.PCA |
| 卡方检验 | 分类变量独立性 | 分类+分类 | chi2_contingency() |
| Cramer's V | 分类关联强度 | 分类+分类 | 自定义公式 |
| Shapiro-Wilk | 正态性检验 | 连续 | stats.shapiro() |
| Q-Q图 | 分布形态可视化 | 连续 | statsmodels.qqplot |
推荐学习路径:先熟练掌握describe()和相关性矩阵(corr() + heatmap)的组合,这已经能解决80%的日常描述性统计分析需求。然后逐步学习分组聚合(groupby + agg)和正态性检验。协方差矩阵和PCA属于进阶内容,在有降维需求时深入学习即可。