Pandas图鉴:Series 和 Index

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

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

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

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

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

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

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

  • Part 1. Motivation:Pandas图鉴(一):Pandas vs Numpy
  • Part 2. Series and Index
  • Part 3. DataFrames
  • Part 4. MultiIndex

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

Part 2. Series 和 Index

Series剖析Series剖析

Series是NumPy中一维数组的对应物,是DataFrame代表其列的基本构件。尽管与DataFrame相比,它的实际重要性正在减弱(你完全可以在不知道Series是什么的情况下解决很多实际问题),但如果不先学习Series和Index,可能很难理解DataFrame的工作原理。

在内部,Series将数值存储在一个普通的NumPy向量中。因此,它继承了它的优点(紧凑的内存布局,快速的随机访问)和缺点(类型同质性,缓慢的删除和插入)。在此基础上,可以通过标签访问Series的值,使用一个叫做index的类似数字的结构。标签可以是任何类型的(通常是字符串和时间戳)。它们不需要是唯一的,但唯一性是提高查询速度所需要的,并且在许多操作中都是假定的。

图片图片

现在每个元素都可以用两种方式来处理:通过label(=使用索引)和通过position(=不使用索引):

图片图片

按位置寻址by position 有时被称为 by positional index,这只是增加了混乱。

很明显,一对方括号是不够的。特别是:

  • s[2:3]不是解决2号元素的最方便方式
  • 如果标签恰好是整数,s[1:3]就变得模糊不清。它可能是指标签1到3(含)或位置指数1到3(不含)。

为了解决这些问题,Pandas又有两种方括号的 "口味":

图片图片

  • .loc[]总是使用标签并包括区间的两端;
  • .iloc[]总是使用位置索引,并排除了右端。

在这里使用方括号而不是小括号的目的是为了获得方便的Python切分:可以使用一个单冒号或双冒号,其含义是熟悉的start:stop:step。缺失的 start(end) 就是从系列的开始(到结束)。步骤参数允许用s.iloc[::2]来引用偶数行,用s['Paris':'Oslo':-1]来获取反向顺序的元素。

它们还支持布尔索引(用布尔数组进行索引),如该图所示:

Series.isin(), Series.between()Series.isin(), Series.between()

而可以在这张图片中看到他们是如何支持 "花式索引" 的(用整数阵列进行索引):

图片图片

由于某些原因,Series没有一个漂亮的富文本外观,所以与DataFrame相比,看似比较低级:

图片图片

这里对Series进行稍加修饰,使其看起来更好,如下图所示:

图片图片

竖线意味着这是一个Series,而不是一个DataFrame。

也可以用pdi.sidebyside(obj1, obj2, ...)来并排显示几个系列或DataFrames:

图片图片

pdi(代表pandas illustrated)是github上的一个开源库pdi[3],具有本文的这个和其他功能。安装非常方便:

pip install pandas-illustrated

索引

负责通过标签获取系列元素(以及DataFrame的行和列)的对象被称为索引。索引速度很快:无论有5个元素还是50亿个元素,都可以在一定的时间内得到结果。

索引是一个真正的多态对象。默认情况下,当创建一个没有索引参数的Series(或DataFrame)时,它初始化为一个类似于Python的range()的惰性对象。就像range()一样,它几乎不使用任何内存,并提供与位置索引相吻合的标签。

现在创建一个有一百万个元素的系列:

>>> s = pd.Series(np.zeros(10**6))
>>> s.index
RangeIndex(start=0, stop=1000000, step=1)
>>> s.index.memory_usage() # 字节数
128 # 与Series([0.])的情况相同

现在,如果删除一个元素,索引就会隐含地变形为一个类似口令的结构,如下所示:

>>> s1 = s.drop(1)
>>> s1.index
Int64Index([ 0, 2, 3, 4, 5, 6, 7、
      ...
      999993, 999994, 999995, 999996, 999997, 999998, 999999],
      dtype='int64', length=999999)
>>> s1.index.memory_usage()
7999992

这个结构消耗了8Mb的内存!,为了避免这种情况,并回到轻量级的类似范围的结构,我们写下:

>>> s2 = s1.reset_index(drop=True)。
>>> s2.index
RangeIndex(start=0, stop=999999, step=1)
>>> s2.index.memory_usage()
128

