前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Pandas图鉴(三):DataFrames

Pandas图鉴(三):DataFrames

作者头像
数据STUDIO
发布2023-09-04 13:04:17
4000
发布2023-09-04 13:04:17
举报
文章被收录于专栏:数据STUDIO
Pandas[1]是用Python分析数据的工业标准。只需敲几下键盘,就可以加载、过滤、重组和可视化数千兆字节的异质信息。它建立在NumPy库的基础上,借用了它的许多概念和语法约定,所以如果你对NumPy很熟悉,你会发现Pandas是一个相当熟悉的工具。即使你从未听说过NumPy,Pandas也可以让你在几乎没有编程背景的情况下轻松拿捏数据分析问题。

Pandas 给 NumPy 数组带来的两个关键特性是:

  1. 异质类型 —— 每一列都允许有自己的类型
  2. 索引 —— 提高指定列的查询速度

事实证明,这些功能足以使Pandas成为Excel和数据库的强大竞争者。

Polars[2]是Pandas最近的转世(用Rust编写,因此速度更快,它不再使用NumPy的引擎,但语法却非常相似,所以学习 Pandas 后对学习 Polars 帮助非常大。

Pandas 图鉴系列文章由四个部分组成:

我们将拆分成四个部分,依次呈现~建议关注和星标@公众号:数据STUDIO,精彩内容等你来~

Part 3. DataFrames

数据框架的剖析

Pandas的主要数据结构是一个DataFrame。它捆绑了一个二维数组,并为其行和列加上标签。它由许多系列对象组成(有一个共享的索引),每个对象代表一个列,可能有不同的dtypes。

读取和写入CSV文件

构建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

用已经存储在内存中的数据构建一个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代表一个行,它的键是列名,它的值是相应的单元格值)。
  • 从一个Series的dict(每个Series代表一个列;默认返回copy,它可以被告知返回一个copy=False的视图)。

如果你 "即时" 添加流媒体数据,则你最好的选择是使用字典或列表,因为 Python 在列表的末尾透明地预分配了空间,所以追加的速度很快。NumPy 数组和 Pandas DataFrame都没有这样做。另一种方法(如果你事先知道行的数量)是用类似 DataFrame(np.zeros) 的东西来手动预分配内存。

使用DataFrame的基本操作

关于DataFrame最好的事情是你可以:

  • 很容易访问它的列,例如,df.area返回列值(或者,df['area']-适合包含空格的列名)。
  • 把这些列当作独立变量来操作,例如,df.population /= 10**6,人口以百万为单位存储,下面的命令创建了一个新的列,称为 "density",由现有列中的值计算得出:

此外,你甚至可以对来自不同DataFrame的列进行算术运算,只要它们的行是有意义的标签,如下图所示:

索引DataFrames

普通的方括号根本不足以满足所有的索引需求。你不能通过标签访问行,不能通过位置索引访问不相干的行,你甚至不能引用单个单元格,因为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中(将相应地显示一个警告)。

根据情况的背景,有不同的解决方案:

  1. 你想改变原始数据框架df。那么就用
代码语言:javascript
复制
df.loc['a':'b', 'A'] = 10
  1. 你已经故意做了副本,并想在该副本上工作:
代码语言:javascript
复制
df1 = df.loc['a':'b']; df1['A']=10 # SettingWithCopy警告

为了摆脱这种情况下的警告,让它成为一个真正的副本:

代码语言:javascript
复制
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算术

你可以将普通的操作,如加、减、乘、除、模、幂等,应用于DataFrame、Series以及它们的组合。

所有的算术运算都是根据行和列的标签来排列的:

在DataFrames和Series的混合操作中,Series的行为(和广播)就像一个行-向量,并相应地被对齐:

可能是为了与列表和一维NumPy向量保持一致(这些向量没有通过标签对齐,并且期望其大小如同DataFrame是一个简单的二维NumPy数组):

因此,在用列-向量序列分割DataFrame这种不理想的情况下(也是最常见的情况!),你必须使用方法而不是运算符,你可以看到如下:

由于这个有问题的决定,每当你需要在DataFrame和类似列的Series之间进行混合操作时,你必须在文档中查找它(或记住它):

add, sub, mul, div, mod, pow, floordiv

合并DataFrames

Pandas有三个函数,concatconcatenate的缩写)、merge和join,它们都在做同样的事情:把几个DataFrame的信息合并成一个。但每个函数的做法略有不同,因为它们是为不同的用例量身定做的。

垂直stacking

这可能是将两个或多个DataFrame合并为一个的最简单的方法:你从第一个DataFrame中提取行,并将第二个DataFrame中的行附加到底部。为了使其发挥作用,这两个DataFrame需要有(大致)相同的列。这与NumPy中的vstack类似,你如下图所示:

在索引中出现重复的值是不好的,会遇到各种各样的问题。即使不关心索引,也要尽量避免在其中有重复的值:

  • 要么使用reset_index=True参数
  • 调用df.reset_index(drop=True)来重新索引从0到len(df)-1的行、
  • 使用keys参数来解决与MultiIndex的歧义(见下文)。

如果DataFrames的列不完全匹配(不同的顺序在这里不算),Pandas可以采取列的交集(kind='inner',默认)或插入NaNs来标记缺失的值(kind='outer'):

水平stacking

Concat 还可以进行水平stacking(类似于NumPy中的hstack):

joinconcat更具可配置性:特别是,它有五种连接模式,而concat只有两种。详情见下面的 "1:1关系连接" 部分。

通过MultiIndex进行堆叠

如果行和列的标签都重合,concat可以做一个相当于垂直堆叠的MultiIndex(像NumPy的dstack):

