ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Pandas数据排序实战:sort_values、sort_index与rank全解析

Pandas数据排序实战:sort_values、sort_index与rank全解析 用 pandas 做数据排序这事儿看起来就是个sort_values的事但真到了实战里单列排序、多列排序、按索引排、缺失值怎么放、字符串怎么按规则排、排序后索引乱不乱……每个点都能卡你一下。我自己刚用 pandas 处理数据那会儿就被“排序后索引乱了”“明明按日期排了怎么还是乱序”“中文字段排序不对”这些问题整得头大后来把排序相关的 API 和参数彻底摸了一遍才发现很多问题根本不是代码写错而是没搞懂 pandas 排序的底层逻辑。这篇就把我这些年用 pandas 做数据排序的实操经验完整盘一遍从最常用的sort_values到sort_index、rank从参数细节到实战坑点适合刚入门 pandas 的初学者也适合已经写了一段时间 pandas 但想系统梳理排序操作的朋友照着敲一遍基本能覆盖你日常 95% 的排序需求。1. pandas 排序的三种核心方式与选型思路先说结论pandas 里做排序主要就三个 APIsort_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消费金额, ascendingFalse)这个例子最简单但已经能看出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(消费金额, ascendingFalse) # 排序后索引乱了想恢复成原来的顺序用 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(ascendingFalse)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消费金额, ascendingFalse) # 多列排序 df.sort_values(by[下单次数, 消费金额], ascending[False, False])多列排序的优先级顺序是by列表里的第一个列优先级最高先按它排排完之后在那一列值相同的情况下再按第二列排依此类推。这个逻辑跟 Excel 里的“主要关键字”“次要关键字”是一样的。如果你在by里传了一个不存在的列名pandas 会直接抛KeyError这个报错信息会明确告诉你是哪个列出了问题排查起来还比较快。2.2 ascending 与 inplace方向控制和原地修改的坑ascending参数可能是理解成本最低但坑最多的参数ascendingTrue升序从小到大。ascendingFalse降序从大到小。多列排序时ascending传列表例如[True, False]表示第一列升序第二列降序。inplace这个参数是一个大坑。早期 pandas 版本里inplaceTrue很常见表示直接修改原始 DataFrame如果不设置返回的是一个新的 DataFrame原数据不变。我强烈建议不要用inplaceTrue。原因有两点第一pandas 官方在后续版本中已经逐步弱化inplace参数未来的趋势是统一使用链式操作第二inplaceTrue容易让你在写代码的时候忘记赋值导致后续代码还拿着旧数据在用排查起来特别烦。正确做法df_sorted df.sort_values(by消费金额, ascendingFalse) # 后续用 df_sorted 做操作而不是df.sort_values(by消费金额, ascendingFalse, inplaceTrue)单看无害但如果你在一个很长的数据处理管线里用inplaceTrue后面某个环节发现数据不符合预期你很难判断到底是哪一步该变没变。2.3 na_position 与 key缺失值处理和自定义排序规则na_position控制缺失值排在什么位置只有两个可选值last默认和first。na_positionlast缺失值排在最后面。na_positionfirst缺失值排在最前面。df_with_na pd.DataFrame({ 商品: [A, B, C, D], 销量: [100, None, 50, 200] }) df_with_na.sort_values(by销量, na_positionfirst)这在你处理真实数据尤其是从数据库导出的数据时非常常见因为数据库里经常有NULL值。默认情况下缺失值排最后但如果你想先查看缺失数据用na_positionfirst就很方便。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编号, keylambda col: col.str.extract((\d)).astype(int)[0])再比如你想不区分大小写地按字母排序用keylambda 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, periods10, freqD), 销量: np.random.randint(50, 200, size10), 价格: np.round(np.random.uniform(10, 100, size10), 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(销售额, ascendingFalse) print(sales_rank) # 第二步按区域排序区域相同的再按销售额降序排 sales_by_region sales_data.sort_values([区域, 销售额], ascending[True, False]) print(sales_by_region)第一步的代码输出结果一目了然销售额最高的刘洋排在第一。第二步就体现多列排序的价值了先按区域分块每个区域内销售业绩最高的人排在该区域的最前面。这种表格拿去给业务方看他们可以快速找到每个区域的头牌销售员。我再补充一个场景有时候你需要“索引也保持有序”比如排序列的序号。默认情况下sort_values会把索引一起带走导致索引顺序跟数据顺序不一致。如果你想让索引重新按 0 到 n-1 排加一个reset_index(dropTrue)。sales_rank sales_data.sort_values(销售额, ascendingFalse).reset_index(dropTrue)dropTrue的意思是丢弃原有索引不把它作为一个新列保留。如果你不写dropTrue原索引会变成一个新列叫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()时间序列排序后的一个常见后续操作是“重采样”比如按月汇总销量。如果你没排序就resamplepandas 有时候会直接报错或者得出错误结果。所以我的习惯是一旦数据里有时间列第一件事就是转成datetime并排序后面所有时间相关的操作都稳了。3.4 利用 rank 实现排名需求rank在业务场景中非常高频。比如公司每个月的销售排行榜除了要看出谁第一谁第二还需要保留原始名单顺序。假设你有一份员工绩效表想给每个人的绩效打个分数然后算排名。这个排名要按分数从高到低分数最高的排第 1 名。performance pd.DataFrame({ 员工编号: [E001, E002, E003, E004, E005], 绩效分数: [85, 92, 78, 92, 88] }) performance[排名] performance[绩效分数].rank(ascendingFalse, methodmin) print(performance)注意这里有两个人都是 92 分用methodmin的话两个人都排第 1 名下一个人从第 3 名开始。如果用的是默认的methodaverage两个人都排 1.5 名这在业务报表里会显得很奇怪。所以我在实际项目里如果要给业务方展示排名一般用methodmin或methodfirst具体看业务上怎么定义并列名次。如果你是做比赛评分这类场景并列名次的后一名应该跳号用min如果追求“先到先得”用first并列的人按行顺序给不同的名次。rank也可以按多个字段组合排名但需要先构造一个临时的综合列比如“总分 业务分 客户评价分”然后对总分做rank。这也是rank常用于“综合排名”的原因——排序往往只能按原始列排名可以基于你临时算出来的新列。4. 常见问题与排查技巧实录最后这部分我把自己和身边朋友在实际开发中踩过的高频坑整理成速查表每一个都是真实案例。你如果排序排序出问题先来这里对号入座。4.1 by 参数传错字符串还是列表傻傻分不清很多人一开始记不住sort_values的by参数到底传字符串还是列表记忆方法其实很简单只按一列排传字符串或列表都行。按多列排必须传列表。如果传了列表ascending必须也是列表或者一个单独的布尔值此时所有列都用同一个升降序。我自己见过最冤的一个报错是多列排序时by[列A, 列B]传了列表但ascendingFalse传了单个布尔值代码居然也能运行pandas 会做广播看起来结果好像对但实际上两列都是降序。如果业务上要求一列升序一列降序结果就错得离谱了。# 错误想要按A列升序、B列降序 df.sort_values(by[A, B], ascendingFalse) # 正确必须传列表 df.sort_values(by[A, B], ascending[True, False])所以我的建议是by传列表时ascending永远传等长列表别偷懒。4.2 inplaceTrue 的连锁反应我之前负责一个数据处理模块团队里的小伙伴在链式操作多个 pandas 操作串在一起中用了inplaceTrue结果调试了整整一个下午找不到 bug。原因就是inplaceTrue直接修改原始数据但链式表达式的返回值是None如果后面继续接.head()或者.reset_index()就会直接报NoneType object has no attribute head。# 错误inplaceTrue 返回 None不能继续链式调用 df.sort_values(销售额, ascendingFalse, inplaceTrue).head() # 正确不传 inplace链式调用 df.sort_values(销售额, ascendingFalse).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因为大写字母 B66和 D68排在 a97和 c99前面。解决方法是key参数统一转小写df_case.sort_values(fruit, keylambda 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(dropTrue)让索引重新从 0 开始避免后面所有跟索引相关的操作踩坑。df_sorted df.sort_values(销售额, ascendingFalse).reset_index(dropTrue)reset_index(dropTrue)会丢弃原来的索引并生成新的默认整数索引这样后续操作就都在一个干净的基础上进行。性能方面的经验如果你的数据量是几千行随便排序都没问题但到了几十万行、上百万行排序时间就会变得肉眼可见。这个时候可以看看是不是必须先排序才能完成业务。比如求每组最大值用groupby(列A)[列B].max()通常比先排序再取第一条更快再比如找 Top N用nlargest比sort_values(ascendingFalse).head(n)更快因为nlargest使用了更高效的算法不会把全部数据排一遍。# 取销售额最高的 3 行 top3 sales_data.nlargest(3, 销售额) # 等价写法但性能略差 top3_slow sales_data.sort_values(销售额, ascendingFalse).head(3)大数据量场景下nlargest比全量排序再取头部快很多。类似的还有nsmallest。我个人的处理大数据排序的习惯是先info()看数据量和类型确认没有明显问题再isna().sum()看缺失值排序前确定好业务上需要的行列排序后用head()抽查结果是否符合预期。这一套流程走下来基本不会翻车。最后再说一个我自己的习惯排序完不要直接信一定打印head()或者对重点行做个抽样看下。很多时候数据看着排对了但实际因为类型问题或者多列排序的优先级理解不对结果是错的。尤其是“先按 A 再按 B”的多列排序最好打印出来拉一眼确认 A 列相同的行内部已经按 B 列排好了。这种检查看起来多余但实际能帮你省下大量最后做数据检查的时间。
返回列表