在数据分析里,排序和排名几乎是每天都要碰的操作,但很多人用着用着就混了。DataFrame的sort_values和rank,看名字都带“排”,实际干的活完全不同:一个是把整张表重新摆顺序,另一个是给每个数据打分定名次。我用pandas做数据分析这几年,见过不少新同学在这里翻车——要么排序后索引乱套,要么排名出来的结果和预期差一截。这篇东西我不打算讲高深理论,就集中把DataFrame的排序和排名讲透,包括参数怎么用、业务场景怎么选、踩过的坑和排查思路,适合刚开始接触pandas的读者,以及在项目中真的要用这些功能做数据处理、出报表的同行。
1. 排序和排名的核心区别与场景选择
1.1 排序是改变行的位置,排名是生成新的值
先把这个最基础的概念掰扯清楚。DataFrame的排序用sort_values,它是把数据行按照某个或多个列的值,重新排成一个新的顺序。返回的结果仍然是一张完整的表,只是行的前后位置变了。你可以把它理解成“排队”:学生们按身高从矮到高站成一列,每个人还是那个人,只是站的顺序变了。
排名则完全不同,用的是rank方法。它不会移动任何行,而是针对每个值,计算它在所有值中的一个“名次”,生成一个新的一列或者说一个Series。还是看身高,排名是给每个学生发一个号牌:最矮的1号、第二矮的2号,并没有让学生移动位置。所以,排序适合用来“展示”数据,排名适合用来“计算”数据。
很多时候,你会看到一些代码写了df.sort_values('score')之后,以为就得到了每个人在班级里的名次,然后直接拿排好的行数当名次用,这其实是有问题的。如果数据没有并列、没有缺失,排序后的行号确实和名次一样;但只要出现并列,你的行号和真实名次就对不上了。比如两个人都考了95分,按顺序排下来一个在第3行一个在第4行,他们的名次其实都是第3名(取决于规则)。这就要靠rank来解决。
1.2 业务场景里到底什么时候用哪个
我自己的习惯是,先问自己一个问题:我要的是“列表的顺序”还是“每个对象的名次”?
如果是“列出销售额最高的前10个地区”,用sort_values降序排完,然后取前10行,再reset_index(drop=True)把序号理干净,这就是最直接的做法。这里顺序本身就是结果,不需要再生成名次。
如果是“每个门店的销售额在全部门店中排名第几”,而且以后还要根据这些名次做汇总、筛选、画图,那就必须用rank生成一个排名列。因为这列数据可以继续参与运算,比如筛选排名前20%的门店、按排名区间分组等。而排序的结果只是一张换了顺序的表,没法直接当数值用。
举个混合场景:你既要做排行榜,又要算每个业务员的绩效系数。排行榜必然用排序;绩效系数要根据销售额在整个团队中的百分位来给,这种就得先rank(pct=True),再用排名列做区间映射。这两个动作经常出现在同一张表上,但目的完全不同,混用了就会出现“看着对,算完错”的尴尬。
2. DataFrame排序实操:从单列到多列,从升序到降序
2.1 单列排序的基本用法和注意事项
直接先看代码:
import pandas as pd df = pd.DataFrame({ 'name': ['A', 'B', 'C', 'D'], 'score': [88, 92, 78, 88], 'age': [21, 23, 20, 22] }) # 按 score 升序排列 df_sorted = df.sort_values(by='score') print(df_sorted)输出结果里,行会按score从小到大排。注意sort_values默认返回一个新DataFrame,原表df的顺序不变。如果你想把结果直接覆盖原变量,可以写成df.sort_values(by='score', inplace=True),但我个人不太推荐用inplace=True。原因很简单:第一,pandas后来很多方法都倾向于不原地修改,保持一致;第二,一旦原地改错了,找不回原始数据,尤其是你在调试脚本时,最好让每一步都返回一个新对象,方便对比。
升序和降序用ascending参数控制:
df_sorted_desc = df.sort_values(by='score', ascending=False)这里还有个容易忽略的点:ascending参数的默认值是True。当你要按字符串列排序时,默认的排序规则是按字母顺序,中文也类似,但如果你希望按拼音或者别的规则,pandas本身对中文支持并不完全满足业务习惯,这种时候要么转成拼音列,要么自定义映射列再排。
2.2 多列排序:优先级和稳定性才是关键
多列排序在业务上很常见,比如先按地区排序,再按销售额排序。写法很简单:
df_sorted_multi = df.sort_values(by=['region', 'sales'], ascending=[True, False])这里by列表里的第一列是主排序键,第二列是次排序键。ascending也要给一个列表,长度和by一致,分别指定每一列是升序还是降序。我个人非常建议显式写ascending列表,因为如果都是一样的排序方向,你当然可以不写第二个参数,但一旦混排,不给列表就会报错或者得到错误方向,不如一开始就写清楚。
多列排序还有一个深水坑:排序算法的稳定性。默认情况下,sort_values使用的排序算法是quicksort,这是不稳定的排序算法。什么意思呢?在多列排序时,pandas是逐列进行排序的,虽然单次排序内部可能不稳定,但pandas的设计会尽量保持相对顺序。不过遇到极端情况,比如你要先按第一列排,再按第二列排,第二列值相同的情况下,稳定排序能保留第一列的相对顺序。这时候可以把排序算法指定为mergesort,它的稳定性更好。代码这样写:
df_sorted_stable = df.sort_values( by=['region', 'sales'], ascending=[True, False], kind='mergesort' )当然,普通情况下用默认的quicksort就够了,只有当你发现排序完,相同第二列的值,第一列顺序被打乱,而且这个顺序对你重要时,才需要切换kind参数。
2.3 按索引排序和索引重置
除了按值排序,DataFrame还能按索引排序,用sort_index()。这个功能在处理分组结果、合并结果时会很实用。比如你用groupby和sum之后,索引变成了分组字段,拿到手是乱序的,想恢复按某列顺序,可以直接sort_index(),或者reset_index()之后再按列排序。
这里要单独强调一个高频操作:reset_index。因为sort_values之后,行的物理顺序变了,但行索引还是原来的,你如果直接按位置遍历,索引会显得杂乱。例如原来第0行是“B”,排序后“B”变成了第2行,但是它的索引还是1。凡是后续要按整数位置切片、给结果加序号、把表导出给其他人看,都建议先执行:
df_final = df.sort_values('sales', ascending=False).reset_index(drop=True)这样索引从0开始重新连续编号,顺序和展示出来的一致。drop=True的意思是不要保留原来的索引作为新列。如果你不想丢,可以不加drop,旧索引会变成一列index,这在某些追溯场景下反而有用。
3. 排名功能深度解析:rank方法的参数与策略
3.1 默认排名和五种排名方式到底有什么区别
rank方法最让我感慨的是它太容易被低估,很多人只知道df['score'].rank(),默认得到“平均排名”,却不知道还有四种别的策略。我用一个最典型的例子来说明:
import pandas as pd df = pd.DataFrame({ 'name': ['A', 'B', 'C', 'D', 'E'], 'score': [90, 90, 85, 85, 95] }) df['rank_avg'] = df['score'].rank() df['rank_min'] = df['score'].rank(method='min') df['rank_max'] = df['score'].rank(method='max') df['rank_first'] = df['score'].rank(method='first') df['rank_dense'] = df['score'].rank(method='dense') print(df)我们来看这一组数据:90分出现了两次,85分出现了两次,95分一次。默认method='average'的结果是:95分排名第1,两个90分都排名第2.5,两个85分都排名第4.5。名次是小数,像实际生活中的并列取平均分。method='min'就是并列的最小名次,两个90分都是第2名,两个85分都是第4名,整体名次为1、2、2、4、4,空缺了名次3和5。method='max'则是取并列的最大名次,90分都是第3名,85分都是第5名,名次为1、3、3、5、5。method='first'的顺序处理就与众不同了:它会按值出现的先后顺序,先出现的90分排第2,后出现的90分排第3,85分同理排第4和第5,即名次1、2、3、4、5,看起来就像是排序后的编号。method='dense'也是唯一的处理:它像“密集排名”,90分并列第2,85分并列第3,名次为1、2、2、3、3,名次是连续整数。
用花名册类比:如果你的老板需要给员工发奖金,名次并列但人数不同时,min可能是“冠军2人,季军1人”,dense则是“前两名都有奖,下一档也算第三”。不同规则直接影响后续计算,所以不能随手写默认。
3.2 排名的方向、缺失值和百分比排名
rank的ascending参数和排序是相反逻辑,需要注意。比如你要按销售额排名,销售额越大名次越小,那就用默认的ascending=True,因为rank默认认为值越大,名次越小,也就是升序排列名次。如果你希望值越大名次越大,则需要ascending=False。很多人在这一点上绕晕,我自己的记忆方法:rank默认得到的排名就是“数值从小到大,名次从小到大”。想得到“销量越高名次越高”,就要ascending=False。
缺失值的处理用na_option参数。默认情况下,NaN在排名结果中仍然是NaN,也就是缺失值不参与排名。若想把缺失值放到最后面,用na_option='bottom';把缺失值放到最前面,用na_option='top'。不过na_option只有在ascending=True时才有明显的效果,如果ascending=False,na_option的“bottom/top”会按照反向逻辑执行。为了避免踩坑,建议先把缺失值单独处理掉,再考虑排名。
还有一个使用率很高的参数pct=True,它会把排名结果转换为0到1之间的百分比排名。比如df['score'].rank(pct=True)返回的就是累计百分比,可以直接作为“前百分之多少”的筛选依据。这在做绩效评级时特别有用,比如后30%要改进,前10%要奖励,用百分比排名一列就够。
3.3 分组排名:groupby和rank的配合
真实数据基本都有分组,比如一个公司多个销售团队,你要的是每个人在团队内的排名,而不是整个公司的排名。这时候要用groupby加rank:
df['rank_in_group'] = df.groupby('team')['sales'].rank(ascending=False)这里代码返回的Series,索引和原DataFrame对齐,这个天然就匹配,可以直接作为新列。需要注意,如果groupby之后又用了transform,写法会更通用:
df['rank_in_group'] = df.groupby('team')['sales'].transform(lambda x: x.rank(ascending=False))两种写法效果相近,但transform更明确,尤其当你要在分组内同时做多种计算时,用transform可以组合多个lambda表达式。分组排名有个隐藏好处:它不会因为分组字段本身是字符串或者顺序不同造成结果错乱,因为groupby会先把分组字段整理好,再对每个组分别调用rank。
分组排名之后,如果你想筛选每个组排名前3的用户,可以配合布尔索引:
df_top3 = df[df['rank_in_group'] <= 3]这里有个细节:如果你们用的是dense方法,那么如果前4名并列第二,rank_in_group <= 3会把所有并列第二的也算进来,数量可能超过3个。这是正确的逻辑,需要业务方确认是否接受这种并列情况。如果要求严格取前3行,应该先排序再用groupby加head(3),或者用method='first'保证名次唯一。
4. 实战案例:销售数据排序与排名分析
4.1 构造一份可直接复现的模拟数据
为了把前面的知识点串起来,我造一份销售数据,包含销售日期、地区、负责人、产品、销售额、订单数六个字段。为了让大家跑出来和我一致,我固定了随机种子,数据量是20行。代码:
import pandas as pd import numpy as np np.random.seed(42) n = 20 df = pd.DataFrame({ 'date': pd.date_range('2025-01-01', periods=n, freq='D'), 'region': np.random.choice(['华东', '华北', '华南', '西南'], n), 'sales_name': np.random.choice(['张三', '李四', '王五', '赵六'], n), 'product': np.random.choice(['手机', '电脑', '耳机', '平板'], n), 'sales': np.random.randint(1000, 8000, n).astype(float), 'orders': np.random.randint(1, 30, n) }) # 人为制造一个缺失值,方便演示 df.loc[5, 'sales'] = np.nan print(df.head(10))这份数据是典型的“订单流水表”,有四个地区、四个产品、四个销售,日期连续。造数据时埋了一个NaN在第6行(索引5)的销售额里,后面会重点讲缺失值在排序、排名中的表现。
4.2 先用排序解决“排行榜”问题
第一个需求:老板要看销售额最高的前10条销售记录。这时候用排序最合适:
top10 = df.sort_values('sales', ascending=False).head(10) print(top10)注意一点:因为我们人为制造了NaN,所以排序时默认情况下NaN会排到最后面,也就是说它会出现在head(10)的结果之外,不会干扰前10条,这是常规预期。但如果数据量大,你希望把缺失值单独拎出来看,可以按na_position='first'把NaN排最前面,或者直接dropna()之后再做排序。
第二个需求:按地区排序,地区内部再按销售额降序。这就是多列排序:
df_sorted = df.sort_values( by=['region', 'sales'], ascending=[True, False], na_position='last' ) print(df_sorted.head(10))这样出来的结果是华东区先按销售额从大到小排好,然后是华北区、华南区、西南区,每个区内部条理分明。这里我加了na_position='last',确保缺失值排在每组最后。
第三个需求:因为排序后索引乱掉了,要另存一列保留原始顺序,或者重置索引。建议:
df_sorted = df_sorted.reset_index(drop=True)如果你需要知道每一行原来的位置,可以在排序前加一列original_index = df.index,这样更清晰。
4.3 再用排名解决“名次计算”问题
现在需求升级:老板要每个负责人在全公司所有销售记录中的销售额百分位排名,还要看地区内的名次。
先算全体排名。因为销售额是“越大越好”,所以要用rank(ascending=False),让销售额高的排在名次小的位置:
df['sales_rank_global'] = df['sales'].rank(ascending=False, na_option='keep')这里我用了na_option='keep',意思是缺失值排名保持NaN。如果你不想让NaN影响其他值的百分比,这个参数很关键。全体百分位排名:
df['sales_pct_rank'] = df['sales'].rank(pct=True, ascending=False)此时sales_pct_rank表示每个值在越高的销售额中,比例越接近0。比如销售额最高的记录,pct约为0.026左右,意味着它超过了约97%的数据。
接下来算地区内排名。需求是“每个地区内部,按销售额排一下名次”:
df['sales_rank_region'] = df.groupby('region')['sales'].rank(ascending=False)因为每个地区只有4到5条记录,所以排名在1到5之间,NaN对应的也为NaN。这个新列可以直接用于后续筛选,比如选出每个地区销售额第一的记录:
top_region_sales = df[df['sales_rank_region'] == 1] print(top_region_sales)这里有个细节需要留心:如果某个地区有两条销售额相同的最高记录,默认rank会都得到1(或者1.5),那这两条都会被筛出来。如果你只想要一条,需要指定合适的方法,或者接受并列事实。
最终我们把前面算好的排名列和原始数据一起导出:
df_final = df.sort_values('sales', ascending=False, na_position='last').reset_index(drop=True) print(df_final[['date', 'region', 'sales_name', 'sales', 'sales_rank_global', 'sales_rank_region']])通过这个案例你会发现,排序和排名其实会在同一个流程里配合出现:排序决定列表展示顺序,排名的计算结果则用于筛选、分档、汇总。
5. 常见坑与排查技巧实录
5.1 排序时NaN会跑到哪里去
sort_values默认是把NaN排到最后,na_position='first'可以把它排前面。这个看起来简单,但多列排序时,如果你把na_position当成全局设置,就会犯迷糊。实际上na_position只作用于当前列的排序,不是整张表的统一行为。按多列排时,如果主排序列有缺失,NaN会按你的设置进入第一波排列,然后次排序列再对非NaN部分进行排序。我这里处理业务数据的习惯是:排序前先看一下每列的缺失情况,df.isna().sum(),再决定需不需要先fillna或者dropna,而不是依赖na_position到处救场。
5.2 排名并列导致的结果偏差
并列值是排名中最容易出事的地方。默认平均排名会产生0.5这种小数,当你用== 1去筛选时,如果第一名的数据有两条,两条都会被排成1.5,结果一个都筛不中。这时候要明确业务预期。如果只是想“找出最大值”,别用rank,直接按条件筛选df['sales'] == df['sales'].max()更直接。如果确实需要唯一名次,就选method='first',它按出现顺序打破平局;如果不需要空号,选dense;如果愿意接受并列取最小名次,选min。我经历过一次统计事故,就是默认排名是平均,然后业务方拿名次去做绩效表,出现了半名次,最后整个表作废。之后凡是要求名次必须为整数的场景,我一律显式写method='min'或method='dense'。
5.3 groupby+rank之后索引对不齐的问题
很多人会用groupby('region')['sales'].rank(),然后把这个结果直接赋值给原DataFrame的新列,这是OK的,因为rank返回的Series索引和原df索引对齐。但如果分组时用了sort=False或者某些情况下索引被重置,就容易对不齐。稳妥的写法是用transform,它保证返回的Series长度和原df一致,索引也天然对齐。举一个反面案例:
# 有时候这样写会导致索引错位或长度不匹配 rank_series = df.groupby('region').apply(lambda x: x['sales'].rank()).reset_index(drop=True)这样的结果可能长度不同或顺序乱了,再用df['rank'] = rank_series就会出现索引错位。如果非要走apply,记得把索引按原df顺序重排。实际工作中我几乎不用apply做分组排名,因为transform快且稳。
5.4 排序后按位置取数导致的错误
sort_values之后,如果你用iloc[0:5]取前几条,这是按行物理位置取的,得到的是新顺序的前5行,没问题。但如果你用loc[0:5],这是按索引标签取的,此时索引可能已经乱了,取到的就不是你想的那5行。我遇到过好几次这种低级错误:排序后没有重置索引,看到结果里索引是5、8、2、13这种乱序,然后用loc[0:5]去取,结果取出来全是原索引为0到5的行。排查半天,最后才发现索引问题。所以,如果你打算在排序后继续做切片或者和别的表合并,先确认索引情况,要么reset_index(drop=True),要么备份原索引列。特别是和外部数据merge时,索引对不齐会导致重复或错行,这是pandas最隐蔽的坑之一。
5.5 排序和排名混用时的判断技巧
我自己的经验是:需要展示结果、取前几个、按某种顺序输出报表,用排序;需要给每个观测值一个名次、进行分位切割、统计各名次区间人数,用排名。如果一眼看不出来,就假设你要做一张表,表里有一列是“排名”,那就用rank;如果只要表按某个值从大到小排着,那就算sort。另外,sort_values之后再配合reset_index,其实等价于很多场景下的rank(method='first'),但两者语义不同,前者是为了顺序,后者是为了命名次,这点千万记住。
最后再分享一个小技巧:如果你想把rank结果直接转换成排序后的行顺序,可以用df.sort_values('sales', ascending=False).reset_index(drop=True).index + 1得到类似method='first'的名次;想保留并列时的真实名次,用rank(method='min')。我在实际项目中经常先用rank(method='min')算出名次,再用名次做业务分档,比如名次小于等于3算A级,4到10算B级,大于10算C级,这样比手动排序再截断要灵活得多。排序和排名表面上是两行API,背后是两种完全不同的数据处理思路,用对了会让后续分析顺滑很多。