如果你是Pandas的新手,你可能会想为什么Pandas不自己做呢?对于非数字标签来说,这有点显而易见:为什么(以及如何)Pandas在删除一行后,会重新标记所有后续的行?对于数字标签,答案就有点复杂了。

首先,Pandas 纯粹通过位置来引用行,所以如果想在删除第3行之后再去找第5行,可以不用重新索引(这就是iloc的作用)。

第二,保留原始标签是一种与过去某个时刻保持联系的方式,就像 "保存游戏" 按钮。如果你有一个有一百列和一百万行的大表,需要找到一些数据。你逐一进行了几次查询,每次都缩小了搜索范围,但只看了列的一个子集,因为同时看到所有的一百个字段是不现实的。现在你已经找到了目标行,想看到原始表中关于它们的所有信息。一个数字索引可以帮助你立即得到它。

从原理上讲,如下图所示:

图片图片

一般来说,需要保持索引值的唯一性。例如,在索引中存在重复的值时,查询速度的提升并不会提升。Pandas没有像关系型数据库那样的 "唯一约束"(该功能[4]仍在试验中),但它有一些函数来检查索引中的值是否唯一,并以各种方式删除重复值。

有时,但一索引不足以唯一地识别某行。例如,同名的城市有时碰巧出现在不同的国家,甚至在同一个国家的不同地区。因此,(城市,州)是一个比单独的城市更适合识别一个地方的候选者。在数据库中,它被称为 "复合主键"。在Pandas中,它被称为MultiIndex(第4部分),索引内的每一列都被称为level。

索引的另一个重要特性是它是不可改变的。与DataFrame中的普通列相比,你不能就地修改它。索引中的任何变化都涉及到从旧的索引中获取数据,改变它,并将新的数据作为一个新的索引重新连接起来。例如,要将列名就地转换为字符串(节省内存),可以写df.columns = df.columns.astype(str),或者不就地转换(对链式方法有用)df.set_axis(df.columns.astype(str), axis=1)。但正是由于不可更改性,不允许只写df.City.name = 'city',所以必须借助于df.rename(columns={'City': 'city'})。

索引有一个名字(在MultiIndex的情况下,每一层都有一个名字)。而这个名字在Pandas中没有被充分使用。一旦在索引中包含了列,就不能再使用方便的df.column_name符号了,而必须恢复到不太容易阅读的df.index或者更通用的df.loc[]。有了MultiIndex。df.merge--可以用名字指定要合并的列,不管这个列是否属于索引。

按值查找元素

考虑以下Series对象:

图片图片

索引提供了一种快速而方便的方法,可以通过标签找到一个值。但是,通过值来寻找标签呢?

s.index[s.tolist().find(x)] # 对于len(s)< 1000来说更快
s.index[np.where(s.value==x)[0][0]] # 对于 len(s) > 1000,速度更快

pdi中有一对包装器,叫做find()和findall(),它们速度快(因为它们根据Series的大小自动选择实际的命令),而且更容易使用。

如下代码所示:

>>> import pdi
>>> pdi.find(s, 2)
'penguin'
>>> pdi.findall(s, 4)
Index(['cat', 'dog'], dtype='object')

缺失值

Pandas使用者对缺失值特别关注。通常情况下,可以通过向read_csv提供一个标志来接收一个带有NaN的DataFrame。否则,可以在构造函数或赋值运算符中使用None(尽管对于不同的数据类型,它的实现方式略有不同),例如:

图片图片

对于NaN,可以做的第一件事是了解是否有任何NaN。从上图可以看出,isna()产生一个布尔数组,而.sum()给出缺失值的总数。

现在你知道它们的存在,可以选择通过删除、用常量值填充或插值来摆脱它们,如下所示:

fillna(), dropna(), interpolate()fillna(), dropna(), interpolate()

另一方面,可以继续使用它们。大多数Pandas函数都会忽略缺失的值:

图片图片

更高级的函数(median, rank, quantile等)也是如此。

算术操作是根据索引来调整的:

图片图片

在索引中存在非唯一值的情况下,其结果是不一致的。不要对具有非唯一索引的系列使用算术运算。

比较

对有缺失值的数组进行比较可能很棘手。这里有一个例子:

>>> np.all(pd.Series([1., None, 3.]) ==
      pd.Series([1., None, 3.]))
