相关系数这个系列写到第4篇了。前面几篇把皮尔逊相关系数翻来覆去讲了不少,包括怎么算、怎么检验、怎么在论文里解释。今天把另一半补上——斯皮尔曼相关系数(Spearman's rank correlation coefficient),也就是很多人常说的“等级相关系数”。
先说清楚这东西能干嘛。你在数学建模比赛里拿到一批数据,想判断两个变量之间有没有关系,但画完散点图发现根本不是直线趋势,或者数据里有几个明显的离群点,这时候皮尔逊相关系数就会给你一个“不太可信”的数字。斯皮尔曼相关系数解决的正是这类问题:它不关心数据的具体取值,只关心排序,所以对非线性关系更包容,对异常值更稳健。这篇文章我会从原理讲到Python实现,再结合数学建模竞赛的实际场景聊聊怎么选、怎么写进论文,最后把我踩过的坑一并列出来。适合正在备战国赛、华为杯的参赛选手,也适合做数据分析时需要对变量关联性做判断的读者。
1. 为什么需要斯皮尔曼相关系数
1.1 皮尔逊的硬门槛:数据得“长”成什么样
想用好一个工具,得先知道它的前提条件。皮尔逊相关系数(Pearson correlation coefficient)衡量的是两个变量之间的线性相关程度,它默认数据满足几个条件:近似线性关系、双变量近似正态分布、没有明显异常值。
问题在于,数学建模拿到的数据往往没这么“听话”。经济数据、人口数据、评分数据,常常是偏态的;变量之间的关系也可能是指数型、对数型、幂函数型,画出来是条弯弯曲曲的曲线;再加上数据采集过程中偶尔混入的脏数据,一个异常值就可能把皮尔逊系数从0.8拉到0.3。用皮尔逊之前你得花大量时间做正态性检验、线性诊断、异常值处理,一套流程下来,光是数据预处理就够喝一壶的。
我见过不少参赛选手强行对不满足条件的数据计算皮尔逊系数,还煞有介事地标注显著性。说实话,评委里懂统计的老师不少,这种做法容易被一眼看穿。更合理的做法是:数据不满足皮尔逊的适用条件时,直接换用斯皮尔曼相关系数。
1.2 斯皮尔曼的核心思想:不看数值看排名
斯皮尔曼相关系数的思路很有意思:既然数值本身的分布有各种问题,那我干脆不看数值,只看排名。它的另一个名字“等级相关系数”也由此而来。
比如有5个学生的数学成绩和物理成绩,哪怕数学满分是100分、物理满分是150分,量纲完全不同,都没关系。只要我把两门成绩分别从高到低排序,然后看每个学生在两门学科中的排名是否一致——排名同步上升,就说明两门成绩存在正向关联。
这个“基于排名”的设计,让斯皮尔曼相关系数天然具备了两个优点。第一,对异常值稳健得很,你数据里混进一个离群点,最多让它自己的排名变一下,对整个排序格局影响很小。第二,不需要假设数据符合正态分布,因为秩(排名)本身就是均匀分布的,和原始数据的分布形态没有关系。
用生活化的类比来说,皮尔逊关心的是“身高和体重是不是同比例增长”,而斯皮尔曼关心的是“个子最高的人是不是体重也最重”。前者看的是具体数值的线性关系,后者看的是等级次序的一致性。很多时候,我们做相关性分析的目的是想回答“变量之间是否存在关联趋势”,而不仅仅是“是否存在精确的线性关系”,这时候斯皮尔曼反而更贴近分析的本意。
2. 斯皮尔曼相关系数的数学原理与计算
2.1 秩与等级:先把数据翻译成排名
斯皮尔曼的英文原名是Spearman's rank correlation coefficient,这里的rank就是“秩”的意思。所谓秩,就是把一组数据从小到大(或从大到小)排序后,每个数据所在的位置编号。比如数据组 [60, 85, 72, 95, 45],从小到大排序后是 [45, 60, 72, 85, 95],那么原始数据对应的秩分别是 [2, 4, 3, 5, 1]。
有并列值时怎么办?比如两个学生的成绩都是85分,排第2还是第3?统计学里的常用做法是取平均秩,也就是原本第2和第3两个位置,两个85分都取2.5。Python的pandas里的rank方法默认就是这么做,method='average',这也是竞赛中最推荐的方式。有些编程实现会用method='min'或者'max',排序逻辑会不同,计算出来的相关系数也可能有细微差别,所以复现别人的代码时一定要留个心眼。
2.2 两种计算公式到底怎么选
斯皮尔曼相关系数的计算有两种方式。
第一种是简化公式,经典教材里的写法是:
rs = 1 - (6 * Σdᵢ²) / (n * (n² - 1))其中dᵢ是第i个样本在两个变量中的秩之差,n是样本量。
这个公式看起来简洁好记,但我必须提醒一句:它只适用于没有并列等级的情况,而且对样本量也有一定要求。只要数据里出现并列值,这个公式得到的结果就和严格定义有偏差。我自己刚学那会儿就踩过这个坑,拿着简化公式手算了一组含并列值的数据,结果和统计软件输出的不一致,查了半天才明白原因。
第二种是通用定义,也是最稳妥的实现思路:把两个变量分别转化为秩,然后对秩计算皮尔逊相关系数。因为秩本身就是数值,所以完全可以直接套用皮尔逊公式:
rs = cov(Rx, Ry) / (std(Rx) * std(Ry))这里的Rx和Ry是原始变量各自的秩序列。这种方法天然兼容并列等级的情况,因为并列值已经通过平均秩处理好了。实际操作中,我建议一律采用第二种思路,跑出来的结果和scipy等统计库完全一致,也不容易出错。
2.3 手算一遍:5个学生的例子
光讲公式太抽象,我们实际算一遍。假设5个学生的数学和物理成绩如下:
| 学生 | 数学成绩 | 物理成绩 | 数学排名 | 物理排名 | d | d² |
|---|---|---|---|---|---|---|
| A | 95 | 88 | 1 | 1 | 0 | 0 |
| B | 80 | 75 | 2 | 3 | -1 | 1 |
| C | 72 | 78 | 3 | 2 | 1 | 1 |
| D | 60 | 55 | 4 | 4 | 0 | 0 |
| E | 45 | 50 | 5 | 5 | 0 | 0 |
数学成绩从高到低排名:A第1,B第2,C第3,D第4,E第5。物理成绩排名:A第1,C第2,B第3,D第4,E第5。计算每个学生的秩之差d,得到d²的平方和为2。
代入简化公式:
rs = 1 - (6 * 2) / (5 * (25 - 1)) = 1 - 12 / 120 = 0.9rs = 0.9,说明数学和物理成绩存在很强的正相关关系。观察数据也能发现,除了B和C两个同学的物理排名互换了一下,其余同学的排名顺序基本一致,整体趋势是成绩好的学生两门都强,这就是高度正向关联的直观体现。
这个案例里没有并列值,简化公式的结果没问题。但如果你要对含并列值的数据手算,建议先做平均秩处理,再按秩序列计算皮尔逊系数,这样最接近统计软件的输出。
3. Python实操与代码实现
3.1 最省事的方案:scipy.stats.spearmanr
写数学建模代码,我习惯直接上scipy,一行代码出结果。spearmanr函数的用法很简单:
import numpy as np from scipy import stats math_scores = np.array([95, 80, 72, 60, 45]) phys_scores = np.array([88, 75, 78, 55, 50]) rho, p_value = stats.spearmanr(math_scores, phys_scores) print(f"斯皮尔曼相关系数: {rho:.4f}") print(f"p值: {p_value:.4f}")运行结果中rho就是斯皮尔曼相关系数,p值对应显著性检验的结果。p值小于0.05通常认为存在显著相关,小于0.01则说明相关性极显著。
实际处理DataFrame数据时,可以用类似下面的方式一次性计算多列之间的相关系数矩阵:
import pandas as pd df = pd.DataFrame({ 'math': math_scores, 'phys': phys_scores, 'chem': [92, 85, 80, 65, 48], 'bio': [82, 78, 70, 58, 52] }) # 计算相关系数矩阵 corr_matrix = df.corr(method='spearman') print(corr_matrix)pandas的corr方法里直接指定method='spearman'就行,返回的矩阵在论文里稍微格式化一下就能用,非常方便。
3.2 手写一个斯皮尔曼计算器(pandas实现)
虽然说现成的库好用,但我一直建议你在比赛前手写一遍核心代码,不是为了造轮子,而是为了真正理解底层逻辑。万一遇到特殊情况需要调整算法,你才知道从哪里下手。
手写实现的核心就是“先排名,再算皮尔逊”:
import pandas as pd import numpy as np def spearman_manual(x, y): # 转为pandas Series方便调用rank rx = pd.Series(x).rank(method='average').values ry = pd.Series(y).rank(method='average').values # 对秩序列计算皮尔逊相关系数 # 等价于 cov(rx, ry) / (std(rx) * std(ry)) rho = np.corrcoef(rx, ry)[0, 1] return rho math_scores = [95, 80, 72, 60, 45] phys_scores = [88, 75, 78, 55, 50] rho_manual = spearman_manual(math_scores, phys_scores) print(f"手写斯皮尔曼相关系数: {rho_manual:.4f}")输出结果应该和scipy一致,都是0.9。这里的关键点是rank方法默认采用average策略,正好对应平均秩的处理方式。如果你用的是method='min',遇到并列值时结果会完全不同,这也是我之前踩坑的地方。
3.3 可视化辅助判断单调关系
动手算之前,我强烈建议先画个散点图。这不是形式主义,而是帮你快速判断两个变量之间的关系形态。如果散点图呈现明显的单调上升或下降趋势,但中间有弯曲,斯皮尔曼会给出比较高的相关系数;如果图形是U型或者倒U型,那就不是单调关系,斯皮尔曼也会“失灵”,需要尝试其他分析方法。
import matplotlib.pyplot as plt plt.figure(figsize=(6, 5)) plt.scatter(math_scores, phys_scores, s=60, alpha=0.7) plt.xlabel('数学成绩') plt.ylabel('物理成绩') plt.title('数学与物理成绩散点图') plt.grid(alpha=0.3) plt.show()散点图可视化还有个好处:能直观发现异常值。如果某个点明显偏离整体趋势,别急着删,先排查一下是不是录入错误。如果是真实数据中的极端情况,斯皮尔曼的抗干扰能力足以应对,这也正是它在竞赛数据预处理环节广受欢迎的原因。还有一点,处理多变量数据时可以绘制相关矩阵热力图,用seaborn的heatmap函数把斯皮尔曼矩阵可视化,颜色深浅一眼就能看出哪些变量关联密切,这对后续特征筛选很有帮助。
4. 在数学建模竞赛中的实战应用
4.1 赛题中“选谁”的决策流程
国赛、华为杯或者美赛的题目,经常要我们分析变量之间的关联性。拿到一组数据,到底用皮尔逊还是斯皮尔曼?我自己的判断流程是这样的:
| 判断点 | 满足条件 | 选择 |
|---|---|---|
| 散点图是否呈直线趋势 | 是 | 皮尔逊 |
| 双变量是否近似正态分布 | 是 | 皮尔逊 |
| 数据中是否有明显异常值 | 无 | 皮尔逊 |
| 变量关系是否单调但非线性 | 是 | 斯皮尔曼 |
| 数据是否存在偏态或厚尾 | 是 | 斯皮尔曼 |
| 数据是否为有序分类变量(如评分、等级) | 是 | 斯皮尔曼 |
| 数据是否含异常值或离群点 | 是 | 斯皮尔曼 |
更保险的做法是两个都算一遍。如果结论一致,说明关系稳健,在论文里可以只报告一个;如果结论不一致,比如皮尔逊系数接近0而斯皮尔曼系数很高,说明变量之间存在的是非线性单调关系,这时以斯皮尔曼为准,在论文里恰好可以作为一个分析亮点展开讨论。
4.2 典型赛题场景与数据特点
数学建模赛题里,斯皮尔曼常见的应用场景可以归纳为三类。
第一类是经济与社会科学数据,比如分析“教育投入与地区经济增长”“人口老龄化与消费结构”这类问题。经济数据普遍存在偏态分布和异常值,而且变量之间的理论关系往往不是精确线性,用斯皮尔曼做相关性初筛非常合适。
第二类是多指标综合评价,比如一些评价模型在构建指标体系之前,需要先检验指标之间的相关性,避免信息重复。问卷量表、专家打分这类数据本质上是有序分类变量,打3分和打4分之间的“距离”并不严格相等,直接计算皮尔逊在统计上不够严谨,斯皮尔曼才是贴合的。
第三类是数据预处理阶段的多重共线性诊断。比如回归建模之前,用斯皮尔曼计算相关系数矩阵,如果发现两个特征之间rs超过0.8,就要考虑保留其中一个或做降维处理。我之前参加强化学习类题目时,就是靠斯皮尔曼筛掉了一批强相关特征,后续模型收敛速度和稳定性都好了不少。
4.3 论文里的标准写法与表述
比赛论文跟实验报告还不一样,评委每天要看几百页论文,你的表述必须规范、清晰、有说服力。我建议按这个套路写。
第一步,说明为什么选斯皮尔曼。可以写“考虑到数据可能存在异常值且变量间关系未必严格线性,本文采用斯皮尔曼相关系数进行相关性分析”。
第二步,给出计算公式或指明使用Python的scipy实现,介绍计算方式。
第三步,用表格呈现结果。常见的做法是做一个相关系数矩阵,并标注显著性水平。可以在相关系数后用星号标注:*表示p<0.05,**表示p<0.01,这也是学术论文的通用惯例。
第四步,对关键结果做文字解读。比如“由表可知,X1与X2的斯皮尔曼相关系数为0.73,且通过显著性检验(p<0.01),表明两者之间存在显著的正向关联”。注意,不要把“相关”写成“因果”,这是统计表述里的大忌。
另外,论文中如果同时使用了皮尔逊和斯皮尔曼,最好明确说明两者结论是否一致,若不一致则解释可能的原因(例如数据中存在非线性单调关系或异常值影响),这能体现你思考的深度,也更容易获得评委的认可。
5. 常见问题与避坑指南
5.1 p值理解与样本量陷阱
斯皮尔曼相关系数本身是好算的,真正容易翻车的地方在显著性检验。很多人跑出rs=0.6就急着下结论,忽略了p值。注意,rs=0.6只能说明样本里存在中等强度相关性,但样本太小的时候,这个结果很可能只是随机波动,p值会告诉你结论的可靠程度。
scipy的spearmanr会直接返回p值,计算原理是基于t分布近似。样本量比较大的时候(比如n大于30),这个近似效果很好;样本量很小(比如n小于10)时,p值可能不准确,可以考虑查斯皮尔曼秩相关系数临界值表来辅助判断。竞赛中如果遇到小样本,建议在论文里注明p值是通过近似方法计算的,这样做更严谨。
5.2 三种相关系数结果不一致怎么办
皮尔逊、斯皮尔曼、Kendall's tau-b这三种相关系数,在绝大多数情况下给出的结论方向一致,但数值可能差异明显。如果出现皮尔逊系数接近0、斯皮尔曼系数却很高的情况,通常说明两个变量之间是一种单调但非线性的关系,比如指数增长、对数衰减等形式。斯皮尔曼能捕捉这种趋势,皮尔逊则“看不见”。
我处理这类问题的习惯是:画出散点图确认趋势形态,然后用斯皮尔曼量化关联强度,最后在论文中把曲线拟合也做出来。这样做既能解释相关性的存在,又能进一步刻画关系的形式,逻辑链完整,比单纯给一个相关系数强得多。
5.3 实操中的几个隐蔽坑
第一个坑是数据顺序错位。使用scipy的spearmanr时,传入的两个数组对应顺序必须一致。尤其是从Excel或CSV里读数据后做了排序、去重等操作,一不留神就把对应关系打乱了,相关系数算出来完全没意义。建议用pandas的DataFrame管理数据,筛选操作后重置索引。
第二个坑是没有处理缺失值。spearmanr遇到缺失值返回的结果是nan,pandas的corr默认会忽略缺失对,但如果你把Series直接喂给scipy,就得提前dropna。我的习惯是统一用df.dropna(subset=[col1, col2])清洗后再计算。
第三个坑是rank方法的选择。前面提过,不同rank方法处理并列值的方式不同。用scipy计算时默认是平均秩,手写实现时如果用method='min',两者结果不一致,容易怀疑是代码写错了,其实是对齐方式的问题。多变量大规模数据用热力图展示相关矩阵时,也要注意标注好用的是哪种相关系数,别让读者产生误解。
第四个坑是忽略多重比较问题。一次性计算10个变量两两之间的相关系数,就会得到45个p值,其中可能有2到3个假阳性。如果要做严格的显著性判断,可以考虑用Bonferroni校正,也就是把显著性水平除以比较次数,虽然保守,但至少不会闹出拿随机噪声当显著相关的笑话。
最后一个坑是过度解读。斯皮尔曼相关系数衡量的只是单调关联强度,并不等于因果关系的存在。气温升高和冰淇淋销量上升的相关系数可能很高,但真正的原因可能是“夏天来了”。在建模论文中,对相关性结果的解读要克制,结合业务背景分析可能的影响机制,而不是急着写“强相关意味着强因果”,这是评委眼里非常减分的一种表述。
我把这些年来碰到的实际问题整理下来,最想强调的还是那句老话:先看数据再选方法。斯皮尔曼不是万能的,但它能帮你在数据没那么“规整”的时候,依然给出一个值得信任的关联性判断。做数学建模这几年,我越来越觉得,一个聪明的选手不是会背多少公式,而是知道在什么条件下选什么工具,并且能把这个选择的前因后果讲清楚。希望这篇文章能帮你少走点弯路,比赛时多拿几个分。