Pandas 给 NumPy 数组带来的两个关键特性是:
事实证明,这些功能足以使Pandas成为Excel和数据库的强大竞争者。
Polars[2]是Pandas最近的转世(用Rust编写,因此速度更快,它不再使用NumPy的引擎,但语法却非常相似,所以学习 Pandas 后对学习 Polars 帮助非常大。
Pandas 图鉴系列文章由四个部分组成:
我们将拆分成四个部分,依次呈现~建议关注和星标@公众号:数据STUDIO,精彩内容等你来~
数据框架的剖析
Pandas的主要数据结构是一个DataFrame。它捆绑了一个二维数组,并为其行和列加上标签。它由许多系列对象组成(有一个共享的索引),每个对象代表一个列,可能有不同的dtypes。
构建DataFrame的一个常见方法是通过读取CSV(逗号分隔的值)文件,如该图所示:
pd.read_csv()
函数是一个完全自动化的、可以疯狂定制的工具。如果你只想学习关于Pandas的一件事,那就学习使用read_csv
。
下面是一个解析非标准CSV文件的例子:
并简要介绍了一些参数:
由于 CSV 没有严格的规范,有时需要试错才能正确读取它。read_csv
最酷的地方在于它能自动检测到很多东西,包括:
如果简单地在Jupyter单元中写df
的结果恰好太长(或太不完整),可以尝试以下方法:
df.head(5)
或 df[:5]
显示前五行。df.dtypes
返回列的类型。df.shape
返回行和列的数量。df.info()
总结了所有相关信息还可以将一个或几个列设置为索引。这个过程如下所示:
索引在Pandas中有很多用途:
所有这些都是以更高的内存消耗和更不明显的语法为代价的。
用已经存储在内存中的数据构建一个DataFrame竟是如此的超凡脱俗,以至于它可以转换你输入的任何类型的数据:
第一种情况,没有行标签,Pandas用连续的整数来标注行。第二种情况,它对行和列都做了同样的事情。向Pandas提供列的名称而不是整数标签(使用列参数),有时提供行的名称。如下图所示:
要为索引列指定一个名字,可以写df.index.name = 'city_name'
或者使用pd.DataFrame(..., index=pd.Index(['Oslo', 'Vienna', 'Tokyo'], name= city_name))
。
下一个选择是用NumPy向量的dict
或二维NumPy数组构造一个DataFrame:
请注意第二种情况下,人口值是如何被转换为浮点数的。实际上,这发生在构建NumPy数组的早期。这里需要注意,从二维NumPy数组中构建数据框架是一个默认的视图。这意味着改变原始数组中的值会改变DataFrame,反之亦然。此外,它还可以节省内存。
这种模式也可以在第一种情况下启用(NumPy向量的dict
),通过设置copy=False
。但这简单的操作可能在不经意间把它变成一个副本。
还有两个创建DataFrame的选项(不太有用):
dict
的列表中(每个dict
代表一个行,它的键是列名,它的值是相应的单元格值)。dict
(每个Series
代表一个列;默认返回copy
,它可以被告知返回一个copy=False
的视图)。如果你 "即时" 添加流媒体数据,则你最好的选择是使用字典或列表,因为 Python 在列表的末尾透明地预分配了空间,所以追加的速度很快。NumPy 数组和 Pandas DataFrame都没有这样做。另一种方法(如果你事先知道行的数量)是用类似 DataFrame(np.zeros)
的东西来手动预分配内存。
关于DataFrame最好的事情是你可以:
df.area
返回列值(或者,df['area']
-适合包含空格的列名)。df.population /= 10**6
,人口以百万为单位存储,下面的命令创建了一个新的列,称为 "density",由现有列中的值计算得出:此外,你甚至可以对来自不同DataFrame的列进行算术运算,只要它们的行是有意义的标签,如下图所示:
普通的方括号根本不足以满足所有的索引需求。你不能通过标签访问行,不能通过位置索引访问不相干的行,你甚至不能引用单个单元格,因为df['x', 'y']
是为MultiIndex准备的!
DataFrame有两种可供选择的索引模式:loc
用于通过标签进行索引,iloc
用于通过位置索引进行索引。
在Pandas中,引用多行/列是一种复制,而不是一种视图。但它是一种特殊的复制,允许作为一个整体进行赋值:
df.loc['a']=10
工作(单行可作为一个整体写入)。df.loc['a']['A']=10
起作用(元素访问传播到原始df)。df.loc['a':'b'] = 10
个作品(分配给一个子数作为一个整体作品)。df.loc['a':'b']['A']=10
不会(对其元素的赋值不会)。最后一种情况,该值将只在切片的副本上设置,而不会反映在原始df中(将相应地显示一个警告)。
根据情况的背景,有不同的解决方案:
df.loc['a':'b', 'A'] = 10
df1 = df.loc['a':'b']; df1['A']=10 # SettingWithCopy警告
为了摆脱这种情况下的警告,让它成为一个真正的副本:
df1 = df.loc['a':'b'].copy(); df1['A']=10
Pandas还支持一种方便的NumPy语法,用于布尔索引。
当使用几个条件时,它们必须用括号表示,如下图所示:
当你期望返回一个单一的值时,你需要特别注意。
因为有可能有几条符合条件的记录,所以loc返回一个Series。要想从中得到一个标量值,你可以使用:
float(s)
或更通用的s.item()
,都会引发ValueError,除非系列中正好有一个值。s.iloc[0]
,只有在没有找到时才会引发异常;同时,它也是唯一一个支持赋值的:df[...].iloc[0] = 100
,但当你想修改所有匹配时,肯定不需要它:df[...] = 100
。另外,你也可以使用基于字符串的查询:
df.query('name=="Vienna"')
df.query('opulation>1e6 and area<1000')
它们更短,与MultiIndex配合得很好,而且逻辑运算符优先于比较运算符(=不需要括号),但它们只能按行过滤,而且你不能通过它们修改DataFrame。
一些第三方库可以使用SQL语法直接查询DataFrames(duckdb[3]),或者通过将DataFrame复制到SQLite并将结果包装成Pandas对象(pandasql[4])间接查询。不出所料,直接方法更快。
你可以将普通的操作,如加、减、乘、除、模、幂等,应用于DataFrame、Series以及它们的组合。
所有的算术运算都是根据行和列的标签来排列的:
在DataFrames和Series的混合操作中,Series的行为(和广播)就像一个行-向量
,并相应地被对齐:
可能是为了与列表和一维NumPy向量保持一致(这些向量没有通过标签对齐,并且期望其大小如同DataFrame是一个简单的二维NumPy数组):
因此,在用列-向量
序列分割DataFrame这种不理想的情况下(也是最常见的情况!),你必须使用方法而不是运算符,你可以看到如下:
由于这个有问题的决定,每当你需要在DataFrame和类似列的Series之间进行混合操作时,你必须在文档中查找它(或记住它):
add, sub, mul, div, mod, pow, floordiv
Pandas有三个函数,concat
(concatenate
的缩写)、merge和join
,它们都在做同样的事情:把几个DataFrame的信息合并成一个。但每个函数的做法略有不同,因为它们是为不同的用例量身定做的。
这可能是将两个或多个DataFrame合并为一个的最简单的方法:你从第一个DataFrame中提取行,并将第二个DataFrame中的行附加到底部。为了使其发挥作用,这两个DataFrame需要有(大致)相同的列。这与NumPy中的vstack
类似,你如下图所示:
在索引中出现重复的值是不好的,会遇到各种各样的问题。即使不关心索引,也要尽量避免在其中有重复的值:
reset_index=True
参数df.reset_index(drop=True)
来重新索引从0到len(df)-1
的行、如果DataFrames的列不完全匹配(不同的顺序在这里不算),Pandas可以采取列的交集(kind='inner'
,默认)或插入NaNs
来标记缺失的值(kind='outer'
):
Concat
还可以进行水平stacking(类似于NumPy中的hstack
):
join
比concat
更具可配置性:特别是,它有五种连接模式,而concat
只有两种。详情见下面的 "1:1关系连接"
部分。
如果行和列的标签都重合,concat
可以做一个相当于垂直堆叠的MultiIndex(像NumPy的dstack
):
如果行和/或列部分重叠,Pandas将相应地对齐名称,而这很可能不是你想要的结果:
一般来说,如果标签重叠,就意味着DataFrame之间有某种程度的联系,实体之间的关系最好用关系型数据库的术语来描述。
这时,关于同一组对象的信息被存储在几个不同的DataFrame中,而你想把它合并到一个DataFrame中。
如果你想合并的列不在索引中,可以使用merge
。
它首先丢弃在索引中的内容;然后它进行连接;最后,它将结果从0到n-1
重新编号。
如果该列已经在索引中,你可以使用join
(这只是merge
的一个别名,left_index
或right_index
设置为True,默认值不同)。
从这个简化的案例中你可以看到(见上面的 "full outer join 全外链"),与关系型数据库相比,Pandas在保持行的顺序方面是相当灵活的。左边和右边的外部连接往往比内部和外部连接更容易理解。所以,如果你想保证行的顺序,你必须对结果进行明确的排序,或者使用CategoricalIndex
(pdi.lock
)。
这是数据库设计中最广泛使用的关系,表A的一条记录(例如 "State")可以与表B的几条记录(例如 "City")相连,但是表B的每条记录只能与表A的一条记录相连(等于一个City只能在一个State,但是一个State由多个City组成)。
就像1:1
的关系一样,要在Pandas中连接一对1:n
的相关表,你有两个选择。如果要merge
的列不在索引中,而且你可以丢弃在两个表的索引中的内容,那么就使用merge
,例如:
merge()默认执行inner join
Merge对行顺序的保持不如 Postgres 那样严格。文档中的 "保留键序" 声明只适用于left_index=True
和/或right_index=True
(其实就是join
的别名),并且只在要合并的列中没有重复值的情况下适用。这就是为什么merge
和join
有一个排序参数。
现在,如果要合并的列已经在右边DataFrame的索引中,请使用join
(或者用right_index=True
进行合并,这完全是同样的事情):
join()在默认情况下做左外连接
这一次,Pandas同时保持了左边DataFrame的索引值和行的顺序不变。
注意:要小心,如果第二个表有重复的索引值,你会在结果中出现重复的索引值,即使左表的索引是唯一的
有时,连接的DataFrame有相同名称的列。merge
和 join
都有一种方法来解决这种模糊性,但语法略有不同(另外,默认情况下,merge
会用'_x'、'_y'
来解决,而连接会引发一个异常),你可以在下面的图片中看到:
总结一下:
merge
执行的是内连接,join
执行的是左外连接;join
是merge
的一个别名,带有left_index=True
和/或right_index=True
。如上所述,当join
针对两个DataFrame运行时,例如df.join(df1)
,它作为merge
的一个别名。但是join
也有一个 "多重连接" 模式,它反过来又是concat(axis=1)
的一个别名。
与普通模式相比,这种模式有些限制:
1:1
的关系(索引到索引的连接)。因此,多个1:n
的关系应该被逐一连接。'pandas-illustrated'
也有一个辅助器,你可以看到下面:
pdi.join
是对join
的一个简单包装,它接受on、how
和suffixes
参数的列表,这样你就可以在一条命令中进行多个join
。就像原来的join
一样,on
列与第一个DataFrame有关,而其他DataFrame是根据它们的索引来连接的。
由于DataFrame是一个列的集合,对行的操作比对列的操作更容易。例如,插入一列总是在原表进行,而插入一行总是会产生一个新的DataFrame,如下图所示:
删除列也需要注意,除了del df['D']
能起作用,而del df.D
不能起作用(在Python层面的限制)。
用drop
删除行的速度出奇的慢,如果原始标签不是唯一的,就会导致错综复杂的bug。比如说:
一个解决方案是使用ignore_index=True
,它告诉concat
在连接后重置行名:
在这种情况下,可以将名字列设置为索引。但是对于更复杂的过滤器来说,这就没有什么用了。
然而,另一个快速、通用的解决方案,甚至适用于重复的行名,就是使用索引而不是删除。你可以手动否定这个条件,或者使用pdi
库中的(一行长的)自动化:
这个操作已经在 Series
部分做了详细描述:Pandas图鉴(二):Series 和 Index。但是DataFrame的 groupby
在此基础上还有一些特殊的技巧。
首先,你可以只用一个名字来指定要分组的列,如下图所示:
如果没有as_index=False
,Pandas会把进行分组的那一列作为索引列。如果这不可取,你可以使用reset_index()
或者指定as_index=False
。
通常情况下,DataFrame中的列比你想在结果中看到的要多。默认情况下,Pandas会对任何可远程求和的东西进行求和,所以必须缩小你的选择范围,如下图:
注意,当对单列求和时,会得到一个Series而不是一个DataFrame。如果出于某种原因,想要一个DataFrame,你可以:
df.groupby('product')[['quantity']].sum()
或df.groupby('product')['quantity'].sum().to_frame()
切换到数字索引也会使它成为一个DataFrame:
df.groupby('product', as_index=False)['quantity'].sum()
或df.groupby('product')['quantity'].sum().reset_index()
但是,尽管外观不寻常,在很多情况下,系列的行为就像一个DataFrame,所以也许pdi.patch_series_repr()
的 "改头换面" 就足够了。
在分组时,不同的列有时应该被区别对待。例如,对数量求和是完全可以的,但对价格求和则没有意义。使用.aggall
可以为不同的列指定不同的聚合函数,如图所示:
或者,你可以为一个单列创建几个聚合函数:
或者,为了避免繁琐的列重命名,你可以这样做:
有时,预定义的函数并不足以产生所需的结果。例如,在平均价格时,最好使用权重。所以你可以为此提供一个自定义函数。与Series相比,该函数可以访问组的多个列(它被送入一个子DataFrame作为参数),如下图所示:
注意,不能在一个命令中结合预定义的聚合和几列范围的自定义函数,比如上面的那个,因为aggreg只接受一列范围的用户函数。一列范围内的用户函数唯一可以访问的是索引,这在某些情况下是很方便的。例如,那一天,香蕉以50%的折扣出售,这可以从下面看到:
为了从自定义函数中访问group by
列的值,它被事先包含在索引中。
通常最少的定制功能会产生最好的性能。因此,按照速度递增的顺序:
g.apply()
实现多列范围的自定义函数g.agg()
实现单列范围的自定义函数(支持用Cython或Numba加速)。一个从不同角度看数据的有用工具--通常与分组一起使用--是透视表。
假设你有一个取决于两个参数i
和j
的变量a
,有两种等价的方式来表示它是一个表格:
当数据是 "dense
" 的时候,"dense
"格式更合适(当有很少的零或缺失元素时),而当数据是 "sparse
"的时候,"long
"格式更好(大多数元素是零/缺失,可以从表中省略)。当有两个以上的参数时,情况会变得更加复杂。
自然,应该有一个简单的方法来在这些格式之间进行转换。而Pandas为它提供了一个简单方便的解决方案:透视表。
作为一个不那么抽象的例子,请考虑以下表格中的销售数据。两个客户购买了指定数量的两种产品。最初,这个数据是长格式
的。要将其转换为宽格式
,请使用df.pivot
:
这条命令抛弃了与操作无关的东西(即索引和价格列),并将所要求的三列信息转换为长格式,将客户名称放入结果的索引中,将产品名称放入其列中,将销售数量放入其 "正文"中。
至于反向操作,你可以使用stack
。它将索引和列合并到MultiIndex
中:
eset_index
如果你想只stack
某些列,你可以使用melt
:
请注意,熔体以不同的方式排列结果的行。
pivot
失去了关于结果的 "主体" 名称的信息,所以对于 stack
和 melt
,我们必须 "提醒" Pandas关于 quantity
列的名称。
在上面的例子中,所有的值都是存在的,但它不是必须的:
对数值进行分组,然后对结果进行透视的做法非常普遍,以至于groupby
和pivot
已经被捆绑在一起,成为一个专门的函数(和一个相应的DataFrame方法)pivot_table
:
aggfunc
参数控制应该使用哪个聚合函数对行进行分组(默认为平均值)。
为了方便,pivot_table
可以计算小计和大计:
一旦创建,数据透视表就变成了一个普通的DataFrame,所以它可以使用前面描述的标准方法进行查询:
当与MultiIndex一起使用时,数据透视表特别方便。我们已经看到很多例子,Pandas函数返回一个多索引的DataFrame。我们仔细看一下。
[1]
Pandas: https://pandas.pydata.org/
[2]
Polars: https://www.pola.rs/
[3]
duckdb: https://duckdb.org/
[4]
pandasql: https://pypi.org/project/pandasql