在实际的数据分析工作里,排名是一个高频需求。比如学生成绩表要按总分排名,总分相同的再按语文成绩区分先后;销售数据表要按销售额排名,销售额相同的再按订单量排序。Pandas提供了sort_values和rank两个核心方法,配合灵活的参数组合,可以轻松实现多列优先级排名以及各种同分处理策略。本文通过完整的案例代码,把这些技巧一次讲透。

一、多列优先级排序的基础:sort_values
sort_values是Pandas中最常用的排序方法,它支持传入一个列名列表,列表中列的先后顺序就代表了优先级。排在前面的列优先比较,只有当前面的列值相同时,才会继续比较后面的列。这在语义上正好符合多列优先级排名的需求。
我们先构造一个学生成绩数据框来做演示,包含姓名、总分和语文三列:
import pandas as pd
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六', '孙七'],
'总分': [580, 580, 610, 550, 580],
'语文': [120, 125, 130, 118, 122]
})
print(df)现在要求按总分降序排名,总分相同时按语文成绩降序区分先后。只需要把两个列名按优先级传入sort_values,并分别指定升降序即可:
df_sorted = df.sort_values(
by=['总分', '语文'],
ascending=[False, False]
).reset_index(drop=True)
print(df_sorted)这里有个细节值得注意:ascending参数接收一个列表,与by中的列一一对应,这样每一列都可以独立控制升序还是降序。另外排序后索引会保持原样,如果后续需要用位置访问,记得调用reset_index重置索引,这是一个容易被忽略但经常引发bug的地方。
二、rank方法与五种同分处理策略
排序只能给出先后顺序,如果要得到具体的名次数字,就需要用到rank方法。rank的核心难点在于同分怎么处理,它提供了method参数,包含五种策略,各有适用场景。
假设总分列中有三个人并列,我们逐一对比五种方法的结果差异:
df['average排名'] = df['总分'].rank(method='average', ascending=False) df['min排名'] = df['总分'].rank(method='min', ascending=False) df['max排名'] = df['总分'].rank(method='max', ascending=False) df['first排名'] = df['总分'].rank(method='first', ascending=False) df['dense排名'] = df['总分'].rank(method='dense', ascending=False) print(df)
假设五个人的总分降序排列后是610、580、580、580、550,三个580占据了第2、3、4的位置。五种方法的结果分别是:
- average:并列者取平均名次,三个580都得3,这也是默认值。体育比赛中并列名次常用这种方式。
- min:并列者都取最靠前的名次,三个580都得2。这和SQL的RANK函数行为一致,是最常见的比赛排名方式,即并列第二,下一个人直接是第五。
- max:并列者都取最靠后的名次,三个580都得4,下一个人是第五。
- first:并列者按数据出现的先后顺序依次编号为2、3、4,完全不重复。适合需要强制唯一名次的场景,比如必须决出唯一名次的选拔。
- dense:并列者名次相同,但下一个不同值紧接着编号,三个580都是2,550是3。这和SQL的DENSE_RANK一致,名次不会出现跳跃。
选哪种方法没有绝对对错,关键看业务语义。如果排名要用于评奖评优,通常用min或dense;如果要求名次唯一不重复,就用first。特别提醒,rank的ascending参数默认为True,也就是数值越小名次越靠前,做成绩排名时务必显式设置ascending=False,否则结果会完全颠倒。
三、多列优先级排名的完整实现方案
理解了排序和排名各自的能力之后,把两者组合起来就能实现多列优先级排名。核心思路是:先用sort_values按优先级排好序,再用rank的first方法或者基于位置生成名次列。
回到成绩表的例子,需求是总分优先、总分相同看语文、语文也相同则并列:
# 先按优先级排序,保证并列者的相对顺序确定
df = df.sort_values(
by=['总分', '语文'], ascending=[False, False]
).reset_index(drop=True)
# 总分做min排名,总分相同者并列
df['名次'] = df['总分'].rank(method='min', ascending=False).astype(int)
print(df[['姓名', '总分', '语文', '名次']])如果业务要求先看总分、再看语文、两者都相同才并列,可以先用排名元组的方式处理。一个实用技巧是把多列压缩成一个比较键,比如把次要列做微小的加权或者直接用groupby分组后再排名:
# 方案一:对多列组合做first排名,得到严格唯一的序号
df['唯一序号'] = df.sort_values(
['总分', '语文'], ascending=[False, False]
).index + 1
# 方案二:先按主要列排名,再用groupby在并列组内按次要列区分
df['总分名次'] = df.groupby('总分')['语文'] \
.rank(method='first', ascending=False)
# 结合两者:总分并列时,语文高者序号靠前
def complex_rank(g):
return g.rank(method='min', ascending=False)
df['综合名次'] = df['总分'].rank(method='min', ascending=False)
print(df)再补充一个分组排名的场景。实际业务中经常需要在每个班级内部独立排名,而不是全年级一起排。这时用groupby加transform或apply即可:
df['班级内名次'] = df.groupby('班级')['总分'] \
.rank(method='min', ascending=False).astype(int)groupby之后调用rank会自动按组计算,返回值与原数据框行对齐,直接赋值为新列即可。这是比循环每个班级再拼接高效得多的写法。
四、常见坑与性能建议
第一个常见的坑是NaN值。rank默认有个na_option参数,取值为keep时NaN保持为NaN,为top或bottom时NaN会排在最前或最后。而sort_values里对应的参数是na_position。如果数据里有缺失值,两种方法的行为差异可能让排名结果不符合预期,建议在排名前明确处理缺失值策略。
第二个坑是数据类型问题。如果列是字符串类型的数字,排序和排名都会按字典序比较,比如9会比10大。排名前务必用astype转换成数值类型:
df['总分'] = pd.to_numeric(df['总分'], errors='coerce')
性能方面,sort_values底层使用稳定的排序算法,rank在大数据量下的时间复杂度是线性的,两者对于百万级别的数据都能在秒级完成。但如果需要反复对多列做复杂排名,建议提前把参与比较的列合并为单个可比较的键列,避免多次排序。此外,rank返回的是float类型,得到最终结果后用astype(int)转换,既节省内存也让输出更整洁。
总结一下,多列优先级排名用sort_values的列列表加ascending列表即可实现;名次数字用rank生成,method参数决定同分策略;分组排名交给groupby加rank;缺失值和类型问题在排名前处理好。掌握这几点,绝大多数排名需求都能干净利落地解决。