用 pandas 做数据排序这事儿,看起来就是个sort_values的事,但真到了实战里,单列排序、多列排序、按索引排、缺失值怎么放、字符串怎么按规则排、排序后索引乱不乱……每个点都能卡你一下。我自己刚用 pandas 处理数据那会儿,就被“排序后索引乱了”“明明按日期排了怎么还是乱序”“中文字段排序不对”这些问题整得头大,后来把排序相关的 API 和参数彻底摸了一遍,才发现很多问题根本不是代码写错,而是没搞懂 pandas 排序的底层逻辑。这篇就把我这些年用 pandas 做数据排序的实操经验完整盘一遍,从最常用的sort_values到sort_index、rank,从参数细节到实战坑点,适合刚入门 pandas 的初学者,也适合已经写了一段时间 pandas 但想系统梳理排序操作的朋友,照着敲一遍基本能覆盖你日常 95% 的排序需求。
1. pandas 排序的三种核心方式与选型思路
先说结论:pandas 里做排序主要就三个 API,sort_values按值排序、sort_index按索引排序、rank做排名。很多人以为排序就是sort_values一把梭,但实际上另外两个在特定场景里非常顶用。把三者的适用场景先搞清楚,后面写代码的时候才能选得准。
1.1 sort_values:最常用的值排序
sort_values是绝大多数场景下的首选,它的核心逻辑就是“按某一列(或多列)的取值大小对整行进行重排”。比如你有一个用户订单表,想按消费金额从高到低看哪些是大客户;再比如你有一份商品表,想按销量排个序,这些都是sort_values的活。
我自己的使用经验是,sort_values用得好不好,关键在于能不能把by参数和ascending参数组合明白。by决定“按哪一列排”,ascending决定“升序还是降序”,这两个参数一组合,基本上单表排序的需求全都能满足。
import pandas as pd df = pd.DataFrame({ '用户': ['张三', '李四', '王五', '赵六'], '消费金额': [128.5, 89.9, 256.0, 47.5], '下单次数': [5, 3, 8, 2] }) # 按消费金额降序排序 df.sort_values(by='消费金额', ascending=False)这个例子最简单,但已经能看出sort_values的核心价值:通过指定列的值来重排整个 DataFrame 的行顺序。
sort_values真正强大的是多列排序。比如你先按“下单次数”降序,再按“消费金额”降序,这样下单次数相同的人里边,消费金额高的排在前面。这种“先按 A 列排,A 列相同再按 B 列排”的需求,业务里非常常见。
df.sort_values(by=['下单次数', '消费金额'], ascending=[False, False])注意这里by传入的是列表,ascending传入的也是列表,两个列表一一对应。这是一个非常容易写错的地方,后面我在常见问题里会专门讲。
1.2 sort_index:排序索引的隐藏技巧
sort_index是按索引排序,而不是按列值排序。索引可能是默认的行号(0、1、2...),也可能是你手动指定的列(比如用户ID、日期、商品编号)。
为什么需要按索引排序?最常见的一个场景是:你做了一系列数据操作(比如groupby、merge、concat),索引被打乱了,这时候你想让数据回到“按某个键有序”的状态,用sort_index就非常方便。
df_sorted = df.sort_values('消费金额', ascending=False) # 排序后索引乱了,想恢复成原来的顺序?用 sort_index df_restored = df_sorted.sort_index()另外一个典型应用是时间序列数据。如果你把日期设为索引,然后想按时间顺序查看数据,直接sort_index()就完成了,不需要单独指定列。
df_time = pd.DataFrame({ '日期': ['2024-03-15', '2024-01-10', '2024-02-20', '2024-04-01'], '数值': [10, 20, 30, 40] }) df_time['日期'] = pd.to_datetime(df_time['日期']) df_time = df_time.set_index('日期') df_time.sort_index()sort_index还有一个妙用:当你想把拼接好的数据恢复成原始顺序时,比如pd.concat之后,用sort_index()可以快速复原。我以前踩过这个坑——concat了两个 DataFrame,结果行顺序全乱了,后来才发现用sort_index()一键解决。
1.3 rank:排序之外的排名需求
rank不是排序,而是排名。它不改变数据的顺序,而是给每一行计算一个名次。很多新手分不清“排序”和“排名”,这里我解释一下:
- 排序:把数据按照大小重新排列顺序,行与行的位置发生改变。
- 排名:给每一行数据一个“第几名”的标记,行的原有顺序保持不变。
业务上什么时候需要排名?比如你想知道每个销售员的业绩排名,但你还想保留原始的数据顺序(比如按工号排),这时候你不想动 DataFrame 的行顺序,只想加一列“排名”,那就用rank而不是sort_values。
df['金额排名'] = df['消费金额'].rank(ascending=False)rank有几个重要参数:method处理并列排名的方式(average是平均排名,min是最小排名,max是最大排名,first是先后顺序排名),ascending控制升序降序。
举个具体的例子:两个人消费金额一样,都是 100 元,如果排名用平均法,两个人都是 1.5 名;如果用最小法,两个人都算第 1 名;如果用最大法,两个人都算第 2 名。这个细节在业务报表里经常让人困惑,你得根据业务需求选择合适的method。
2. 核心参数详解与实操要点
如果你已经用 pandas 做过一些基础的数据处理,那你对sort_values的参数肯定不陌生,但很多参数藏在文档里没人讲。我把最核心的几个参数展开讲透,这些参数用对了,你的排序代码会干净很多。
2.1 by 参数:排序列的指定方式
by的值可以是字符串,也可以是字符串列表。
- 单列排序:
by='列名',此时ascending可以传一个布尔值True或False。 - 多列排序:
by=['列A', '列B'],此时ascending需要传一个列表,长度与by一致。
# 单列排序 df.sort_values(by='消费金额', ascending=False) # 多列排序 df.sort_values(by=['下单次数', '消费金额'], ascending=[False, False])多列排序的优先级顺序是:by列表里的第一个列优先级最高,先按它排,排完之后,在那一列值相同的情况下,再按第二列排,依此类推。这个逻辑跟 Excel 里的“主要关键字”“次要关键字”是一样的。
如果你在by里传了一个不存在的列名,pandas 会直接抛KeyError,这个报错信息会明确告诉你是哪个列出了问题,排查起来还比较快。
2.2 ascending 与 inplace:方向控制和原地修改的坑
ascending参数可能是理解成本最低但坑最多的参数:
ascending=True:升序,从小到大。ascending=False:降序,从大到小。- 多列排序时,
ascending传列表,例如[True, False]表示第一列升序,第二列降序。
inplace这个参数是一个大坑。早期 pandas 版本里inplace=True很常见,表示直接修改原始 DataFrame;如果不设置,返回的是一个新的 DataFrame,原数据不变。
我强烈建议:不要用inplace=True。原因有两点:第一,pandas 官方在后续版本中已经逐步弱化inplace参数,未来的趋势是统一使用链式操作;第二,inplace=True容易让你在写代码的时候忘记赋值,导致后续代码还拿着旧数据在用,排查起来特别烦。
正确做法:
df_sorted = df.sort_values(by='消费金额', ascending=False) # 后续用 df_sorted 做操作而不是:
df.sort_values(by='消费金额', ascending=False, inplace=True)单看无害,但如果你在一个很长的数据处理管线里用inplace=True,后面某个环节发现数据不符合预期,你很难判断到底是哪一步该变没变。
2.3 na_position 与 key:缺失值处理和自定义排序规则
na_position控制缺失值排在什么位置,只有两个可选值:'last'(默认)和'first'。
na_position='last':缺失值排在最后面。na_position='first':缺失值排在最前面。
df_with_na = pd.DataFrame({ '商品': ['A', 'B', 'C', 'D'], '销量': [100, None, 50, 200] }) df_with_na.sort_values(by='销量', na_position='first')这在你处理真实数据(尤其是从数据库导出的数据)时非常常见,因为数据库里经常有NULL值。默认情况下缺失值排最后,但如果你想先查看缺失数据,用na_position='first'就很方便。
key参数是 pandas 排序里一个被低估的功能,它允许你在排序之前对列的值应用一个函数,然后按函数处理后的结果排序。比如你有一列是字符串形式的数字('10'、'9'、'2'),默认按字符串排序会得到 '10'、'2'、'9',这不是数值顺序。这时候用key参数传一个转成数字的函数就解决问题了。
df_mixed = pd.DataFrame({ '编号': ['A10', 'A9', 'A2', 'A1'], '数值': [1, 2, 3, 4] }) df_mixed.sort_values(by='编号', key=lambda col: col.str.extract('(\d+)').astype(int)[0])再比如你想不区分大小写地按字母排序,用key=lambda col: col.str.lower()就能实现。
3. 从数据清洗到业务场景的完整实操
这一部分我把排序操作放到完整的实战场景里来走一遍。因为排序从来不是孤立的一步,它总是跟数据读取、清洗、类型转换、索引设置、结果导出连在一起。
3.1 准备数据:先掌握创建 DataFrame 的常用姿势
排序操作的第一步是有一份数据。很多新手在练习排序时卡在“数据从哪来”的问题上,所以我先列几种创建测试数据的常用姿势。
import pandas as pd import numpy as np # 方式一:直接通过字典创建 df1 = pd.DataFrame({ '城市': ['北京', '上海', '广州', '深圳'], 'GDP': [41610, 43900, 28839, 30665], '人口': [2189, 2487, 1874, 1756] }) # 方式二:通过列表嵌套创建 df2 = pd.DataFrame([ ['北京', 41610, 2189], ['上海', 43900, 2487], ['广州', 28839, 1874], ['深圳', 30665, 1756] ], columns=['城市', 'GDP', '人口']) # 方式三:用 numpy 生成随机数据模拟真实场景 df3 = pd.DataFrame({ '日期': pd.date_range('2024-01-01', periods=10, freq='D'), '销量': np.random.randint(50, 200, size=10), '价格': np.round(np.random.uniform(10, 100, size=10), 2) })第三种方式在练习和测试时特别爽,直接造出一份看起来像模像样的数据,不用手动敲一大堆数字。
这里提醒一个点:在排序之前,先确认列的数据类型是不是想要的。比如用read_csv读进来之后,数值列可能会变成字符串(尤其是带千分位逗号的数字),这时候排序会完全不对,必须先做类型转换。数据类型的确认和转换是数据预处理里的基础,排序踩坑十有八九是类型问题。
3.2 单列排序与多列排序的完整流程
假设你现在有一份销售明细数据,需要按销售额排个名次,再看哪个区域的销售最好。
sales_data = pd.DataFrame({ '区域': ['华东', '华南', '华北', '华东', '华南', '华北'], '销售员': ['张伟', '王强', '李娜', '刘洋', '陈晨', '赵敏'], '销售额': [12000, 15000, 9000, 18000, 16000, 11000], '订单数': [23, 30, 15, 35, 28, 20] }) # 第一步:按销售额降序查看销售排名 sales_rank = sales_data.sort_values('销售额', ascending=False) print(sales_rank) # 第二步:按区域排序,区域相同的再按销售额降序排 sales_by_region = sales_data.sort_values(['区域', '销售额'], ascending=[True, False]) print(sales_by_region)第一步的代码输出结果一目了然,销售额最高的刘洋排在第一。第二步就体现多列排序的价值了,先按区域分块,每个区域内销售业绩最高的人排在该区域的最前面。这种表格拿去给业务方看,他们可以快速找到每个区域的头牌销售员。
我再补充一个场景:有时候你需要“索引也保持有序”,比如排序列的序号。默认情况下sort_values会把索引一起带走,导致索引顺序跟数据顺序不一致。如果你想让索引重新按 0 到 n-1 排,加一个reset_index(drop=True)。
sales_rank = sales_data.sort_values('销售额', ascending=False).reset_index(drop=True)drop=True的意思是丢弃原有索引,不把它作为一个新列保留。如果你不写drop=True,原索引会变成一个新列叫index。
3.3 时间序列数据与索引重排实战
时间序列数据是排序的高频场景——日志数据按时间排序、股票数据按日期排序、订单数据按下单时间排序。
时间序列排序最容易踩的坑是:日期列是字符串类型,而不是真正的时间类型。字符串排序和日期排序的结果在某些格式下看起来一样,但换一种格式就可能完全不对。
# 错误示范:日期是字符串,按字符串排序会出问题 df_log = pd.DataFrame({ '时间': ['2024-01-02 10:30', '2024-01-02 09:15', '2024-01-01 23:59'], '事件': ['登录', '下单', '注册'] }) # 字符串按ASCII排序,像 '2024-01-02' 会排在 '2024-01-01' 后面,但同一天内 10:30 和 09:15 的顺序是错的 df_log.sort_values('时间')输出会得到:
时间 事件 2 2024-01-01 23:59 注册 1 2024-01-02 09:15 下单 0 2024-01-02 10:30 登录这个例子刚好碰巧是对的(因为字符串从小时角度看 09 小于 10)。但如果你混用'2024-1-2'这种不带前导零的格式,字符串排序就会出问题:'2024-1-10'会排在'2024-1-2'前面,因为第二个字符'1'小于'2',这显然是错的。
正确做法是先把字符串转成datetime64类型,再排序:
df_log['时间'] = pd.to_datetime(df_log['时间']) df_log.sort_values('时间')一旦转成时间类型,sort_values就按照时间先后排序,万无一失。
另一个常见操作是“把时间列设为索引,然后按时间排序”。设完索引之后直接sort_index(),代码非常简洁。
df_log = df_log.set_index('时间') df_log.sort_index()时间序列排序后的一个常见后续操作是“重采样”,比如按月汇总销量。如果你没排序就resample,pandas 有时候会直接报错或者得出错误结果。所以我的习惯是:一旦数据里有时间列,第一件事就是转成datetime并排序,后面所有时间相关的操作都稳了。
3.4 利用 rank 实现排名需求
rank在业务场景中非常高频。比如公司每个月的销售排行榜,除了要看出谁第一谁第二,还需要保留原始名单顺序。
假设你有一份员工绩效表,想给每个人的绩效打个分数,然后算排名。这个排名要按分数从高到低,分数最高的排第 1 名。
performance = pd.DataFrame({ '员工编号': ['E001', 'E002', 'E003', 'E004', 'E005'], '绩效分数': [85, 92, 78, 92, 88] }) performance['排名'] = performance['绩效分数'].rank(ascending=False, method='min') print(performance)注意这里有两个人都是 92 分,用method='min'的话,两个人都排第 1 名,下一个人从第 3 名开始。如果用的是默认的method='average',两个人都排 1.5 名,这在业务报表里会显得很奇怪。所以我在实际项目里,如果要给业务方展示排名,一般用method='min'或method='first',具体看业务上怎么定义并列名次。
如果你是做比赛评分这类场景,并列名次的后一名应该跳号,用min;如果追求“先到先得”,用first(并列的人按行顺序给不同的名次)。
rank也可以按多个字段组合排名,但需要先构造一个临时的综合列,比如“总分 = 业务分 + 客户评价分”,然后对总分做rank。这也是rank常用于“综合排名”的原因——排序往往只能按原始列,排名可以基于你临时算出来的新列。
4. 常见问题与排查技巧实录
最后这部分我把自己和身边朋友在实际开发中踩过的高频坑整理成速查表,每一个都是真实案例。你如果排序排序出问题,先来这里对号入座。
4.1 by 参数传错:字符串还是列表,傻傻分不清
很多人一开始记不住sort_values的by参数到底传字符串还是列表,记忆方法其实很简单:
- 只按一列排:传字符串或列表都行。
- 按多列排:必须传列表。
- 如果传了列表,
ascending必须也是列表,或者一个单独的布尔值(此时所有列都用同一个升降序)。
我自己见过最冤的一个报错是:多列排序时by=['列A', '列B']传了列表,但ascending=False传了单个布尔值,代码居然也能运行(pandas 会做广播),看起来结果好像对,但实际上两列都是降序。如果业务上要求一列升序一列降序,结果就错得离谱了。
# 错误:想要按A列升序、B列降序 df.sort_values(by=['A', 'B'], ascending=False) # 正确:必须传列表 df.sort_values(by=['A', 'B'], ascending=[True, False])所以我的建议是:by传列表时,ascending永远传等长列表,别偷懒。
4.2 inplace=True 的连锁反应
我之前负责一个数据处理模块,团队里的小伙伴在链式操作(多个 pandas 操作串在一起)中用了inplace=True,结果调试了整整一个下午找不到 bug。原因就是inplace=True直接修改原始数据,但链式表达式的返回值是None,如果后面继续接.head()或者.reset_index(),就会直接报'NoneType' object has no attribute 'head'。
# 错误:inplace=True 返回 None,不能继续链式调用 df.sort_values('销售额', ascending=False, inplace=True).head() # 正确:不传 inplace,链式调用 df.sort_values('销售额', ascending=False).head()这个错误的报错信息其实挺明确的,但如果你在一个几十行的数据处理函数里排查,很容易忽略是这一步的问题。我的经验是:统一风格,全项目都不使用inplace,所有操作都返回新对象并赋值。这样代码可读性更高,也更好调试。
4.3 字母大小写和混合类型排序的三个坑
第一个坑是字符串排序。pandas 的字符串排序是基于 ASCII 码的,大写字母排在前面,小写字母排在后面。如果你想把 Apple 和 apple 看成同一个等级来排序,默认行为是不符合预期的。
df_case = pd.DataFrame({'fruit': ['apple', 'Banana', 'cherry', 'Date']}) df_case.sort_values('fruit') # 输出顺序可能是:Banana、Date、apple、cherry因为大写字母 B(66)和 D(68)排在 a(97)和 c(99)前面。解决方法是key参数统一转小写:
df_case.sort_values('fruit', key=lambda col: col.str.lower())第二个坑是混合类型排序。当一列里既有数字又有字符串时(比如[1, 2, '3', 4]),pandas 虽然能排,但结果可能出乎意料,尤其是字符串 '10' 不会按数字 10 参与排序。这种问题的根源是数据录入不规范,建议先做数据清洗,把列转成统一的数值类型。
第三个坑是中文字符串排序。中文排序依赖 Unicode 编码,如果你想让中文按拼音或笔画排序,pandas 默认做不到,需要借助key参数调用locale.strxfrm或者用拼音库处理。但这属于相对小众的需求,绝大多数业务场景里,中文列主要是用来分组的,不是用来排序的。如果你真的需要,最简单的方式是先给数据加一列拼音或者拼音首字母,然后对这列排序。
# 示例:按拼音排(这里简化为首字母示例,实际可引入 pypinyin 库) df_cn = pd.DataFrame({'城市': ['北京', '上海', '广州', '深圳']}) df_cn['拼音首字母'] = ['bj', 'sh', 'gz', 'sz'] df_cn.sort_values('拼音首字母')4.4 排序后操作与性能优化的心得
排序之后有一件很容易被忽视的事:索引依然是原始顺序。如果你排序后直接拿去跟别的 DataFrame 做merge或concat,可能会导致行对不上。我的习惯是排序后立刻reset_index(drop=True),让索引重新从 0 开始,避免后面所有跟索引相关的操作踩坑。
df_sorted = df.sort_values('销售额', ascending=False).reset_index(drop=True)reset_index(drop=True)会丢弃原来的索引并生成新的默认整数索引,这样后续操作就都在一个干净的基础上进行。
性能方面的经验:如果你的数据量是几千行,随便排序都没问题;但到了几十万行、上百万行,排序时间就会变得肉眼可见。这个时候可以看看是不是必须先排序才能完成业务。比如求每组最大值,用groupby('列A')['列B'].max()通常比先排序再取第一条更快;再比如找 Top N,用nlargest比sort_values(ascending=False).head(n)更快,因为nlargest使用了更高效的算法,不会把全部数据排一遍。
# 取销售额最高的 3 行 top3 = sales_data.nlargest(3, '销售额') # 等价写法(但性能略差) top3_slow = sales_data.sort_values('销售额', ascending=False).head(3)大数据量场景下,nlargest比全量排序再取头部快很多。类似的还有nsmallest。
我个人的处理大数据排序的习惯是:先info()看数据量和类型,确认没有明显问题;再isna().sum()看缺失值;排序前确定好业务上需要的行列;排序后用head()抽查结果是否符合预期。这一套流程走下来,基本不会翻车。
最后再说一个我自己的习惯:排序完不要直接信,一定打印head()或者对重点行做个抽样看下。很多时候数据看着排对了,但实际因为类型问题或者多列排序的优先级理解不对,结果是错的。尤其是“先按 A 再按 B”的多列排序,最好打印出来拉一眼,确认 A 列相同的行内部已经按 B 列排好了。这种检查看起来多余,但实际能帮你省下大量最后做数据检查的时间。