本文译自 Unit8 数据科学家 Rudolf Höhn 先生的文章 "from pandas-wan to pandas-master",文章发表在博客平台 Medium 上,文章介绍了 pandas 的发展现状、内存优化、索引和方法链等内容,作者在文章中给出了许多提升程序运行性能的建议。Unit8 是一家位于瑞士莫尔日,成立于 2017 年的初创公司,致力于利用大数据和人工智能技术解决各行各业问题。2019 年 5 月 21 日,公司加入数字瑞士(digitalswitzerland)组织,该组织包括 150 多家公司、学术和政府机构,其使命是将瑞士打造为全球领先的数字创新中心。
"from pandas-wan to pandas-master" 的译文于 2019 年 8 月 15 日首发于机器之心微信公众号,由李诗萌、张倩翻译,其译文标题为 “从小白到大师,这里有一份 pandas 入门指南”,运筹OR帷幄公众号于 2019 年 9 月 7 日转载了这篇翻译。鉴于译文中的一些错误,译文对原文部分内容的删节,以及译文对原文表格处理不当导致部分内容不可见等原因,我们参考机器之心的翻译对原文作了重译,限于水平,翻译可能还有不少问题,欢迎大家批评指正。
在 Unit8,我们为客户提供支持,我们利用数据资源构建有影响的高水平的机器学习模型,这些模型是商业用以产生强大影响力的合适工具。在追寻使命的漫漫征途里,我们使用了许多工具,其中之一就是 Python 库: pandas。
通过这篇文章,你将有望发现一种、两种或更多种新的使用 pandas 编写代码的方式。
这篇博文介绍 pandas 的最佳实践,它适用于所有使用 pandas 的人,无论这种使用是否频繁,它也适用于所有想要使用 pandas 的人。即便你以前从未使用过 pandas,现在开始也不迟,你说对吗?
文章将会涉及以下几个方面:
- pandas 的发展现状
- 内存优化
- 索引
- 方法链
- 随机给出的一些小建议
在你阅读本文时,我推荐你查阅那些你看不懂的函数的帮助信息(docstrings)。做个简单的谷歌搜索以及花上几秒钟阅读 pandas 文档,将会让你的阅读更加愉快。
译者注:docstrings 是紧跟在 def 或 class 后的第一个字符串,这个字符串通常用来记录函数或类的帮助信息,以函数为例,这些信息可能包括函数的功能、参数的数据类型和含义、返回值的类型和含义以及使用示例等内容,这个字符串存储在对象属性 __doc__ 中,可以使用对象名.__doc__ 查看,此外也可以使用 help(对象名) 命令查看。
那什么是 pandas ?
pandas 是一个 “开放源代码,使用 BSD 许可证的库,它为 Python 编程语言提供高性能、易用的数据结构和数据分析工具”(摘自 pandas 网站)。总的来说,它提供了叫做 DataFrame 和 Series 的数据抽象(已不推荐使用 Panel),它管理索引以实现数据的快速存取,它执行分析和转换运算,它甚至能(使用 matplotlib 后端)画图。
截止到本文写作,pandas 的最新发行版本为 v0.24.2.(译者注:截止到我们翻译,pandas 的最新发行版本为 v0.25.0)
是的,pandas 正走在通往 1.0 版本的道路上,而要到达那里,就不得不改变一点点人们已经习惯的用法。这里有一个非常有趣的演讲:pandas 核心贡献者 Marc Garcia 的 “走向 pandas 1.0”。
下一个版本 v0.25 的发行定在 2019 年 7 月(v0.25rc0 已于 7 月 4 日发行),它与 v1.0 有相同的代码库,但在使用即将弃用的方法时会显示警告信息(warning messages)。所以,如果你计划使用 v1.0, 那么当你运行你的 v0.25 代码库时,请务必关注所有的弃用警告(deprecation wranings)。
一句话总结,pandas v1.0 主要改善了稳定性(例如:时间序列)并且移除了未使用过的代码库(例如:SparseDataFrame).
让我们开始工作吧,我们选择的数据集是(来自 Kaggle)的玩具数据集 “1985 到 2016 年国家自杀率”。这个数据集虽然简单,但对于你上手 pandas 已经足够了。
在深入研究代码之前,如果你想重现结果,还需要执行这个简短的数据预处理过程,以确保你拥有正确的列名和列类型。
import pandas as pd
import numpy as np
import os
# to download https://www.kaggle.com/russellyates88/suicide-rates-overview-1985-to-2016
data_path = 'path/to/folder/'
df = (pd.read_csv(filepath_or_buffer=os.path.join(data_path, 'master.csv'))
.rename(columns={'suicides/100k pop' : 'suicides_per_100k',
' gdp_for_year ($) ' : 'gdp_year',
'gdp_per_capita ($)' : 'gdp_capita',
'country-year' : 'country_year'})
.assign(gdp_year=lambda _df: _df['gdp_year'].str.replace(',','').astype(np.int64))
)
小建议:如果你要读入一个大文件,请在 read_csv() 中使用参数 chunksize=N,此时函数将返回一个输出为 DataFrame 对象的迭代器
下面是数据集的部分展示:
country | year | sex | age | suicides_no | population | suicides_per_100k | country_year | HDI for year | gdp_year | gdp_capita | generation |
---|---|---|---|---|---|---|---|---|---|---|---|
Albania | 1987 | male | 15-24 years | 21 | 312900 | 6.71 | Albania1987 | 2156624900 | 796 | Generation X | |
Albania | 1987 | male | 35-54 years | 16 | 308000 | 5.19 | Albania1987 | 2156624900 | 796 | Silent | |
Albania | 1987 | female | 15-24 years | 14 | 289700 | 4.83 | Albania1987 | 2156624900 | 796 | Generation X | |
Albania | 1987 | male | 75+ years | 1 | 21800 | 4.59 | Albania1987 | 2156624900 | 796 | G.I. Generation | |
Albania | 1987 | male | 25-34 years | 9 | 274300 | 3.28 | Albania1987 | 2156624900 | 796 | Boomers | |
Albania | 1987 | female | 75+ years | 1 | 35600 | 2.81 | Albania1987 | 2156624900 | 796 | G.I. Generation | |
Albania | 1987 | female | 35-54 years | 6 | 278800 | 2.15 | Albania1987 | 2156624900 | 796 | Silent | |
Albania | 1987 | female | 25-34 years | 4 | 257200 | 1.56 | Albania1987 | 2156624900 | 796 | Boomers | |
Albania | 1987 | male | 55-74 years | 1 | 137500 | 0.73 | Albania1987 | 2156624900 | 796 | G.I. Generation | |
Albania | 1987 | female | 5-14 years | 0 | 311000 | 0.0 | Albania1987 | 2156624900 | 796 | Generation X |
>>> df.columns
Index(['country', 'year', 'sex', 'age', 'suicides_no', 'population',
'suicides_per_100k', 'country_year', 'HDI for year', 'gdp_year',
'gdp_capita', 'generation'],
dtype='object')
这里有 101 个国家,年份从 1985 到 2016,有两种性别,六个世代以及六个年龄段。使用一些简单而有用的方法,我们就可以获得这些信息。
-
unique() 和 nunique() 用来获取去除了重复值的列(或去重列中的元素数目)
>>> df['generation'].unique() array(['Generation X', 'Silent', 'G.I. Generation', 'Boomers', 'Millenials', 'Generation Z'], dtype=object) >>> df['country'].nunique() 101
-
describe() 为每一个数值列输出不同的统计数字(例如:最小值,最大值,均值,个数),如果设置参数 'include=all' 则还会显示每个对象列去重后的元素个数以及顶部元素(即频率最高的元素)的个数。
year | suicides_no | population | suicides_per_100k | HDI for year | gdp_year | gdp_capita | |
---|---|---|---|---|---|---|---|
count | 27820.0 | 27820.0 | 27820.0 | 27820.0 | 8364.0 | 27820.0 | 27820.0 |
mean | 2001.2583752695903 | 242.57440690150972 | 1844793.6173975556 | 12.816097411933864 | 0.7766011477761837 | 445580969025.7266 | 16866.464414090584 |
std | 8.46905502444141 | 902.0479168336403 | 3911779.441756363 | 18.961511014503195 | 0.09336670859029964 | 1453609985940.912 | 18887.576472205572 |
min | 1985.0 | 0.0 | 278.0 | 0.0 | 0.483 | 46919625.0 | 251.0 |
25% | 1995.0 | 3.0 | 97498.5 | 0.92 | 0.713 | 8985352832.0 | 3447.0 |
50% | 2002.0 | 25.0 | 430150.0 | 5.99 | 0.779 | 48114688201.0 | 9372.0 |
75% | 2008.0 | 131.0 | 1486143.25 | 16.62 | 0.855 | 260202429150.0 | 24874.0 |
max | 2016.0 | 22338.0 | 43805214.0 | 224.97 | 0.9440000000000001 | 18120714000000.0 | 126352.0 |
- head() 和 tail() 用来显示数据框的一小部分
使用这些方法,你将很快了解你正在分析的表格文件。
理解数据并且为数据框的每一列选择合适的数据类型,是处理数据前的一个重要步骤。
在内部,pandas 将数据框存储为不同类型的 numpy 数组(例如:一个 float64 矩阵,一个 int32 矩阵)。
下面是大幅度降低内存消耗的两种方法:
译者注:实际上是一种方法,或许作者指的是这里有两个函数
import pandas as pd
def mem_usage(df: pd.DataFrame) -> str:
"""This method styles the memory usage of a DataFrame to be readable as MB.
Parameters
----------
df: pd.DataFrame
Data frame to measure.
Returns
-------
str
Complete memory usage as a string formatted for MB.
"""
return f'{df.memory_usage(deep=True).sum() / 1024 ** 2 : 3.2f} MB'
def convert_df(df: pd.DataFrame, deep_copy: bool = True) -> pd.DataFrame:
"""Automatically converts columns that are worth stored as
``categorical`` dtype.
Parameters
----------
df: pd.DataFrame
Data frame to convert.
deep_copy: bool
Whether or not to perform a deep copy of the original data frame.
Returns
-------
pd.DataFrame
Optimized copy of the input data frame.
"""
return df.copy(deep=deep_copy).astype({
col: 'category' for col in df.columns
if df[col].nunique() / df[col].shape[0] < 0.5})
memory_usage() 是 pandas 自带的用来分析数据框内存消耗的方法。上面代码中,deep=True 用来确保将真实的系统消耗考虑在内。
理解列的类型是重要的,做如下两件简单的事情,你就可以减少 90 % 的内存占用:
- 搞清楚你的数据框正在使用的类型
- 搞清楚存在哪些可用类型能够降低你的数据框的内存占用(例如:如果对取值范围在 0 到 59 且只有 1 位小数的 price 列使用 float64,就可能造成不必要的开销)
除了你可能正在使用的减少数值类型大小的方式(用 int32 代替 int64),pandas 还自带有一种分类(category)类型来减少内存占用。
如果你是一名 R 开发者,你会发现它和 factor 类型是一致的。
这种类别类型使用索引替代重复值,而将真实值存储在其他地方。一个教科书式的例子便是国家,如果要多次存储相同的字符串 “瑞士” 或者 “波兰”,为什么不简洁地用 0 和 1 替代它们并存储一个字典呢?
categorical_dict = {0: 'Switzerland', 1: 'Poland'}
pandas 实际上就做着和这个非常类似的事情,它加入所有这些方法让类型得以使用并保证能够显示国家名称。
回到我们的方法 convert_df(),如果去重后元素个数小于原来元素个数的 50 %,该方法会把列类型自动转换为 category. 虽然这个数字可以任意选取,但由于数据框类型的转换意味着在 numpy 数组中移动数据,因此数字的选取应确保这种转换是值得的。
译者注:保证转换操作本身带来的开销,小于不转换相较于转换所增加的额外开销
让我们看看我们的数据发生了什么
>>> mem_usage(df)
10.28 MB
>>> mem_usage(df.set_index(['country', 'year', 'sex', 'age']))
5.00 MB
>>> mem_usage(convert_df(df))
1.40 MB
>>> mem_usage(convert_df(df.set_index(['country', 'year', 'sex', 'age'])))
1.40 MB
通过使用我们 “机智的” 转换器,数据框的内存占用减少了几乎 10 倍(严格来说是 7.34 倍)。
pandas 虽然强大,但也要付出一些代价。当你加载一个 DataFrame 时,pandas 会创建索引并在 numpy 数组内部存储数据。所以这意味着什么呢?意味着一旦加载了数据,只要索引管理得当,你就可以快速存取它们。
存取数据的方式主要有两种:索引(index)和查询(query),不同情况下你对这两种方式的选择也会不一样。但在大多数情况中,索引(和多索引)都是最佳选择。我们来看下面的例子:
译者注:查询是计算机科学中的术语,它是一个从数据库中获取数据的请求,我们熟知的 SQL 的英文全称就是 Structured Query Language(结构化查询语言)
>>> %%time
>>> df.query('country == "Albania" and year == 1987 and sex == "male" and age == "25-34 years"')
CPU times: user 7.27 ms, sys: 751 µs, total: 8.02 ms
# ==================
>>> %%time
>>> mi_df.loc['Albania', 1987, 'male', '25-34 years']
CPU times: user 459 µs, sys: 1 µs, total: 460 µs
译者注:请在 IPython 解释器(例如安装 jupyter)下使用 %%time,CPython 解释器不支持此命令
什么?20 倍加速?
你可能马上会问自己,创建多索引需要花费多长时间?
%%time
mi_df = df.set_index(['country', 'year', 'sex', 'age'])
CPU times: user 10.8 ms, sys: 2.2 ms, total: 13 ms
采用查询花费的时间是这里的 1.5 倍。如果你只需要检索一次数据(这种情况很少见),query 是合适的方法。否则,坚持使用索引吧,你的 CPU 会感谢你的。
.set_index(drop=False) 保证不会删除作为新索引的列
当你想要查看数据框时,采用 .loc[] / .iloc[] 方法的效果非常好,但当你要修改数据框时,采用它们的效果就没那么好了。如果你需要手动(例如:使用循环)构建数据框,请考虑其他数据结构(例如:字典,列表)并在你准备好了所有数据时创建你的 DataFrame. 否则,对 DataFrame 中的每一个新行,pandas 都会更新索引,而这种更新并不是一次简单的哈希映射。
>>> pd.DataFrame({'a':range(2), 'b': range(2)}, index=['a', 'a']).loc['a']
a b
a 0 0
a 1 1
正因如此,一个未排序的索引会降低运行效率。为了检查索引是否排序和对索引进行排序,主要采用如下两个方法。
%%time
>>> mi_df.sort_index()
CPU times: user 34.8 ms, sys: 1.63 ms, total: 36.5 ms
>>> mi_df.index.is_monotonic
True
DataFrame 中的方法链是一种链接多种方法并返回一个数据框的行为,这些方法来自于 DataFrame 类。pandas 现在的版本中,使用方法链的原因是这样不用存储中间变量,且能避免下述情形的发生:
import numpy as np
import pandas as pd
df = pd.DataFrame({'a_column': [1, -999, -999],
'powerless_column': [2, 3, 4],
'int_column': [1, 1, -1]})
df['a_column'] = df['a_column'].replace(-999, np.nan)
df['power_column'] = df['powerless_column'] ** 2
df['real_column'] = df['int_column'].astype(np.float64)
df = df.apply(lambda _df: _df.replace(4, np.nan))
df = df.dropna(how='all')
我们使用下面的方法链替代上述代码。
df = (pd.DataFrame({'a_column': [1, -999, -999],
'powerless_column': [2, 3, 4],
'int_column': [1, 1, -1]})
.assign(a_column=lambda _df: _df['a_column'].replace(-999, np.nan),
power_column=lambda _df: _df['powerless_column'] ** 2,
real_column=lambda _df: _df['int_column'].astype(np.float64))
.apply(lambda _df: _df.replace(4, np.nan))
.dropna(how='all')
)
老实说,第二段代码要漂亮和简洁得多。
方法链的工具箱中包含许多将 DataFrame 或者 Series (或者 DataFrameGroupBy) 对象作为输出的方法(例如:apply, assign, loc, query, pipe, groupby, agg)。
理解这些方法最好的方式就是实践,让我们从一些简单的例子开始。
(df
.groupby('age')
.agg({'generation':'unique'})
.rename(columns={'generation':'unique_generation'})
# Recommended from v0.25
# .agg(unique_generation=('generation', 'unique'))
)
获得每个年龄段世代的简单方法链
age | unique_generation |
---|---|
15-24 years | ['Generation X' 'Millenials'] |
25-34 years | ['Boomers' 'Generation X' 'Millenials'] |
35-54 years | ['Silent' 'Boomers' 'Generation X'] |
5-14 years | ['Generation X' 'Millenials' 'Generation Z'] |
55-74 years | ['G.I. Generation' 'Silent' 'Boomers'] |
75+ years | ['G.I. Generation' 'Silent'] |
产生数据框,age 列为索引
从上表我们知道 “世代 X” 覆盖三个年龄段(译者注:作者笔误,实为 4 个),此外让我们来分解一下这条方法链。第一步按年龄段分组,这一方法返回一个 DataFrameGroupBy 对象,在这个对象中,每一组将汇总该组对应的世代标签。
尽管在这个案例中,汇总方法采用 unique,但实际上任何(匿名)函数都是可以的。
在最新的发行版本(v0.25)中,pandas 引入了一种新的使用 agg 的方式。
(df
.groupby(['country', 'year'])
.agg({'suicides_per_100k': 'sum'})
.rename(columns={'suicides_per_100k':'suicides_sum'})
# Recommended from v0.25
# .agg(suicides_sum=('suicides_per_100k', 'sum'))
.sort_values('suicides_sum', ascending=False)
.head(10)
)
使用 sort_values 和 head 获得自杀率较高的国家和年份
(df
.groupby(['country', 'year'])
.agg({'suicides_per_100k': 'sum'})
.rename(columns={'suicides_per_100k':'suicides_sum'})
# Recommended from v0.25
# .agg(suicides_sum=('suicides_per_100k', 'sum'))
.nlargest(10, columns='suicides_sum')
)
使用 nlargest 获得自杀率较高的国家和年份
这两段程序的输出是相同的:拥有二水平(two level)索引的一个 DataFrame 和包含最大 10 个值的一个新列 suicides_sum.
country | year | suicides_sum |
---|---|---|
Lithuania | 1995 | 639.3 |
Lithuania | 1996 | 595.61 |
Hungary | 1991 | 575.0000000000001 |
Lithuania | 2000 | 571.8 |
Hungary | 1992 | 570.26 |
Lithuania | 2001 | 568.9799999999999 |
Russian Federation | 1994 | 567.64 |
Lithuania | 1998 | 566.36 |
Lithuania | 1997 | 565.4400000000002 |
Lithuania | 1999 | 561.5300000000001 |
列 “国家” 和 “年份” 均是索引
nlargest(10) 比 sort_values(ascending=False).head(10) 更有效率
另一个有趣的方法是 unstack,它能旋转索引水平。
(mi_df
.loc[('Switzerland', 2000)]
.unstack('sex')
[['suicides_no', 'population']]
)
suicides_no | suicides_no | population | population | |
---|---|---|---|---|
sex | female | male | female | male |
age | ||||
15-24 years | 20 | 79 | 410136 | 426957 |
25-34 years | 47 | 147 | 537823 | 530378 |
35-54 years | 124 | 360 | 1072711 | 1094229 |
5-14 years | 1 | 4 | 412273 | 436831 |
55-74 years | 128 | 239 | 723750 | 649009 |
75+ years | 79 | 152 | 330903 | 184589 |
“age” 是索引,列 “suicides_no” 和 “population” 有第二个水平列 “sex”
下一个方法 pipe 是用途最广泛的方法之一,就像 shell 脚本一样,pipe 方法执行管道运算,它让方法链可以执行更丰富的运算。pipe 的一个简单却强大的用法是用来记录不同信息。
def log_head(df, head_count=10):
print(df.head(head_count))
return df
def log_columns(df):
print(df.columns)
return df
def log_shape(df):
print(f'shape = {df.shape}')
return df
使用管道的不同记录函数
例如,我们想通过比较列 year 验证列 country_year 是否正确。
(df
.assign(valid_cy=lambda _serie: _serie.apply(
lambda _row: re.split(r'(?=\d{4})', _row['country_year'])[1] == str(_row['year']),
axis=1))
.query('valid_cy == False')
.pipe(log_shape)
)
验证列 “country_year” 的管道
尽管管道的输出是一个 DataFrame,但它也打印标准输出(console / REPL)。
shape = (0, 13)
你也可以将不同的 pipe 放到一个方法链中。
(df
.pipe(log_shape)
.query('sex == "female"')
.groupby(['year', 'country'])
.agg({'suicides_per_100k':'sum'})
.pipe(log_shape)
.rename(columns={'suicides_per_100k':'sum_suicides_per_100k_female'})
# Recommended from v0.25
# .agg(sum_suicides_per_100k_female=('suicides_per_100k', 'sum'))
.nlargest(n=10, columns=['sum_suicides_per_100k_female'])
)
在女性中,自杀率较高的国家和年份
产生的 DataFrame 如下所示:
year | country | sum_suicides_per_100k_female |
---|---|---|
2009 | Republic of Korea | 170.89 |
1989 | Singapore | 163.16 |
1986 | Singapore | 161.67 |
2010 | Republic of Korea | 158.52 |
2007 | Republic of Korea | 149.6 |
2011 | Republic of Korea | 147.84 |
1991 | Hungary | 147.35 |
2008 | Republic of Korea | 147.04000000000002 |
2000 | Aruba | 146.22 |
2005 | Republic of Korea | 145.35 |
索引是 “year” 和 “country”
标准输出中的打印结果如下所示:
shape = (27820, 12)
shape = (2321, 1)
除了向命令行解释器输出记录,我们还可以使用 pipe 直接将函数作用到数据框列上。
from sklearn.preprocessing import MinMaxScaler
def norm_df(df, columns):
return df.assign(**{col: MinMaxScaler().fit_transform(df[[col]].values.astype(float))
for col in columns})
for sex in ['male', 'female']:
print(sex)
print(
df
.query(f'sex == "{sex}"')
.groupby(['country'])
.agg({'suicides_per_100k': 'sum', 'gdp_year': 'mean'})
.rename(columns={'suicides_per_100k':'suicides_per_100k_sum',
'gdp_year': 'gdp_year_mean'})
# Recommended in v0.25
# .agg(suicides_per_100k=('suicides_per_100k_sum', 'sum'),
# gdp_year=('gdp_year_mean', 'mean'))
.pipe(norm_df, columns=['suicides_per_100k_sum', 'gdp_year_mean'])
.corr(method='spearman')
)
print('\n')
自杀数的增长与 GDP 的降低有关吗?自杀数与性别相关吗?
在命令行解释器中,上面的代码打印出如下结果:
male
suicides_per_100k_sum gdp_year_mean
suicides_per_100k_sum 1.000000 0.421218
gdp_year_mean 0.421218 1.000000
female
suicides_per_100k_sum gdp_year_mean
suicides_per_100k_sum 1.000000 0.452343
gdp_year_mean 0.452343 1.000000
让我们深入研究一下代码。norm_df() 将 DataFrame 和数据框列索引构成的列表作为输入,然后使用 MinMaxScaling 对数据作标准化处理。通过使用字典生成式,norm_df() 创建了一个字典 {column_name:method, …},字典随后被解压为 assign() 的参数 (column_name=method, …)。
在这个特别的例子中,最小最大标准化并不会改变相关系数的输出结果,它的引入只是为了论证 pipe 可以将函数直接作用到数据框列上 :)
(不远的?) 将来,惰性求值(lazy evaluation)将会出现在方法链中,所以在方法链上投入时间会是个很好的想法。
下面的小建议非常有用,但并不适合之前任何一部分。
- itertuples() 比通过数据框行迭代要更有效率
>>> %%time
>>> for row in df.iterrows(): continue
CPU times: user 1.97 s, sys: 17.3 ms, total: 1.99 s
>>> for tup in df.itertuples(): continue
CPU times: user 55.9 ms, sys: 2.85 ms, total: 58.8 ms
注意:tup 是一个 namedtuple
- join() 使用了 merge()
- 在 Jupyter 笔记本中,在单元开始部分使用 %%time 计算运行时间是有效的
- UInt8 数据类型支持整数 NaN
- 从现在开始记住,使用低级方法(尽可能使用 Python 的核心函数)执行密集 I/O (例如:展开大型 CSV 转储)效果更好
译者注:I/O 指输入/输出;dump 是计算机科学中的术语,中文译为转储,详情参考知乎:计算机术语 dump 是什么意思?
下面还有一些本文没有涉及的有用的方法或数据结构,它们是值得花时间去理解的。
因为这篇短文,你将有望对 pandas 背后的工作原理及其发展现状有更好的理解。你了解了优化数据框内存占用的不同工具,并且知道了如何快速洞察数据。现在索引和查询已不再那么让人费解。最后,你可以尝试使用方法链写更长的链了。
这个笔记本是一个支持文档,其中除了包含本文展示的所有代码,还在性能上比较了单数值索引数据框(df)和多索引数据框(mi_df)。
实践出真知,持续提高你的技能并帮助我们建设一个更好的世界吧。
PS: 有时纯粹的 Numpy 实现更快(Julien Herzen;)
感谢 Julien Herzen,Gael Grosch 和 Kamil Zalewski.