如果行和/或列部分重叠,Pandas将相应地对齐名称,而这很可能不是你想要的结果:

一般来说,如果标签重叠,就意味着DataFrame之间有某种程度的联系,实体之间的关系最好用关系型数据库的术语来描述。

1:1的关系joins

这时,关于同一组对象的信息被存储在几个不同的DataFrame中,而你想把它合并到一个DataFrame中。

如果你想合并的列不在索引中,可以使用merge

它首先丢弃在索引中的内容;然后它进行连接;最后,它将结果从0到n-1重新编号。

如果该列已经在索引中,你可以使用join(这只是merge的一个别名,left_indexright_index设置为True,默认值不同)。

从这个简化的案例中你可以看到(见上面的 "full outer join 全外链"),与关系型数据库相比,Pandas在保持行的顺序方面是相当灵活的。左边和右边的外部连接往往比内部和外部连接更容易理解。所以,如果你想保证行的顺序,你必须对结果进行明确的排序,或者使用CategoricalIndexpdi.lock)。

1:n关系joins

这是数据库设计中最广泛使用的关系,表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的别名),并且只在要合并的列中没有重复值的情况下适用。这就是为什么mergejoin有一个排序参数。

现在,如果要合并的列已经在右边DataFrame的索引中,请使用join(或者用right_index=True进行合并,这完全是同样的事情):

join()在默认情况下做左外连接

这一次,Pandas同时保持了左边DataFrame的索引值和行的顺序不变。

注意:要小心,如果第二个表有重复的索引值,你会在结果中出现重复的索引值,即使左表的索引是唯一的

有时,连接的DataFrame有相同名称的列。mergejoin 都有一种方法来解决这种模糊性,但语法略有不同(另外,默认情况下,merge会用'_x'、'_y'来解决,而连接会引发一个异常),你可以在下面的图片中看到:

总结一下:

  • 在非索引列上进行合并连接,连接要求 "right" 列是有索引的;
  • 合并丢弃左边DataFrame的索引,连接保留它;
  • 默认情况下,merge执行的是内连接,join执行的是左外连接;
  • 合并不保留行的顺序,连接保留它们(有一些限制);
  • joinmerge的一个别名,带有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、howsuffixes参数的列表,这样你就可以在一条命令中进行多个join。就像原来的join一样,on列与第一个DataFrame有关,而其他DataFrame是根据它们的索引来连接的。

插入和删除

由于DataFrame是一个列的集合,对行的操作比对列的操作更容易。例如,插入一列总是在原表进行,而插入一行总是会产生一个新的DataFrame,如下图所示:

删除列也需要注意,除了del df['D']能起作用,而del df.D不能起作用(在Python层面的限制)。

drop删除行的速度出奇的慢,如果原始标签不是唯一的,就会导致错综复杂的bug。比如说:

一个解决方案是使用ignore_index=True,它告诉concat在连接后重置行名:

在这种情况下,可以将名字列设置为索引。但是对于更复杂的过滤器来说,这就没有什么用了。

然而,另一个快速、通用的解决方案,甚至适用于重复的行名,就是使用索引而不是删除。你可以手动否定这个条件,或者使用pdi库中的(一行长的)自动化:

Group by

这个操作已经在 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加速)。
  • 预定义函数(Pandas或NumPy函数对象,或其名称为字符串)。

一个从不同角度看数据的有用工具--通常与分组一起使用--是透视表。

Pivoting 和 "unpivoting"

假设你有一个取决于两个参数ij的变量a,有两种等价的方式来表示它是一个表格:

当数据是 "dense" 的时候,"dense"格式更合适(当有很少的零或缺失元素时),而当数据是 "sparse"的时候,"long"格式更好(大多数元素是零/缺失,可以从表中省略)。当有两个以上的参数时,情况会变得更加复杂。

自然,应该有一个简单的方法来在这些格式之间进行转换。而Pandas为它提供了一个简单方便的解决方案:透视表。

作为一个不那么抽象的例子,请考虑以下表格中的销售数据。两个客户购买了指定数量的两种产品。最初,这个数据是长格式的。要将其转换为宽格式,请使用df.pivot

这条命令抛弃了与操作无关的东西(即索引和价格列),并将所要求的三列信息转换为长格式,将客户名称放入结果的索引中,将产品名称放入其列中,将销售数量放入其 "正文"中。

至于反向操作,你可以使用stack。它将索引和列合并到MultiIndex中:

eset_index

如果你想只stack某些列,你可以使用melt

请注意,熔体以不同的方式排列结果的行。

pivot失去了关于结果的 "主体" 名称的信息,所以对于 stackmelt,我们必须 "提醒" Pandas关于 quantity 列的名称。

在上面的例子中,所有的值都是存在的,但它不是必须的:

对数值进行分组,然后对结果进行透视的做法非常普遍,以至于groupbypivot已经被捆绑在一起,成为一个专门的函数(和一个相应的DataFrame方法)pivot_table

  • 没有列参数,它的行为类似于groupby;
  • 当没有重复的行来分组时,它的工作方式就像透视一样;
  • 否则,它就进行分组和透视。

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据STUDIO 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Part 3. DataFrames
  • 读取和写入CSV文件
  • 创建一个DataFrame
  • 使用DataFrame的基本操作
  • 索引DataFrames
  • DataFrame算术
  • 合并DataFrames
  • 垂直stacking
  • 水平stacking
  • 通过MultiIndex进行堆叠
  • 1:1的关系joins
  • 1:n关系joins
  • 多重连接
  • 插入和删除
  • Group by
  • Pivoting 和 "unpivoting"
    • 参考资料
    相关产品与服务
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档