Pandas 给 NumPy 数组带来的两个关键特性是:
事实证明,这些功能足以使Pandas成为Excel和数据库的强大竞争者。
Polars[2]是Pandas最近的转世(用Rust编写,因此速度更快,它不再使用NumPy的引擎,但语法却非常相似,所以学习 Pandas 后对学习 Polars 帮助非常大。
Pandas 图鉴系列文章由四个部分组成:
我们将拆分成四个部分,依次呈现~建议关注和星标@公众号:数据STUDIO,精彩内容等你来~
剖析 MultiIndex
对于没有听说过Pandas的人来说,MultiIndex最直接的用法是使用第二个索引列作为第一个索引列的补充,可以更加独特地识别每一行。例如,为了区分不同州的城市,州名通常被附加到城市名上。(你知道美国有大约40个斯普林菲尔德吗?)在关系型数据库中,它被称为复合主键。
你可以在DataFrame从CSV解析出来后指定要包含在索引中的列,也可以直接作为read_csv
的参数。
你也可以在事后用append=True
将现有的级别追加到MultiIndex中,正如你在下图中看到的那样:
其实更典型的是Pandas,当有一些具有某种属性的对象时,特别是当它们随着时间的推移而演变时,就会代表多个维度。比如说:
这也被称为 "Panel data
",而Pandas的名字就来源于此。
现在增加这样一个层面:
现在有一个四维空间,其中
下图说明了这一概念:
为了给对应列的维度名称留出空间,Pandas将整个标题向上移动:
rename_axis
关于MultiIndex,首先要注意它并不是简单的分组。在其内部,它只是一个扁平的标签序列,如下图所示:
还可以通过对行标签进行排序来获得同样的groupby效果:
sort_index
你甚至可以通过设置一个相应的Pandas option 来完全禁用可视化分组:pd.options.display.multi_sparse=False
。
Pandas (以及Python本身)对数字和字符串有区别,所以在数据类型没有被自动检测到的情况下,可以将数字转换为字符串:
pdi.set_level(df.columns, 0, pdi.get_level(df.columns, 0).astype('int'))
其实也可以用标准工具做同样的事情:
df.columns = df.columns.set_levels(df.columns.level[0].astype(int), level=0)
在正确使用这些工具,我们首先需要了解什么是 levels
和 codes
,而pdi
允许你使用MultiIndex,就像level
是普通的列表或NumPy数组一样。
levels
和 codes
是通过将某一级别的常规标签列表分解成,以加快像透视、连接等操作:
pdi.get_level(df, 0) == Int64Index([2010, 2010, 2020, 2020])
df.columns.level[0] == Int64Index([2010, 2020])
df.columns.codes[0] == Int64Index([0, 1, 0, 1] )
除了从CSV文件中读取和从现有的列中建立外,还有一些方法来创建MultiIndex。这些方法不太常用--主要用于测试和调试。
由于历史原因,使用Pandas自己表示的MultiIndex的最直观的方式并不可行。
这里的 levels
和 codes
(现在)被认为是实施细节,不应该暴露给最终用户。
也许,建立MultiIndex的最简单的方法是如下:
rename_axis
这里也有个缺点,需要在单独的一行或单独的链式方法中分配层次的名称。有几个替代的构造函数将名字和标签捆绑在一起。
from_arrays, from_tupes
当层次形成有规律的结构时,可以指定关键元素,让Pandas自动交错,如下图:
from_product
上面列出的所有方法也适用于列。比如说:
通过MultiIndex访问DataFrame的好处是,可以很容易地一次引用所有层次(可能会省略内部层次),而且语法很好,很熟悉。
现在,如果想选择俄勒冈州的所有城市,或者只留下有人口的那一列怎么办?Python的语法在这里施加了两个限制:
df['a', 'b']
和df[('a', 'b')]
--它的处理方式是一样的,所以你不能只写df[:, 'Oregon']
。否则,Pandas将永远不知道你指的是Oregon这一列还是Oregon第二层行。df.loc[(:, 'Oregon'), :]
。警告! 这里不是一个有效的Pandas语法!只有在pdi.patch_mi_co()
之后才有效。
这种语法的唯一缺点是,当使用两个索引器时,它会返回一个副本,所以你不能写df.mi[:, 'Oregon'].co['population'] = 10
。有许多替代的索引器,其中一些允许这样的分配,但它们都有自己的奇怪的规则:
swaplevel
因此,df[:,'population']
可以用以下方法实现
df.swaplevel(axis=1)['population']
注意,这里不方便超过两层
df.xs('population', level=1, axis=1)。
它感觉不够Pythonic,尤其是在选择多个层次时。
这个方法无法同时过滤行和列,所以名字xs
(代表 "cross-section")背后的原因并不完全清楚。它不能用于设置值。
pd.IndexSlice
创建一个别名,并在.loc
中使用它:idx=pd.IndexSlice; df.loc[:, idx[:, 'population']]
这更像是Pythonic的做法,但为了访问一个元素而必须使用别名,这多少是个负担(而且没有别名就太长了)。你可以同时选择行和列。
a[3:10:2]==a[slice(3,10,2)]
,那么你可能也会理解下面的内容:df.loc[:, (slice(None), 'population')]
,但无论如何,它几乎无法阅读。它可以同时选择行和列。可写。Pandas有很多方法可以用大括号来访问DataFrame的元素,但都不够方便,所以这里推荐采用另一种索引语法:
.query
方法的小型语言(它是唯一能够做'or'的方法,而不仅仅是'and'):df.query('state=="Oregon" or city=="Portland"')。
它既方便又快速,但缺乏IDE的支持(没有自动完成,没有语法高亮等),而且它只过滤行,不过滤列。这意味着你不能用它来实现df[:, 'population']
,而不需要转置DataFrame(除非所有列都是相同的类型,否则会丢失类型)。
这里有一个所有MultiIndex索引方法的汇总表:
rw=读/写,ro=只读;'mi[]'和'co[]'是pdi的扩展。
它们中没有一个是完美的,但有些接近了。
Pandas并没有为列提供set_index
。为列增加层次的一个常见方法是将现有的层次从索引中 "unstacking"出来:
tack, unstack
Pandas的stack
与NumPy的stack
非常不同。我们看看文档中对命名规则的描述:
"这个函数是通过类比来命名的,即一个集合被重新组织,从水平位置上的并排(DataFrame的列)到垂直方向上的堆叠(DataFrame的索引中)。"
Series有unstack
,但没有stack
,因为它已经被 stack
了。作为一维的,Series在不同情况下可以作为行向量或列向量,但通常被认为是列向量(例如DataFrame的列)。
比如说:
也可以通过名称或位置索引来指定要堆叠/取消堆叠的级别。在这个例子中,df.stack()
、df.stack(1)
和df.stack('year')
产生了相同的结果,df1.unstack()
、df1.unstack(2
)和df1.unstack('year')
也是如此。目的地总是 "在最后一个级别之后",并且不可配置。如果需要把级别放在其他地方,可以使用df.swaplevel().sort_index()
或者pdi.swap_level(df, sort=True)
列必须不包含重复的值才有资格进行 stack
(unstack
时同样适用于索引):
stack
和unstack
都有一个缺点,就是对结果的索引进行不可预知的排序。这有时可能会让人恼火,但这是在有大量缺失值时给出可预测结果的唯一方法。
考虑一下下面的例子。你希望一周中的哪几天以何种顺序出现在右表中?
你可以推测,如果约翰的周一站在约翰的周五的左边,那么'Mon'< 'Fri'
,同样,对西尔维娅来说,'Fri'<'Sun'
,所以结果应该是'Mon'<'Fri'<'Sun'
。这是合法的,但是如果剩下的列是不同的顺序,例如'Mon'<'Fri'
和'Tue'<'Fri'
呢?或者'Mon'<'Fri'
和'Wed'<'Sat'
?
好吧,一周并没有那么多天,Pandas可以根据先前的知识推断出顺序。但是,对于星期天应该站在一周的末尾还是开头,人类还没有得出决定性的结论。Pandas应该默认使用哪个顺序?阅读区域设置?而对于不那么琐碎的顺序,比如说,中国各省市的顺序,又该如何处理?
在这种情况下,Pandas所做的只是简单地按字母顺序排序,你可以看到下面:
虽然这是一个合理的默认值,但它仍然感觉不对。应该有一个解决方案!现在有了一个。它被称为CategoricalIndex。即使有些标签丢失了,它也会记住顺序。它最近被顺利地集成到Pandas工具链中。它唯一缺乏的是基础设施。它很难构建;它很脆弱(在某些操作中会退回到对象dtype
),但它是完全可用的,而且pdi库有一些帮助工具来提高学习曲线。
例如,要告诉Pandas,比如说,持有产品的简单Index(如果需要把星期几解开,就不可避免地会被排序)的顺序,你需要写一些像df.index = pd.CategoricalIndex(df.index, df.index, sorted=True)
这样可怕的东西。而对于MultiIndex来说,这就更显得矫情了。
pdi
库有一个辅助函数locked
(以及一个默认为inplace=True
的别名lock
),用于锁定某个MultiIndex
级别的顺序,将该level
提升到CategoricalIndex
:
level
名称旁边的复选标记✓意味着该level
被锁定。它可以通过pdi.vis(df)
手动实现可视化,也可以通过pdi.vis_patch()
对DataFrame的HTML表示进行猴子修补来自动实现。应用补丁后,只要在Jupyter单元格中写上df
,就会显示所有锁定的level
的复选标记。
lock
和locked
在简单的情况下自动工作(如客户名称),但在更复杂的情况下需要用户的提示(如缺少日子的星期)。
在level
转换为CategoricalIndex后,在sort_index、stack、unstack、pivot、pivot_table
等操作中保持原来的顺序。
不过,即使是通过df['new_col'] = 1
添加一个列这样的简单操作也会破坏它。使用pdi.insert(df.columns, 0, 'new_col', 1)
可以正确处理带有CategoricalIndex的级别。
除了已经提到的方法之外,还有一些其他的方法:
pdi.get_level(obj, level_id)
返回一个通过数字或名称引用的特定级别,适用于DataFrames、Series和MultiIndex,是df.columns.get_level_values
的别名;pdi.set_level(obj, level_id, labels)
用给定的数组(列表、NumPy数组、系列、索引等)替换一个关卡的标签,--在纯Pandas中没有直接的对应关系:pdi.insert_level(obj, pos, labels, name)
用给定的值添加一个关卡(必要时适当广播),--在纯Pandas中不容易做到;pdi.drop_level(obj, level_id)
从MultiIndex中删除指定的level(向df.droplevel
添加inplace
参数):pdi.swap_levels(obj, src=-2, dst=-1)
交换两个level(默认为最里面的两个级别),将inplace
和sort
参数添加到df.swaplevel
中pdi.move_level(obj, src, dst)
将一个特定的级别src移动到指定的位置dst(
在纯Pandas中不能轻易完成):除了上面提到的参数外,本节的所有函数都有以下参数:
axis=None
,其中None表示DataFrame的 "列
" 和Series的 "index
"(又称 "info"轴);sort=False
,可选择在操作后对相应的MultiIndex进行排序;inplace=False
,可选择执行原地操作(对单个索引不起作用,因为它是不可变的)。上面的所有操作都是在传统意义上理解level
这个词(level标签数与DataFrame中的列数相同),向最终用户隐藏index.label和index.code
的机制。
在极少数情况下,当移动和交换单独的level是不够的,可以通过这个纯粹的Pandas调用,一次性重新排序所有的级别:
df.columns = df.columns.reorder_levels(['M', 'L', 'K'] )
其中['M', 'L', 'K']
是所需的level顺序。
一般来说,使用get_level
和set_level
来对标签进行必要的修正就足够了,但是如果想一次性对MultiIndex的所有层次进行转换,Pandas有一个(名字不明确的)函数rename
,它接受一个dict
或者一个函数:
rename
至于重命名level,它们的名字被存储在.names
字段中。这个字段不支持直接赋值(为什么不呢):
df.index.names[1] = 'x' # TypeErrorbut
可以作为一个整体被替换:
df.index.names = ['z', 'x'] # ok
另外,也可以使用一个可连锁的rename_axis
:
当只需要重命名一个特定level时,语法如下:
或者如果想通过数字而不是名字来引用级别,可以使用df.index = df.index.set_names('z', level=0)
或pdi.rename_level(df, 'z', 0, axis=0)
(这两种方法也可以通过名字来工作)。
方便的查询方法只解决了处理行中MultiIndex的复杂性。而且,尽管有所有的辅助函数,当一些棘手的Pandas函数返回列中的MultiIndex时,对初学者来说也会倍感厉害。所以,pdi库有以下内容:
join_levels(obj, sep='_', name=None)
将所有的MultiIndex级别连接成一个索引。split_level(obj, sep='_', names=None)
将索引分割成一个多索引。两者都有可选的 axis
和 inplace
参数。
至于纯粹的Pandas解决方案,如下代码所示:
df.columns = ['_'.join(k) for k in df.columns.to_flat_index()]
df.columns = pd.MultiIndex.from_tuples(k.split('_') for k in df.columns)
由于MultiIndex是由多个层次组成的,所以排序比单个Index的排序要复杂一些。它仍然可以用sort_index
方法来完成,但是可以通过以下参数来进一步微调:
要对列级进行排序,请指定 axis=1
。
Pandas可以以完全自动化的方式将一个带有MultiIndex的DataFrame写入CSV文件:df.to_csv('df.csv')
。然而,在读取这样的文件时,Pandas无法自动解析MultiIndex,需要用户提供一些提示。例如,要读取一个有三层高的列和四层宽的索引的DataFrame,你需要指定
pd.read_csv('df.csv', header=[0,1,2], index_col=[0,1,2,3])
这意味着前三行包含了列的信息,后面每行的前四个字段包含了索引level(如果列中有多于一个level,你不能在 read_csv
中通过名字引用行级别,只能通过数字)。
手动解读MultiIndex列的层数并不方便,所以更好的办法是在将DataFrame保存为CSV之前,将所有的列头层数stack()
,而在读取之后再将其unstack()
。
如果需要一个即用即走的解决方案,来研究一下二进制格式,比如Python pickle格式:
df.to_pickle('df.pkl'), pd.read_pickle('df.pkl')
%store df
或 %store -r df
(存储在 $HOME/.ipython/profile_default/db/autorestore)这种格式小而快,但它只能从Python中访问。如果你需要与其他生态系统的互操作性,请关注更多的标准格式,如Excel格式(在读取MultiIndex时需要与read_csv
一样的提示)。下面是代码:
!pip install openpyxl
df.to_excel('df.xlsx')
df1 = pd.read_excel('df.xlsx', header=[0,1,2], index_col=[0,1,2,3])
Parquet[3]文件格式支持多索引DataFrame,没有任何提示(唯一的限制是所有列的标签必须是字符串),产生的文件更小,而且工作速度更快(见基准):
df.to_parquet('df.parquet')。
df1 = pd.read_parquet('df.parquet')。
官方Pandas文档有一个表格[4],列出了所有~20种支持的格式。
在整体使用多索引DataFrame的操作中,适用与普通DataFrame相同的规则(见第三部分)。但处理单元格的子集有其自身的一些特殊性。
可以像下面这样简单地更新通过外部MultiIndex level引用的列的子集:
或者如果想保持原始数据的完整性
df1 = df.assign(population=df.population*10)。
也可以用density=df.population/df.area
来轻松获得人口密度。
但并不能用df.assign
将结果分配到原始DataFrame中。
一种方法是将所有不相关的列索引层层叠加到行索引中,进行必要的计算,然后再将它们解叠回来(使用pdi.lock
来保持原来的列顺序)。
或者,你也可以使用pdi.assign
:
pdi.assign
有锁定顺序的意识,所以如果你给它提供一个锁定level的DataFrame这不会解锁它们,这样后续的stack/unstack等操作将保持原来的列和行的顺序。
[在这里](https://github.com/ZaxR/pandas_multiindex_tutorial/blob/master/Pandas MultiIndex Tutorial.ipynb "在这里")可以找到一个用巨大的MultiIndex处理现实生活中的销售数据集的好例子。
总而言之,Pandas是一个分析和处理数据的伟大工具。希望这篇文章能帮助你理解解决典型问题的 "方法" 和 "原因",并体会到Pandas库的真正价值和魅力。
[1]
Pandas: https://pandas.pydata.org/
[2]
Polars: https://www.pola.rs/
[3]
Parquet: https://en.wikipedia.org/wiki/Apache_Parquet
[4]
表格: https://pandas.pydata.org/docs/user_guide/io.html
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有