False
>>> np.all(pd.Series([1, None, 3], dtype='Int64') ==
      pd.Series([1, None, 3], dtype='Int64'))
True
>>> np.all(pd.Series(['a', None, 'c']) ==
      pd.Series(['a', None, 'c']))
False

为了正确地进行比较,NaN需要被替换成保证在数组中缺少的东西。例如,用''、-1或∞:

>>> np.all(s1.fillna(np.inf) == s2.fillna(np.inf))  #对所有的dtypes都有效
True

或者更好的是,使用标准的NumPy或Pandas比较函数:

>>> s = pd.Series([1., None, 3.])
>>> np.array_equal(s.value, s.value, equal_nan=True)
True
>>> len(s.compare(s)) == 0
True

这里,比较函数返回一个差异列表(实际上是一个DataFrame),而array_equal直接返回一个布尔值。

当比较混合类型的DataFrame时,NumPy就会出问题(问题#19205[5]),而Pandas做得非常好。下面是这一情况:

>>> df = pd.DataFrame({'a': [1., None, 3.], 'b': ['x', None, 'z']})
>>> np.array_equal(df.values, df.values, equal_nan=True)
TypeError
<...>
>>> len(df.compare(df)) == 0
True

添加、插入、删除

尽管系列对象应该是大小不可变的,但有可能在原地追加、插入和删除元素,但所有这些操作都是:

  • 缓慢,因为它们需要为整个对象重新分配内存并更新索引;
  • 痛苦的不方便。

下面是插入数值的一种方式和删除数值的两种方式:

图片图片

第二种删除值的方法(通过删除)比较慢,而且在索引中存在非唯一值的情况下可能会导致复杂的错误。

Pandas有df.insert方法,但它只能将列(而不是行)插入到数据框架中(而且对序列根本不起作用)。

另一种追加和插入的方法是用iloc对DataFrame进行切片,应用必要的转换,然后用concat把它放回去。pdi中实现了一个叫做insert的函数,可以自动完成这个过程:

图片图片

注意,(就像在df.insert中一样)插入的位置是由0<=i<=len(s)的位置给出的,而不是由索引中的元素的标签。

你可以为一个新元素提供一个标签。对于一个非数字性的索引,它是必须的。例如:

图片图片

要通过标签指定插入点,你可以把pdi.find和pdi.insert结合起来,如下图所示:

图片图片

注意,与df.insert不同,pdi.insert返回一个副本,而不是在原地修改Series/DataFrame。

统计数据

Pandas提供了全方位的统计功能。它们可以深入了解百万元素系列或数据框架中的内容,而无需手动滚动数据。

所有的Pandas统计函数都会忽略NaN,如下图所示:

图片图片

注意,Pandas std给出的结果与NumPy std不同。

由于系列中的每个元素都可以通过标签或位置索引来访问,所以有一个argmin(argmax)的姐妹函数,叫做idxmin(idxmax),如图所示:

图片图片

下面是Pandas的自描述性统计函数的列表,供参考:

  • std,样本标准差;
  • var,无偏方差;
  • sem,无偏标准误差的平均值;
  • quantile,样本四分位数(s.quantile(0.5) ≈ s.median());
  • mode,即出现频率最高的值;
  • nlargest和nsmallest,默认情况下,按外观顺序排列;
  • diff,第一次离散差分;
  • cumsum和cumprod,累积和,以及乘积;
  • cummin和cummax,累积最小和最大。

还有一些更专业的统计功能:

  • pct_change,当前和前一个元素之间的变化百分比;
  • skew,无偏差的偏度(第三时刻);
  • kurt 或 kurtosis,无偏的谷度(第四时刻);
  • cov,corr 和 autocorr,协方差,相关,和自相关;
  • rolling、加权和指数加权的窗口。

重复数据

特别注意检测和处理重复的数据,可以在图片中看到:

is_unique,nunique, value_countsis_unique,nunique, value_counts

drop_duplicates 和 duplicated 可以保留最后出现的,而不是第一个。

请注意,s.unique()比np.unique要快(O(N)vs O(NlogN)),它保留了顺序,而不是像np.unique那样返回排序后的结果。

缺失值被当作普通值处理,这有时可能会导致令人惊讶的结果。

图片图片

如果想排除NaN,你需要明确地做到这一点。在这个特殊的例子中,s.dropna().is_unique == True。

还有一个单调函数家族:

  • s.is_monotonic_increasing()、
  • s.is_monotonic_decreasing()、
  • s._strict_monotonic_increasing()、
  • s._string_monotonic_decreasing()
  • s.is_monotonic() - 这是s.is_monotonic_increasing()的同义词,对于单调下降的序列返回False!

字符串和正则表达式

几乎所有的Python字符串方法在Pandas中都有一个矢量的版本:

count, upper, replacecount, upper, replace

当这样的操作返回多个值时,有几个选项来决定如何使用它们:

split, join, explodesplit, join, explode

如果知道正则表达式,Pandas也有矢量版本的常用操作:

findall, extract, replacefindall, extract, replace

Group by

在数据处理中,一个常见的操作是计算一些统计数据,而不是对整个数据集,而是对其中的某些组。第一步是通过提供将一个Series(或一个DataFrame)分成若干组的标准来建立一个惰性对象。这个惰性的对象没有任何有意义的表示,但它可以是:

  • 迭代(产生分组键和相应的子系列--非常适合于调试):

groupbygroupby

  • 以与普通系列相同的方式进行查询,以获得每组的某个属性(比迭代快):

图片图片

所有操作都不包括NaNs

在这个例子中,根据数值除以10的整数部分,将系列分成三组。对于每一组,要求提供元素的总和,元素的数量,以及每一组的平均值。

除了这些集合功能,还可以根据特定元素在组内的位置或相对价值来访问它们。下面是这种情况:

min, median, max, first, nth, lastmin, median, max, first, nth, last

你也可以用g.agg(['min', 'max'])一次计算几个函数,或者用g.describe()一次显示一大堆的统计函数。

如果这些还不够,也可以通过自己的Python函数传递数据。它可以是

  • 用g.apply(f)接受一个组x(一个系列对象)并生成一个单一的值(如sum())的函数f。
  • 一个函数f接受一个组x(一个系列对象),并用g.transform(f)生成一个与x相同大小的系列对象(例如,cumsum())。

图片图片

在上面的例子中,输入的数据被排序了。这对于groupby来说是不需要的。实际上,如果组内元素不是连续存储的,它也同样能工作,所以它更接近collections.defaultdict而不是itertools.groupby。而且它总是返回一个没有重复的索引。

图片图片

与defaultdict和关系型数据库的GROUP BY子句不同,Pandas groupby是按组名排序的。它可以用sort=False来禁用,如下代码所示:

>>> s = pd.Series([1, 3, 20, 2, 10])
>>> for k, v in s.groupby(s//10, sort=False):
       print(k, v.tolist())
0 [1, 3, 2]
2 [20]
1 [10]

参考资料

[1]Pandas: https://pandas.pydata.org/

[2]Polars: https://www.pola.rs/

[3]pdi: https://github.com/axil/pandas-illustrated

[4]该功能: https://pandas.pydata.org/docs/reference/api/pandas.Flags.allows_duplicate_labels.html

[5]问题#19205: https://github.com/numpy/numpy/issues/19205

责任编辑:武晓燕 来源: 数据STUDIO
相关推荐

2023-07-31 11:44:38

Pandas性能数组

2021-08-17 09:55:50

pandas 8indexPython

2011-08-29 16:08:11

MIUI小米手机

2011-03-30 11:00:53

Windows 8

2024-11-26 08:00:00

SQLPandasPandaSQL

2020-04-24 15:47:31

互联网公司裁员

2022-07-14 09:24:28

大数据技术

2020-03-10 08:55:50

PandasNumPy函数

2023-10-15 17:07:35

PandasPython库

2023-07-06 14:49:44

PandasPolars语法

2023-05-05 18:45:21

Python人工智能机器学习

2023-11-30 15:53:43

2010-09-15 13:54:36

WidgetOPhone

2021-06-08 09:18:54

SQLPandas数据透视表

2012-12-14 14:48:01

诺基亚Series 40S40

2021-01-22 05:53:08

C# IndexRange

2022-06-17 10:52:01

数据存储采集

2010-08-31 10:30:59

CSSpositionz-index

2013-01-25 15:19:07

Series 40S40

2013-01-25 15:13:58

Series 40S40
点赞
收藏

51CTO技术栈公众号