Pandas DataFrame排序与排名:sort_values和rank的实战区别与用法
在数据分析里排序和排名几乎是每天都要碰的操作但很多人用着用着就混了。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(dropTrue)把序号理干净这就是最直接的做法。这里顺序本身就是结果不需要再生成名次。如果是“每个门店的销售额在全部门店中排名第几”而且以后还要根据这些名次做汇总、筛选、画图那就必须用rank生成一个排名列。因为这列数据可以继续参与运算比如筛选排名前20%的门店、按排名区间分组等。而排序的结果只是一张换了顺序的表没法直接当数值用。举个混合场景你既要做排行榜又要算每个业务员的绩效系数。排行榜必然用排序绩效系数要根据销售额在整个团队中的百分位来给这种就得先rank(pctTrue)再用排名列做区间映射。这两个动作经常出现在同一张表上但目的完全不同混用了就会出现“看着对算完错”的尴尬。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(byscore) print(df_sorted)输出结果里行会按score从小到大排。注意sort_values默认返回一个新DataFrame原表df的顺序不变。如果你想把结果直接覆盖原变量可以写成df.sort_values(byscore, inplaceTrue)但我个人不太推荐用inplaceTrue。原因很简单第一pandas后来很多方法都倾向于不原地修改保持一致第二一旦原地改错了找不回原始数据尤其是你在调试脚本时最好让每一步都返回一个新对象方便对比。升序和降序用ascending参数控制df_sorted_desc df.sort_values(byscore, ascendingFalse)这里还有个容易忽略的点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], kindmergesort )当然普通情况下用默认的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, ascendingFalse).reset_index(dropTrue)这样索引从0开始重新连续编号顺序和展示出来的一致。dropTrue的意思是不要保留原来的索引作为新列。如果你不想丢可以不加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(methodmin) df[rank_max] df[score].rank(methodmax) df[rank_first] df[score].rank(methodfirst) df[rank_dense] df[score].rank(methoddense) print(df)我们来看这一组数据90分出现了两次85分出现了两次95分一次。默认methodaverage的结果是95分排名第1两个90分都排名第2.5两个85分都排名第4.5。名次是小数像实际生活中的并列取平均分。methodmin就是并列的最小名次两个90分都是第2名两个85分都是第4名整体名次为1、2、2、4、4空缺了名次3和5。methodmax则是取并列的最大名次90分都是第3名85分都是第5名名次为1、3、3、5、5。methodfirst的顺序处理就与众不同了它会按值出现的先后顺序先出现的90分排第2后出现的90分排第385分同理排第4和第5即名次1、2、3、4、5看起来就像是排序后的编号。methoddense也是唯一的处理它像“密集排名”90分并列第285分并列第3名次为1、2、2、3、3名次是连续整数。用花名册类比如果你的老板需要给员工发奖金名次并列但人数不同时min可能是“冠军2人季军1人”dense则是“前两名都有奖下一档也算第三”。不同规则直接影响后续计算所以不能随手写默认。3.2 排名的方向、缺失值和百分比排名rank的ascending参数和排序是相反逻辑需要注意。比如你要按销售额排名销售额越大名次越小那就用默认的ascendingTrue因为rank默认认为值越大名次越小也就是升序排列名次。如果你希望值越大名次越大则需要ascendingFalse。很多人在这一点上绕晕我自己的记忆方法rank默认得到的排名就是“数值从小到大名次从小到大”。想得到“销量越高名次越高”就要ascendingFalse。缺失值的处理用na_option参数。默认情况下NaN在排名结果中仍然是NaN也就是缺失值不参与排名。若想把缺失值放到最后面用na_optionbottom把缺失值放到最前面用na_optiontop。不过na_option只有在ascendingTrue时才有明显的效果如果ascendingFalsena_option的“bottom/top”会按照反向逻辑执行。为了避免踩坑建议先把缺失值单独处理掉再考虑排名。还有一个使用率很高的参数pctTrue它会把排名结果转换为0到1之间的百分比排名。比如df[score].rank(pctTrue)返回的就是累计百分比可以直接作为“前百分之多少”的筛选依据。这在做绩效评级时特别有用比如后30%要改进前10%要奖励用百分比排名一列就够。3.3 分组排名groupby和rank的配合真实数据基本都有分组比如一个公司多个销售团队你要的是每个人在团队内的排名而不是整个公司的排名。这时候要用groupby加rankdf[rank_in_group] df.groupby(team)[sales].rank(ascendingFalse)这里代码返回的Series索引和原DataFrame对齐这个天然就匹配可以直接作为新列。需要注意如果groupby之后又用了transform写法会更通用df[rank_in_group] df.groupby(team)[sales].transform(lambda x: x.rank(ascendingFalse))两种写法效果相近但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)或者用methodfirst保证名次唯一。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, periodsn, freqD), 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, ascendingFalse).head(10) print(top10)注意一点因为我们人为制造了NaN所以排序时默认情况下NaN会排到最后面也就是说它会出现在head(10)的结果之外不会干扰前10条这是常规预期。但如果数据量大你希望把缺失值单独拎出来看可以按na_positionfirst把NaN排最前面或者直接dropna()之后再做排序。第二个需求按地区排序地区内部再按销售额降序。这就是多列排序df_sorted df.sort_values( by[region, sales], ascending[True, False], na_positionlast ) print(df_sorted.head(10))这样出来的结果是华东区先按销售额从大到小排好然后是华北区、华南区、西南区每个区内部条理分明。这里我加了na_positionlast确保缺失值排在每组最后。第三个需求因为排序后索引乱掉了要另存一列保留原始顺序或者重置索引。建议df_sorted df_sorted.reset_index(dropTrue)如果你需要知道每一行原来的位置可以在排序前加一列original_index df.index这样更清晰。4.3 再用排名解决“名次计算”问题现在需求升级老板要每个负责人在全公司所有销售记录中的销售额百分位排名还要看地区内的名次。先算全体排名。因为销售额是“越大越好”所以要用rank(ascendingFalse)让销售额高的排在名次小的位置df[sales_rank_global] df[sales].rank(ascendingFalse, na_optionkeep)这里我用了na_optionkeep意思是缺失值排名保持NaN。如果你不想让NaN影响其他值的百分比这个参数很关键。全体百分位排名df[sales_pct_rank] df[sales].rank(pctTrue, ascendingFalse)此时sales_pct_rank表示每个值在越高的销售额中比例越接近0。比如销售额最高的记录pct约为0.026左右意味着它超过了约97%的数据。接下来算地区内排名。需求是“每个地区内部按销售额排一下名次”df[sales_rank_region] df.groupby(region)[sales].rank(ascendingFalse)因为每个地区只有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, ascendingFalse, na_positionlast).reset_index(dropTrue) print(df_final[[date, region, sales_name, sales, sales_rank_global, sales_rank_region]])通过这个案例你会发现排序和排名其实会在同一个流程里配合出现排序决定列表展示顺序排名的计算结果则用于筛选、分档、汇总。5. 常见坑与排查技巧实录5.1 排序时NaN会跑到哪里去sort_values默认是把NaN排到最后na_positionfirst可以把它排前面。这个看起来简单但多列排序时如果你把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()更直接。如果确实需要唯一名次就选methodfirst它按出现顺序打破平局如果不需要空号选dense如果愿意接受并列取最小名次选min。我经历过一次统计事故就是默认排名是平均然后业务方拿名次去做绩效表出现了半名次最后整个表作废。之后凡是要求名次必须为整数的场景我一律显式写methodmin或methoddense。5.3 groupbyrank之后索引对不齐的问题很多人会用groupby(region)[sales].rank()然后把这个结果直接赋值给原DataFrame的新列这是OK的因为rank返回的Series索引和原df索引对齐。但如果分组时用了sortFalse或者某些情况下索引被重置就容易对不齐。稳妥的写法是用transform它保证返回的Series长度和原df一致索引也天然对齐。举一个反面案例# 有时候这样写会导致索引错位或长度不匹配 rank_series df.groupby(region).apply(lambda x: x[sales].rank()).reset_index(dropTrue)这样的结果可能长度不同或顺序乱了再用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(dropTrue)要么备份原索引列。特别是和外部数据merge时索引对不齐会导致重复或错行这是pandas最隐蔽的坑之一。5.5 排序和排名混用时的判断技巧我自己的经验是需要展示结果、取前几个、按某种顺序输出报表用排序需要给每个观测值一个名次、进行分位切割、统计各名次区间人数用排名。如果一眼看不出来就假设你要做一张表表里有一列是“排名”那就用rank如果只要表按某个值从大到小排着那就算sort。另外sort_values之后再配合reset_index其实等价于很多场景下的rank(methodfirst)但两者语义不同前者是为了顺序后者是为了命名次这点千万记住。最后再分享一个小技巧如果你想把rank结果直接转换成排序后的行顺序可以用df.sort_values(sales, ascendingFalse).reset_index(dropTrue).index 1得到类似methodfirst的名次想保留并列时的真实名次用rank(methodmin)。我在实际项目中经常先用rank(methodmin)算出名次再用名次做业务分档比如名次小于等于3算A级4到10算B级大于10算C级这样比手动排序再截断要灵活得多。排序和排名表面上是两行API背后是两种完全不同的数据处理思路用对了会让后续分析顺滑很多。