Skip to content

第 12 章 Pandas 进阶

本章内容提要:

  • 深入 Pandas 数据结构
  • 迭代与函数应用
  • 数据清洗
  • 简单可视化

本书在第 8 章、第 10 章分别向读者介绍了 Pandas 的基本数据结构、操作和导入常见的数据文件,本章的内容将更加深入,除了介绍更多的数据类型,本章还包含函数应用、数据清洗等数据处理的一些核心技能。

12.1 深入 Pandas 数据结构

12.1.1 回顾

在学习新的知识之前,我们不妨先来回顾和整理一下目前接触到的 Pandas 的数据结构以及它们的联系。

NumPy 数组是 Pandas 数据结构的构成核心,用于存储数据值。我们常用一维和二维的 ndarray。

In [1]: import numpy as np
In [2]: a = np.arange(9)
In [3]: a
Out[3]: array([0, 1, 2, 3, 4, 5, 6, 7, 8])
In [4]: b = np.arange(9).reshape((3, 3))
In [5]: b
Out[5]:
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])

Pandas 的 Series 就是在一维 ndarray 的基础上添加了对数据含义的描述,也就是所谓的索引或标签。与 ndarray 本身所支持的整数索引所不同的是,Pandas 同时支持整数索引和字符索引。

默认情况下整数索引会被使用,而且是一个范围索引对象 RangeIndex,该对象减少了对内存的利用。例如,0-9 即可用起点 0,步长 1,终止点 10 加以表述。更大范围的数值也是如此。

In [6]: import pandas as pd
In [7]: pd.RangeIndex(10)
Out[7]: RangeIndex(start=0, stop=10, step=1)

下面代码确认了 RangeIndex 默认被使用:

In [8]: a_series = pd.Series([5, 7, 9])
In [9]: a_series.index
Out[9]: RangeIndex(start=0, stop=3, step=1)

当然,Pandas 的特色在于对字符索引的支持,字符索引既可以明确数值含义,也建立的映射关系方便数据的访问、修改等操作。

加上字符索引,上面的 Series 摇身一变成为了 3 个用户某个属性的度量值。

In [10]: a_series = pd.Series([5, 7, 9], index = ['user1', 'user2', 'user3'])
In [11]: a_series
Out[11]:
user1    5
user2    7
user3    9
dtype: int64

不妨加上个名字,让含义限定为信用得分。

In [12]: a_series = pd.Series([5, 7, 9], index = ['user1', 'user2', 'user3'], name='credit_score')
In [13]: a_series
Out[13]:
user1    5
user2    7
user3    9
Name: credit_score, dtype: int64

这里 Series 只能表示用户的一种属性,DataFrame 进行了拓展,支持多种属性且不同属性的数据类型可以不同。这完美地与工作中常见的表格数据对应了起来。虽然说数据的主体表现方式是一个矩阵,但与 2 维 ndarray 是完全不同的。

以下代码展示了一个典型的数据框,行一般用于表示独立的记录,如这里的 student;列一般表示记录的相关属性,如这里 student 的 score 和 height。

In [14]: df = pd.DataFrame([[5, 166], [7, 178], [9, 160]],
    ...: index=['student1', 'student2', 'student3'], columns=['score', 'height'])
In [15]: df
Out[15]:
          score  height
student1      5     166
student2      7     178
student3      9     160

行索引依旧是使用 index 描述,为了描述不同的列,DataFrame 引入了 column 属性值。这样,两个维度的索引和数据含义的描述对应了起来。

总结一下,Pandas 的数据结构是由 NumPy 数组加上数据描述组成,其中

  • Series = 1 维 ndarray + index
  • DataFrame = 多个 1 维 ndarray + index + column

这些知识可以归纳为一个比较形象的图形,如图 12-1 所示。

图12-1 Numpy 数组与 Pandas 数据结构对比(图片来自网络)

12.1.2 分类变量

本小节介绍一个新的数据类型——分类变量。分类有时也称为因子型变量(factor),它用于表示重复的文本列。一些包含有限个元素的列常常会在需要处理的数据中出现,如性别、国家、一些程序描述词(低、中、高)等。分类变量的元素是固定的,如性别只会有男、女。分类变量有时候可能有顺序,如低 < 中 < 高。

看到这里,读者可能心中对分类变量有了一个比较形象的描述:有序的集合。没错,它看起来就是如此。那么分类变量在数据分析时有什么用呢?Pandas 库为什么要提供这样一个数据类型?

  • 节省存储——分类变量在存储时是将字符串映射为整数值的,这大大节省了内存的使用。数据越大,效率越高。例如,有 10 万个 one、two、three,分类变量将它们映射为 1、2、3 进行存储,而不是实际的英文字符。
  • 分类排序——例如有 3 个分类 one、two、three,我们需要绘制它们的频数条形图。我们可以使用分类变量按照自己的的想法排列这三个分类,控制绘图时它们的排序。

有两种办法可以创建分类变量,一种是在创建 Pandas 的 Series 或 DataFrame 时指定数据类型 dtype 为 category,第二种是直接使用 Pandas 提供的构造器函数 Categorical()。

我们先看第一种办法:

In [16]: pd.Series(['a', 'a', 'b', 'c', 'b'], dtype='category')
Out[16]:
0    a
1    a
2    b
3    c
4    b
dtype: category
Categories (3, object): [a, b, c]

我们对比下不指定该参数值时的结果:

In [17]: pd.Series(['a', 'a', 'b', 'c', 'b'])
Out[17]:
0    a
1    a
2    b
3    c
4    b
dtype: object

两者的差别主要体现在 dtype 上,默认存储字符使用的是 object 类型,当 dtype 指定为 category 后,Pandas 将 5 个字符转换具有 3 个唯一值 [a, b, c] 的类别。也就是说这个生成的 Series 存储的数据是从 [a, b, c] 中重复抽样的结果。

我们再看如何使用第二种方法构造分类变量。

In [2]: pd.Categorical(['a', 'a', 'b', 'c', 'b'])
Out[2]:
[a, a, b, c, b]
Categories (3, object): [a, b, c]

函数文档显示我们可以自定义类别以及是否排序。

pd.Categorical(
    values,
    categories=None,
    ordered=None,
    dtype=None,
    fastpath=False,
)

我们试一试:

In [6]: pd.Categorical(['a', 'a', 'b', 'c', 'b'], categories=['a', 'c'])
Out[6]:
[a, a, NaN, c, NaN]
Categories (2, object): [a, c]

In [7]: pd.Categorical(['a', 'a', 'b', 'c', 'b'], ordered=True)
Out[7]:
[a, a, b, c, b]
Categories (3, object): [a < b < c]

第一行代码我们指定了合法的类别是 “a” 和 “c”,所以其他的字母都会被转换为 NaN 值。第二行代码的结果显示了不仅有 3 个类别,而且还存在 a < b < c 的顺序关系。

对于分类数据,我们一个常见需求是获取元素的频数或频率,这可以通过 describe() 方法实现。

In [9]: cts = pd.Categorical(['a', 'a', 'b', 'c', 'b'], ordered=True)
In [10]: cts.describe()
Out[10]:
            counts  freqs
categories
a                2    0.4
b                2    0.4
c                1    0.2

该对象的类别也是有用的,这可能使用对象的属性值 categories 获取。

In [11]: cts.categories
Out[11]: Index(['a', 'b', 'c'], dtype='object')

另外,ordered 属性可以给出对象是否经过排序,返回的是一个布尔值。

In [12]: cts.ordered
Out[12]: True

对分类对象常见的操作有重命名、新增、删除和比较,下面举例介绍。

上面我们看到分类信息存储在对象的 categories 属性中,我们重写该属性即可重命名类别。

In [13]: cts_new = cts.copy()
In [14]: cts_new.categories = ['aa', 'bb', 'cc']
In [15]: cts
Out[15]:
[a, a, b, c, b]
Categories (3, object): [a < b < c]
In [16]: cts_new
Out[16]:
[aa, aa, bb, cc, bb]
Categories (3, object): [aa < bb < cc]

最后的输出显示所有的元素都被替换了,这是一个非常有用的特性。

增加新的类别可以使用 add_categories() 方法实现,新的类别会被添加到最后。

In [17]: cts_new.add_categories(['ff'])
Out[17]:
[aa, aa, bb, cc, bb]
Categories (4, object): [aa < bb < cc < ff]

删除类别后,原有的值会被 NaN 值替代:

In [19]: cts_new.remove_categories("bb")
Out[19]:
[aa, aa, NaN, cc, NaN]
Categories (2, object): [aa < cc]

分类对象的比较在对象是有序时比较有用。

In [23]: cts
Out[23]:
[a, a, b, c, b]
Categories (3, object): [a < b < c]

In [24]: cts2 = pd.Categorical(['b', 'c', 'a', 'a'], ordered=True)
In [25]: cts > cts2
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-25-d954cff14835> in <module>
----> 1 cts > cts2

~/miniconda3/lib/python3.7/site-packages/pandas/core/arrays/categorical.py in f(self, other)
    113                 other_codes = other._codes
    114
--> 115             mask = (self._codes == -1) | (other_codes == -1)
    116             f = getattr(self._codes, op)
    117             ret = f(other_codes)

ValueError: operands could not be broadcast together with shapes (5,) (4,)
In [26]: cts2 = pd.Categorical(['b', 'c', 'a', 'a', 'a'], ordered=True)
In [27]: cts > cts2
Out[27]: array([False, False,  True,  True,  True])

当两个对象都是分类对象时一定要注意长度要一致,并且设定的类别一致。

当其中一个对象是标量时,计算会自动进行广播。

In [28]: cts > 'b'
Out[28]: array([False, False, False,  True, False])

读者可以自行尝试其他对比类型的结果。

12.1.3 时间序列

数据的生成和采集往往是连续的过程,这离不开时间的累积。 时间序列即是按时间顺序组成的数据序列,它展示了数据变化的趋势、可能的周期性和规律性。 时间序列分析的主要目的是根据已有的历史数据寻找规律、建立模型用来对未来的数据值进行预测。 这种类型的分析常用于金融领域,Pandas 的创建的初始目的就是为了处理金融数据,因此提供 了时间日期对象和丰富的时序分析功能特性。

时间日期

Python 的标准库就提供了对日期和时间的支持,如计算当前的时间戳,我们可以使用下面的代码:

In [32]: import time
In [33]: time.time()
Out[33]: 1576340722.0232272

时间戳是以 1970 年 1 月 1 日零点经过了多长时间来表示。

时间戳单位最适于做日期运算。但是 1970 年之前的日期就无法以此表示了。 未来太遥远的日期也不可以,Linux/macOS 和 Windows 系统只支持到 2038 年。

将时间戳传递给 localtime() 函数,我们可以获得更为可读的时间记录。

In [34]: time.localtime(time.time())
Out[34]: time.struct_time(tm_year=2019, tm_mon=12, tm_mday=15, tm_hour=10, tm_min=6, tm_sec=45, tm_wday=6, tm_yda
y=349, tm_isdst=0)

如果想要获得更为简要的时间表示,可以将上述代码传为 asctime() 的参数:

In [36]: time.asctime(time.localtime(time.time()))
Out[36]: 'Sun Dec 15 10:09:29 2019'

时间日期格式化符号

上一个代码的结果是按照星期、日期、时间、年份的结果输出的,很多时候我们需要自己格式化时间日期的显示。 因此,了解相关的格式化符号是有必要的,它在所有的时间日期有关的 Python 包或其他编程语言里面都是通用的。

常用的格式化符号汇总如下表。

符号 含义
%y 两位数的年份表示(00-99)
%Y 四位数的年份表示(000-9999)
%m 月份(01-12)
%d 月内中的一天(0-31
%H 24小时制小时数(0-23
%I 12小时制小时数(01-12)
%M 分钟数(00=59)
%S 秒(00-59)
%a 简化星期名称
%A 完整星期名称
%b 简化的月份名称
%B 完整的月份名称
%c 本地相应的日期表示和时间表示
%j 年内的一天(001-366)
%p 本地A.M.或P.M.的等价符
%U 一年中的星期数(00-53)星期天为星期的开始
%w 星期(0-6),星期天为星期的开始
%W 一年中的星期数(00-53)星期一为星期的开始
%x 本地相应的日期表示
%X 本地相应的时间表示
%Z 当前时区的名称
%% %号本身

time 模块提供了 strftime() 函数用于格式化。 下面举一个简单的例子,以年月日时间的顺序输出当前时间日期,该格式是我们平时最常见的格式。

In [37]: time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
Out[37]: '2019-12-15 10:28:23'

datetime 模块

datetime 模块也是 Python 提供的标准库,它在分析中更为常用。 该模块提供了 4 个主要的类用于表示时间日期及其变化。

  • time:只包含时、分、秒、微妙等信息
  • date:只包含年、月、日、星期等信息
  • datetime:包含上述两种信息
  • timedelta:表示 datetime 之间差值的类

这里我们仅介绍最常见的时间日期表示,更为详细的内容请读者阅读官方文档。

时间表示一般可以分为本地时间和世界标准时,当然也可以用时间戳,但可读性很差。

In [39]: now = datetime.datetime.now()  # 当前本地时间
In [40]: now
Out[40]: datetime.datetime(2019, 12, 15, 10, 34, 54, 516482)
In [41]: utc = datetime.datetime.utcnow()  # 当前世界标准时
In [42]: utc
Out[42]: datetime.datetime(2019, 12, 15, 2, 35, 18, 609633)
In [45]: now.timestamp()  # 当前时间戳
Out[45]: 1576377294.516482

格式化字符串可以调用 strftime() 方法(注意,在 time 模块中使用的是同名函数)。

In [46]: now.strftime("%Y-%m-%d %H:%M:%S")
Out[46]: '2019-12-15 10:34:54'

时间差也比较常用,直接将两个 datetime 对象相减就可以,返回的是相差的秒数和微秒数。 另外也可以直接通过对应的属性值访问。

In [47]: now2 = datetime.datetime.now()
In [48]: now2 - now
Out[48]: datetime.timedelta(seconds=486, microseconds=231216)
In [49]: td = now2 - now
In [52]: td.seconds
Out[52]: 486

由于 datetime 模块比较好用,Pandas 库直接将其引入作为一个子模块。

下面代码显示了调用 datetime 子模块的 now() 函数得到的是一个 datetime 对象。

In [55]: pd.datetime.now()
Out[55]: datetime.datetime(2019, 12, 15, 10, 47, 58, 642985)

Pandas 日期序列

在处理时间日期数据时我们会经常需要生成日期序列以及转换不同的日期频率(季度、月份、周等), Pandas 库在这方面提供了相关的功能特性。

使用 date_range() 函数可以创建日期序列,默认的频率是天。

In [56]: pd.date_range('20190101', periods=7)
Out[56]:
DatetimeIndex(['2019-01-01', '2019-01-02', '2019-01-03', '2019-01-04',
               '2019-01-05', '2019-01-06', '2019-01-07'],
              dtype='datetime64[ns]', freq='D')

D 是 Day 的缩写。我们可以更改日期的频率,比如说月份。

In [58]: pd.date_range('20190101', periods=7, freq='M')
Out[58]:
DatetimeIndex(['2019-01-31', '2019-02-28', '2019-03-31', '2019-04-30',
               '2019-05-31', '2019-06-30', '2019-07-31'],
              dtype='datetime64[ns]', freq='M')

商业分析中常常只使用工作日,这可以使用 bdate_range() 生成序列,它会自动跳过周末。

In [59]: pd.bdate_range('20190101', periods=7)
Out[59]:
DatetimeIndex(['2019-01-01', '2019-01-02', '2019-01-03', '2019-01-04',
               '2019-01-07', '2019-01-08', '2019-01-09'],
              dtype='datetime64[ns]', freq='B')

此时输出结果显示频率是 B(商业的英文单词首字母)。在生成的序列中,1 月 5 号 和 6 号被自动跳过了。

时间差

Pandas 库提供了 Timedelta 类来表示时间差异,相比于 datetime 模块提供的函数, 它更加的灵活和功能丰富。

我们可以直接传入具有描述性的英文语句,它会被 Pandas 自动解析。

In [60]: pd.Timedelta('1 days 2 hours 3 minutes 4 seconds')
Out[60]: Timedelta('1 days 02:03:04')

我们也可以使用整数值,并指定时间差的单位来生成 Timedelta 对象。

In [61]: pd.Timedelta(10, unit='h')
Out[61]: Timedelta('0 days 10:00:00')

我们还可以传入关键字参数表示时间的频率。

In [64]: pd.Timedelta(days=10)
Out[64]: Timedelta('10 days 00:00:00')
In [65]: pd.Timedelta(hours=10)
Out[65]: Timedelta('0 days 10:00:00')
In [66]: pd.Timedelta(minutes=10)
Out[66]: Timedelta('0 days 00:10:00')

Timedelta 对象常用于时间的加减运算中,运算支持自动广播,下面是简单的例子。

In [67]: pd.date_range('20190101', periods=7)
Out[67]:
DatetimeIndex(['2019-01-01', '2019-01-02', '2019-01-03', '2019-01-04',
               '2019-01-05', '2019-01-06', '2019-01-07'],
              dtype='datetime64[ns]', freq='D')
In [68]: pd.date_range('20190101', periods=7) + pd.Timedelta(hours=10)
Out[68]:
DatetimeIndex(['2019-01-01 10:00:00', '2019-01-02 10:00:00',
               '2019-01-03 10:00:00', '2019-01-04 10:00:00',
               '2019-01-05 10:00:00', '2019-01-06 10:00:00',
               '2019-01-07 10:00:00'],
              dtype='datetime64[ns]', freq='D')
In [69]: pd.date_range('20190101', periods=7) - pd.Timedelta(hours=10)
Out[69]:
DatetimeIndex(['2018-12-31 14:00:00', '2019-01-01 14:00:00',
               '2019-01-02 14:00:00', '2019-01-03 14:00:00',
               '2019-01-04 14:00:00', '2019-01-05 14:00:00',
               '2019-01-06 14:00:00'],
              dtype='datetime64[ns]', freq='D')

12.2 迭代与函数应用

12.2.1 迭代

Pandas 对象之间的基本迭代的行为取决于数据类型。 当迭代一个 Series 对象时,它被视为数组,迭代会逐一使用元素值。 而 DataFrame 遵循类似的规则迭代对象的列标签。

我们先分别生成一个 Series 和 DataFrame 对象。

In [74]: s = pd.Series(['a', 'b', 'c'])
In [75]: df = df = {'姓名': ['小明','小王','小张'], '语文':[80,85,90], '数学':[99,88,86]}
In [76]: df = pd.DataFrame(df)
In [77]: s
Out[77]:
0    a
1    b
2    c
dtype: object
In [78]: df
Out[78]:
   姓名  语文  数学
0  小明  80  99
1  小王  85  88
2  小张  90  86

用 for 循环迭代两个对象看看结果是否如前面所说。

In [80]: for i in s:
    ...:     print(i)
    ...:
a
b
c
In [81]: for i in df:
    ...:     print(i)
    ...:
姓名
语文
数学

的确如此。Series 对象和 DataFrame 对象的 for 循环差别很大。

当我们需要迭代 Series 对象的索引时可以通过 index 属性访问。

In [82]: for i in s.index:
    ...:     print(i)
    ...:
0
1
2

迭代 DataFrame 的需求常常不只是获取列标签,还有对内容进行迭代。 可以使用的方法有以下 3 种。

  • iteritems() - 迭代键值对。
  • iterrows() - 将行迭代为索引 Series 对。
  • itertuples() - 以命名元组的形式迭代行。

先看看第一个方法:

In [84]: for key, value in df.iteritems():
    ...:     print(key, value)
    ...:
姓名 0    小明
1    小王
2    小张
Name: 姓名, dtype: object
语文 0    80
1    85
2    90
Name: 语文, dtype: int64
数学 0    99
1    88
2    86
Name: 数学, dtype: int64
In [85]: for key, value in df.iteritems():
    ...:     print(type(value))
    ...:
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>

iteritems() 方法以 DataFrame 的列标签为键,列值为值进行迭代。 每一个值都是 Series 对象。

我们再来看第二个方法:

In [87]: for key, value in df.iterrows():
    ...:     print(key, value)
    ...:
0 姓名    小明
语文    80
数学    99
Name: 0, dtype: object
1 姓名    小王
语文    85
数学    88
Name: 1, dtype: object
2 姓名    小张
语文    90
数学    86
Name: 2, dtype: object
In [88]: for row, value in df.iterrows():
    ...:     print(type(value))
    ...:
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>

iterrows() 方法的结果也是 Series 对象,以 DataFrame 的列标签作为索引。 读者需要注意的是,此时由于每一行是一个 Series 对象,之前 DataFrame 每列 的数据类型会自动强制转换,因此当前每一个 Series 都是字符对象 object。

我们最后看看第三个方法 itertuples():

In [89]: for key, value in df.itertuples():
    ...:     print(key, value)
    ...:
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-89-6b42ad46ae68> in <module>
----> 1 for key, value in df.itertuples():
      2     print(key, value)
      3

ValueError: too many values to unpack (expected 2)

In [90]: for value in df.itertuples():
    ...:     print(value)
    ...:
Pandas(Index=0, 姓名='小明', 语文=80, 数学=99)
Pandas(Index=1, 姓名='小王', 语文=85, 数学=88)
Pandas(Index=2, 姓名='小张', 语文=90, 数学=86)

In [91]: for value in df.itertuples():
    ...:     print(type(value))
    ...:
<class 'pandas.core.frame.Pandas'>
<class 'pandas.core.frame.Pandas'>
<class 'pandas.core.frame.Pandas'>

当我们使用与前两个方法类似的操作时,程序报错了,原因是该方法生成的每一个元素 都是一个类名为 Pandas 的独立元组。我们可以使用 tuple() 将其转换为 Python 内置 的元组对象。

In [92]: for value in df.itertuples():
    ...:     print(tuple(value))
    ...:
(0, '小明', 80, 99)
(1, '小王', 85, 88)
(2, '小张', 90, 86)

12.2.2 函数应用

针对 DataFrame 对象一般有 3 个不同层面的操作:一是整个 DataFrame, 二是按行或按列,三是每一个元素。如果我们想要将包/库提供的函数 应用到 DataFrame 上,有三个相应的重要方法,它们分别是 pipe()、apply() 和 applymap()。

Series 对象也可以使用,不过此处内容聚焦于 DataFrame 对象的操作。

pipe()

pipe() 是表格级别的函数应用,我们先定义一个乘法器。

In [97]: def timer(e1, e2):
    ...:     return(e1*e2)
    ...:

创建用于示例的 DataFrame 对象:

In [98]: df1 = pd.DataFrame(6*np.random.randn(6, 3), columns=['col1', 'col2', 'col3'])
In [99]: df1
Out[99]:
        col1      col2       col3
0  -2.327459  4.391074   8.796776
1   3.736191  2.711543 -11.112365
2  -5.686908 -0.246942  -0.692201
3   4.060646  9.178073   1.355170
4  10.171053 -3.417467   0.447833
5  -7.363384 -0.176782  -6.391243

使用 pipe() 调用该上述定义的乘法器,对 df1 乘以 10。

In [100]: df1.pipe(timer, 10)
Out[100]:
         col1       col2        col3
0  -23.274593  43.910736   87.967759
1   37.361914  27.115432 -111.123654
2  -56.869085  -2.469423   -6.922007
3   40.606458  91.780725   13.551700
4  101.710534 -34.174668    4.478325
5  -73.633838  -1.767825  -63.912430

这里 10 自动进行了广播拓展到 df1 相同的大小再进行的运算,传入 pipe() 第二个参数也可以是相同大小的 DataFrame。

In [103]: df1.pipe(timer, pd.DataFrame(6*np.random.randn(6, 3), columns=['col1', 'col2', 'col3']))
Out[103]:
        col1        col2       col3
0   5.756520  -26.905602  21.285264
1  20.548535  -10.953445 -99.671865
2  -2.653793    1.188218   2.159359
3  15.746131  225.602231  13.177158
4  95.979467   -1.891072   0.294889
5  14.734334    0.651522  27.829243

pipe() 函数的实用性并不强,由于广播机制的存在,我们完全可以直接 使用运算符达到相同的目的。

In [104]: df1 * 10
Out[104]:
         col1       col2        col3
0  -23.274593  43.910736   87.967759
1   37.361914  27.115432 -111.123654
2  -56.869085  -2.469423   -6.922007
3   40.606458  91.780725   13.551700
4  101.710534 -34.174668    4.478325
5  -73.633838  -1.767825  -63.912430
In [105]: df1 * df1
Out[105]:
         col1       col2        col3
0    5.417067  19.281527   77.383267
1   13.959126   7.352467  123.484665
2   32.340928   0.060981    0.479142
3   16.488844  84.237015    1.836486
4  103.450327  11.679079    0.200554
5   54.219421   0.031252   40.847987

apply()

apply() 是 3 个方法中最常用最实用的,可以对列或行进行函数应用。 默认情况下,apply() 对列进行操作。

还是使用上面的数据和函数,目的也一样,对每列乘以 10。

In [114]: df1.apply(timer, axis=0, e2=10)
Out[114]:
         col1       col2        col3
0  -23.274593  43.910736   87.967759
1   37.361914  27.115432 -111.123654
2  -56.869085  -2.469423   -6.922007
3   40.606458  91.780725   13.551700
4  101.710534 -34.174668    4.478325
5  -73.633838  -1.767825  -63.912430

这里 df1 被 apply() 传入为 timer() 函数的第一个参数,第二个参数必须用 关键字参数指定。

我们可以指定 apply() 应用于特定的列或行,实际上此时就是 Series 对象使用 apply()。

例如,我们只操作第 3 列或第 3 行。

In [123]: df1.iloc[:,2].apply(timer, e2=10)
Out[123]:
0     87.967759
1   -111.123654
2     -6.922007
3     13.551700
4      4.478325
5    -63.912430
Name: col3, dtype: float64

In [124]: df1.iloc[2,].apply(timer, e2=10)
Out[124]:
col1   -56.869085
col2    -2.469423
col3    -6.922007
Name: 2, dtype: float64

applymap()

applymap() 进行的是元素级别的应用,它也完全可以做到上述 pipe() 的示例结果。

这里我们直接调用匿名函数,更加方便快捷。

In [125]: df1.applymap(lambda x: 10 * x)
Out[125]:
         col1       col2        col3
0  -23.274593  43.910736   87.967759
1   37.361914  27.115432 -111.123654
2  -56.869085  -2.469423   -6.922007
3   40.606458  91.780725   13.551700
4  101.710534 -34.174668    4.478325
5  -73.633838  -1.767825  -63.912430

当然这体现不出该方法的优势,applymap() 在对所有元素做选择性操作时才是最有价值的。

例如,我们对 df1 中小于 0 的平方,大于 0 的加 10。

In [126]: df1.applymap(lambda x: x ** 2 if x < 0 else x + 10)
Out[126]:
        col1       col2        col3
0   5.417067  14.391074   18.796776
1  13.736191  12.711543  123.484665
2  32.340928   0.060981    0.479142
3  14.060646  19.178073   11.355170
4  20.171053  11.679079   10.447833
5  54.219421   0.031252   40.847987

12.2.3 字符串函数

除了数值计算,数据分析常常也处理文本数据,字符串函数在其中作用重大。 Pandas 库为文本数据提供了字符属性,可以方便地利用 Python 内置字符串函数 同名方法进行操作。

本小节将对常见的字符串操作函数进行举例,最后进行表格汇总。

我们先构建一个样例数据。

In [127]: sample_data = pd.Series(['Mike', 'Shixiang', np.nan, '012345', 'HAPPY', 'hurry'])
In [128]: sample_data
Out[128]:
0        Mike
1    Shixiang
2         NaN
3      012345
4       HAPPY
5       hurry
dtype: object

使用字符串方法前需要访问 str 属性。

lower()

lower() 方法将所有字母变为小写。

In [129]: sample_data.str.lower()
Out[129]:
0        mike
1    shixiang
2         NaN
3      012345
4       happy
5       hurry
dtype: object

upper()

upper() 方法的作用与 lower() 相反。

In [130]: sample_data.str.upper()
Out[130]:
0        MIKE
1    SHIXIANG
2         NaN
3      012345
4       HAPPY
5       HURRY
dtype: object

len()

len() 方法获取字符长度。

In [131]: sample_data.str.len()
Out[131]:
0    4.0
1    8.0
2    NaN
3    6.0
4    5.0
5    5.0
dtype: float64

replace()

replace() 方法替换字符串。

In [132]: sample_data.str.replace('H', 'YY')
Out[132]:
0        Mike
1    Shixiang
2         NaN
3      012345
4      YYAPPY
5       hurry
dtype: object

count()

count() 方法对指定字符进行计数。

In [133]: sample_data.str.count('a')
Out[133]:
0    0.0
1    1.0
2    NaN
3    0.0
4    0.0
5    0.0
dtype: float64

swapcase()

swapcase() 方法转换字母大小写。

In [134]: sample_data.str.swapcase()
Out[134]:
0        mIKE
1    sHIXIANG
2         NaN
3      012345
4       happy
5       HURRY
dtype: object

其他的方法不再一一列举,全部都汇总为下表。

方法 描述
lower() Series/Index中的字符串转换为小写
upper() Series/Index中的字符串转换为大写
len() 计算字符串长度
strip() 帮助从两侧的系列/索引中的每个字符串中删除空格(包括换行符)
split(' ') 用给定的模式拆分每个字符串
cat(sep=' ') 使用给定的分隔符连接系列/索引元素
get_dummies() 返回具有单热编码值的 DataFrame
contains(pattern) 如果元素中包含子字符串,则返回每个元素的布尔值True,否则为False
replace(a,b) 将字符a替换为值b
repeat(value) 重复每个元素指定的次数
count(pattern) 返回模式中每个元素的出现总数
startswith(pattern) 如果系列/索引中的元素以模式开始,则返回true
endswith(pattern) 如果系列/索引中的元素以模式结束,则返回true
find(pattern) 返回模式第一次出现的位置
findall(pattern) 返回模式的所有出现的列表
swapcase() 变换字母大小写
islower() 检查系列/索引中每个字符串中的所有字符是否小写,返回布尔值
isupper() 检查系列/索引中每个字符串中的所有字符是否大写,返回布尔值
isnumeric() 检查系列/索引中每个字符串中的所有字符是否为数字,返回布尔值

12.2.4 分组计算

分组计算提供了一种非常强大的汇总技术,它的核心可以分为 3 个步骤: 拆分、应用和合并。

前面的 apply() 函数可以让我们对某列或行进行数值计算,在真实的世界中, 我们可能需要根据表格的某一列分组,然后分别计算每个组别中其他列的汇总值,如和,均值。

Pandas 提供了 groupby() 方法完成上面的需求。

我们看一个来自 Pandas 官方文档的示例,计算不同动物最大速度的均值。

df = pd.DataFrame({'Animal': ['Falcon', 'Falcon',
                               'Parrot', 'Parrot'],
                   'Max Speed': [380., 370., 24., 26.]})
In [136]: df
Out[136]:
   Animal  Max Speed
0  Falcon      380.0
1  Falcon      370.0
2  Parrot       24.0
3  Parrot       26.0

In [137]: df.groupby(['Animal']).mean()
Out[137]:
        Max Speed
Animal
Falcon      375.0
Parrot       25.0

虽然数据很简单,但它足以帮助我们理解它的操作方法,其核心步骤描述如下:

  • 拆分 - 将 DataFrame 按照 Animal 分为两个子 DataFrame;
  • 应用 - 对两个子 DataFrame 的速度列进行均值函数计算得到汇总值;
  • 合并 - 将分组计算的结果合并起来。

12.3 数据清洗

作为一名数据工作者,我们 80% 的时间不是花在数据的转换和计算上,而是花在数据清洗上。 由于原始数据的来源不一致、数据记录的人力物力投入不平衡、数据存储格式的设计不相同等等 各种原因,数据的缺失、不规整是现实生活中不可避免问题。特别是在当前流行的机器学习 和数据挖掘等领域,质量层次不齐的数据导致模型预测面临严重的准确性和可拓展性问题。

12.3.1 缺失值值处理

当数据记录缺失时,一般用 NA(Not Available)值代表,NA 值处理时数据清洗的重点。 由计算引入的 NaN(Not A Number)也可以归入缺失值。

我们先生成一个简单的缺失值数据。

In [138]: df = pd.DataFrame(np.random.randn(4, 4), index = ['user1', 'user2', 'user3', 'user4'], columns=['c
     ...: ol1', 'col2', 'col3', 'col4'])
In [139]: df
Out[139]:
           col1      col2      col3      col4
user1  0.368869  1.021476 -0.771651 -1.908077
user2  0.023887  0.799769 -0.230265 -0.800586
user3 -0.139025 -0.032772  1.078525 -1.453405
user4 -1.042709  1.022162 -0.686548 -1.497647
In [141]: df = df.reindex(['user0', 'user1', 'user2', 'user3', 'user4', 'user5'])
In [142]: df
Out[142]:
           col1      col2      col3      col4
user0       NaN       NaN       NaN       NaN
user1  0.368869  1.021476 -0.771651 -1.908077
user2  0.023887  0.799769 -0.230265 -0.800586
user3 -0.139025 -0.032772  1.078525 -1.453405
user4 -1.042709  1.022162 -0.686548 -1.497647
user5       NaN       NaN       NaN       NaN

检查缺失值

Pandas 库提供了 isnull() 和 notnull() 函数对缺失值进行检测。

我们既可以检测整个 DataFrame,也可以值关注某一列。

In [143]: df.isnull()
Out[143]:
        col1   col2   col3   col4
user0   True   True   True   True
user1  False  False  False  False
user2  False  False  False  False
user3  False  False  False  False
user4  False  False  False  False
user5   True   True   True   True

In [144]: df.col1.isnull()
Out[144]:
user0     True
user1    False
user2    False
user3    False
user4    False
user5     True
Name: col1, dtype: bool

缺失值相关计算

当数据存在缺失值时,Pandas 计算时会自动忽略它们。当所有的元素是缺失值时, 结果返回缺失值。注意在求和数据时,缺失值会被当做 0 处理。

In [145]: df.sum()
Out[145]:
col1   -0.788979
col2    2.810636
col3   -0.609939
col4   -5.659715
dtype: float64

In [146]: pd.Series([np.nan, np.nan]).sum()
Out[146]: 0.0

In [147]: pd.Series([np.nan, np.nan]).mean()
Out[147]: nan

填充缺失值

Pandas 库提供了诸多方法用于清除缺失值。其中,fillna() 函数可以通过集中方法 填充缺失值,下面举例介绍。

最常见的策略是用一个标量填充缺失值,如果没有特别的需求,一般可以设为 0。

In [148]: df.fillna(0)
Out[148]:
           col1      col2      col3      col4
user0  0.000000  0.000000  0.000000  0.000000
user1  0.368869  1.021476 -0.771651 -1.908077
user2  0.023887  0.799769 -0.230265 -0.800586
user3 -0.139025 -0.032772  1.078525 -1.453405
user4 -1.042709  1.022162 -0.686548 -1.497647
user5  0.000000  0.000000  0.000000  0.000000

还可以设定缺失值根据前后的数据进行填充,分为向前和向后两种。

In [150]: df.fillna(method='pad')   # 向前填充
Out[150]:
           col1      col2      col3      col4
user0       NaN       NaN       NaN       NaN
user1  0.368869  1.021476 -0.771651 -1.908077
user2  0.023887  0.799769 -0.230265 -0.800586
user3 -0.139025 -0.032772  1.078525 -1.453405
user4 -1.042709  1.022162 -0.686548 -1.497647
user5 -1.042709  1.022162 -0.686548 -1.497647

In [151]: df.fillna(method='backfill')  # 向后填充
Out[151]:
           col1      col2      col3      col4
user0  0.368869  1.021476 -0.771651 -1.908077
user1  0.368869  1.021476 -0.771651 -1.908077
user2  0.023887  0.799769 -0.230265 -0.800586
user3 -0.139025 -0.032772  1.078525 -1.453405
user4 -1.042709  1.022162 -0.686548 -1.497647
user5       NaN       NaN       NaN       NaN

含缺失值的数据提供的是不完整的信息,在样本较多时可以考虑直接舍弃。

使用 dropna() 方法可以直接去掉含缺失值的行或列,默认是行。

In [152]: df.dropna()
Out[152]:
           col1      col2      col3      col4
user1  0.368869  1.021476 -0.771651 -1.908077
user2  0.023887  0.799769 -0.230265 -0.800586
user3 -0.139025 -0.032772  1.078525 -1.453405
user4 -1.042709  1.022162 -0.686548 -1.497647

如果按列去除,df 就没有可以用的数据了。

In [153]: df.dropna(axis=1)
Out[153]:
Empty DataFrame
Columns: []
Index: [user0, user1, user2, user3, user4, user5]

12.3.2 连接

最后用于汇报或者绘图的数据可能来自多个数据表格,我们有时需要将它们合并到一起, 称为连接。Pandas 库提供了 merge() 函数用于 DataFrame 的连接。

连接操作一般是根据键进行的,键是两个数据表格共有的列。按键连接根据键的多少 可以分为单键连接和多键连接。连接操作与 SQL 操作极为相似。

无论多少个键的连接、不同类型的连接都是使用 merge() 函数,只是参数设定不同。 merge() 函数的参数列表显示如下:

pd.merge(
    left,
    right,
    how='inner',
    on=None,
    left_on=None,
    right_on=None,
    left_index=False,
    right_index=False,
    sort=False,
    suffixes=('_x', '_y'),
    copy=True,
    indicator=False,
    validate=None,
)

我们接着构建用于连接的两个数据框,一个 DataFrame 存储故事的 id 的名字, 一个 DataFrame 存储故事所属的 subject 和故事评分。

In [155]: stories = pd.DataFrame({'story_id':[1,2,3], 'title':['lions', 'tigers', 'bears']})

In [156]: data = pd.DataFrame({'subject':[1,2,1,2], 'story_id':[1,2,5,6], 'rating':[6.7, 7.8, 3.2, 9.0]})

In [157]: stories
Out[157]:
   story_id   title
0         1   lions
1         2  tigers
2         3   bears

In [158]: data
Out[158]:
   subject  story_id  rating
0        1         1     6.7
1        2         2     7.8
2        1         5     3.2
3        2         6     9.0

接下来根据不同的需求分别介绍连接操作。

左连接

连接的 DataFrame 根据参数的顺序按 left 和 right 区分。 按左连接(left join)操作合并之后显示 left 的所有行。

连接的方式由 how 参数控制,用于连接的列名由 on 参数指定。

In [159]: pd.merge(stories, data, how='left', on='story_id')
Out[159]:
   story_id   title  subject  rating
0         1   lions      1.0     6.7
1         2  tigers      2.0     7.8
2         3   bears      NaN     NaN

连接后不存在的数值将以 NaN 填充。

右连接

在理解左连接后,理解其他的操作就比较简单了。 右连接(right join)操作合并之后显示 right 的所有行。 其实这与对调输入的两个 DataFrame 的左连接结果一致。

In [160]: pd.merge(stories, data, how='right', on='story_id')
Out[160]:
   story_id   title  subject  rating
0         1   lions        1     6.7
1         2  tigers        2     7.8
2         5     NaN        1     3.2
3         6     NaN        2     9.0

In [161]: pd.merge(data, stories, how='left', on='story_id')
Out[161]:
   subject  story_id  rating   title
0        1         1     6.7   lions
1        2         2     7.8  tigers
2        1         5     3.2     NaN
3        2         6     9.0     NaN

虽然结果一致,但结果显示两种操作的列名顺序有些不同。

外连接

外连接(outer join)操作也可以看作取并集,它会合并 left 和 right 所有的行。

In [162]: pd.merge(stories, data, how='outer', on='story_id')
Out[162]:
   story_id   title  subject  rating
0         1   lions      1.0     6.7
1         2  tigers      2.0     7.8
2         3   bears      NaN     NaN
3         5     NaN      1.0     3.2
4         6     NaN      2.0     9.0

内连接

内连接(inner join)操作也可以看作取交集,它会合并 left 和 right 共有的行。

In [163]: pd.merge(stories, data, how='inner', on='story_id')
Out[163]:
   story_id   title  subject  rating
0         1   lions        1     6.7
1         2  tigers        2     7.8

上述所说的共有是指用于连接的键的共有值,如 stories 和 data 的 story_id 共有 的值是 1 和 2。

多键连接

多键连接难度也不大,以列表形式指定 on 参数为两个 DataFrame 共有的列名即可。

In [168]: data2 = pd.merge(stories, data, how='inner', on='story_id')

In [169]: data
Out[169]:
   subject  story_id  rating
0        1         1     6.7
1        2         2     7.8
2        1         5     3.2
3        2         6     9.0

In [170]: pd.merge(data2, data, how='inner', on=['story_id', 'subject'])
Out[170]:
   story_id   title  subject  rating_x  rating_y
0         1   lions        1       6.7       6.7
1         2  tigers        2       7.8       7.8

如果两个 DataFrame 除了作为键的列之外还有同名列,合并后会被自动 添加 x 和 y 后缀以示区别。

12.3.3 级联

除了通过键将 DataFrame 以列的形式连接到一起,还有级联的方式可以合并 DataFrame。 级联操作使用 concat() 函数实现,它可以将多个 DataFrame 按行(默认)或按列组合。

In [171]: data = pd.DataFrame({'subject':[1,2,1,2], 'story_id':[1,2,5,6], 'rating':[6.7, 7.8, 3.2, 9.0]})
In [172]: data2 = pd.DataFrame({'subject':[1,2], 'story_id':[3, 4], 'rating':[5, 9.7]})

In [173]: data
Out[173]:
   subject  story_id  rating
0        1         1     6.7
1        2         2     7.8
2        1         5     3.2
3        2         6     9.0

In [174]: data2
Out[174]:
   subject  story_id  rating
0        1         3     5.0
1        2         4     9.7

上述代码生成了两个列名一致的 DataFrame,接下来我们将它们按行组合起来。

In [175]: pd.concat([data, data2])
Out[175]:
   subject  story_id  rating
0        1         1     6.7
1        2         2     7.8
2        1         5     3.2
3        2         6     9.0
0        1         3     5.0
1        2         4     9.7

怎么样,简单吧?

有时我们可能想要标定行的数据来源,这可以使用键实现。

In [176]: pd.concat([data, data2], keys=['data', 'data2'])
Out[176]:
         subject  story_id  rating
data  0        1         1     6.7
      1        2         2     7.8
      2        1         5     3.2
      3        2         6     9.0
data2 0        1         3     5.0
      1        2         4     9.7

仔细观察读者不难发现 index 中的 0 和 1 重复了,指定 ignore_index 选项 可以变成连续的 index。不过此操作后 keys 的设定将不起作用了。

In [177]: pd.concat([data, data2], keys=['data', 'data2'], ignore_index=True)
Out[177]:
   subject  story_id  rating
0        1         1     6.7
1        2         2     7.8
2        1         5     3.2
3        2         6     9.0
4        1         3     5.0
5        2         4     9.7

In [178]: pd.concat([data, data2],  ignore_index=True)
Out[178]:
   subject  story_id  rating
0        1         1     6.7
1        2         2     7.8
2        1         5     3.2
3        2         6     9.0
4        1         3     5.0
5        2         4     9.7

我们再试试按列合并:

In [180]: pd.concat([data, data2],  axis=1)
Out[180]:
   subject  story_id  rating  subject  story_id  rating
0        1         1     6.7      1.0       3.0     5.0
1        2         2     7.8      2.0       4.0     9.7
2        1         5     3.2      NaN       NaN     NaN
3        2         6     9.0      NaN       NaN     NaN

缺少的行会使用 NaN 自动填充。指定 ignore_index 后,所有列索引将重新生成。

In [181]: pd.concat([data, data2],  ignore_index=True, axis=1)
Out[181]:
   0  1    2    3    4    5
0  1  1  6.7  1.0  3.0  5.0
1  2  2  7.8  2.0  4.0  9.7
2  1  5  3.2  NaN  NaN  NaN
3  2  6  9.0  NaN  NaN  NaN

除了 concat() 函数,append() 方法也可以用于行的合并。

In [182]: data.append(data2)
Out[182]:
   subject  story_id  rating
0        1         1     6.7
1        2         2     7.8
2        1         5     3.2
3        2         6     9.0
0        1         3     5.0
1        2         4     9.7

不过它的最大用处在于添加新的行,如给 DataFrame 添加 Series 对象。

In [185]: data.append(pd.Series({'subject':1, 'story_id':10, 'rating':7}, name=6))
Out[185]:
   subject  story_id  rating
0        1         1     6.7
1        2         2     7.8
2        1         5     3.2
3        2         6     9.0
6        1        10     7.0

12.4 Pandas 可视化

Pandas 为 Series 和 DataFrame 对象提供了 Matplotlib 库 plot() 函数的简单包装实现, 本节进行简单的介绍。

我们先导入示例数据集 mtcars,它是美国 Motor Trend 收集 的 1973 到 1974 年期间总共 32 辆汽车的 11 个指标, 包含油耗、设计、性能等方面。

In [187]: mtcars = pd.read_csv('files/chapter10/mtcars.csv')
In [188]: mtcars.describe()
Out[188]:
             mpg        cyl        disp  ...         am       gear     carb
count  32.000000  32.000000   32.000000  ...  32.000000  32.000000  32.0000
mean   20.090625   6.187500  230.721875  ...   0.406250   3.687500   2.8125
std     6.026948   1.785922  123.938694  ...   0.498991   0.737804   1.6152
min    10.400000   4.000000   71.100000  ...   0.000000   3.000000   1.0000
25%    15.425000   4.000000  120.825000  ...   0.000000   3.000000   2.0000
50%    19.200000   6.000000  196.300000  ...   0.000000   4.000000   2.0000
75%    22.800000   8.000000  326.000000  ...   1.000000   4.000000   4.0000
max    33.900000   8.000000  472.000000  ...   1.000000   5.000000   8.0000

[8 rows x 11 columns]

In [189]: mtcars.shape
Out[189]: (32, 11)

mtcars 所有列中 mpg 是每百公里油耗,cyl 是发动机汽缸数。下面我们就使用这两列进行可视化分析。

In [193]: df = mtcars.loc[:, ['cyl', 'mpg']]
In [194]: df.head()
Out[194]:
   cyl   mpg
0    6  21.0
1    6  21.0
2    4  22.8
3    6  21.4
4    8  18.7
In [195]: %matplotlib inline
In [196]: df.plot()

图12-2 使用 plot 方法自动生成线图

默认情况下,plot() 方法使用线图形式进行绘制。如果想要绘制其他的图形类型,我们可以使用关键字参数 kind 进行指定。

  • bar - 条形图
  • barh - 横条形图
  • hist - 直方图
  • box - 箱线图
  • area - 面积图
  • scatter 散点图

12.4.1 条形图

条形图利用条形的高度来表示数值,此处我们有 32 辆汽车的数据,所以会有 32 组条形。

df.plot(kind='bar')

图12-3 条形图

使用 barh 来调换 x 和 y 轴。

df.plot(kind='barh')

图12-4 水平条形图

我们可能想将图形堆叠起来,可以指定 stacked 为 True。

df.plot(kind='bar', stacked=True)

图12-5 堆叠条形图

图中 x 轴显示的是数值,没有特别的含义,我们给数据加上标签,让它显示出来。

df2 = df.copy()
df2.index = ['car '+str(i) for i in np.arange(32) + 1]
df2.plot(kind='bar', stacked=True)

12.4.2 直方图

直方图可以比较直观展示数据分布,是初步了解数据的最好方式之一。

df.plot(kind='hist')

图12-6 直方图

通过指定 bins 选项,我们可以修改 bin 的宽度。

df.plot(kind='hist', bins=20)

图12-7 直方图,设置条形数量

上面将两个变量的分布绘制在了一个图中,我们还可以直接调用 hist() 方法为每一个变量单独绘制直方图。

df.hist(bins=20)

图12-8 分组直方图

12.4.3 箱线图

上面的直方图显示了气缸数是 3 个离散值 4、6、8。我们可能更加想要比较不同组别之间油耗的差异,这种情况下箱线图是很好的展示方式。

不过 plot() 方法绘制箱线图时默认是为每列单独绘制,无法进行分组。

df.plot(kind='box')

图12-9 箱线图

为了将油耗按照气缸数分组后绘制箱线图,可以直接使用 boxplot() 方法,它支持更多的绘图参数。例如,通过 by 参数指定进行分组的列名。

# 为了优化显示效果,我们进行了 3 项自定义:
# 去掉网格线
# 旋转 x 轴标签
# 增大字体
df.boxplot(by='cyl', grid=False, rot=45, fontsize=15)

图12-10 分组箱线图

12.4.4 面积图

面积图的效果有点类似堆叠条形图,前者更适用于连续变量,后者更适用于离散变量。

df.plot(kind='area')

图12-11 面积图

12.4.5 散点图

散点图可以更加直观地观测单个数据点的情况。这里变量 cyl 是离散值,所以下面绘制的图形可能看起来有点奇怪。不过,有时候这种表现形式可能带来意想不到的效果。

df.plot(kind='scatter', x='cyl', y='mpg')

图12-12 散点图

12.4.6 饼图

饼图可以显示数据的占比情况,一般是使用一列的数据,即一个 Series 对象。下面代码提取了 cyl 变量并进行了可视化。

df.cyl.plot(kind='pie')

图12-13 饼图

当需要可视化多列,即一个 DataFrame 对象时,我们需要指定 subplots 为 True。

下面代码提取了 df 前 5 行的数据用于可视化。

df.head(5).plot(kind='pie', subplots=True)

图12-14 饼图(2)

12.5 章末小结

Pandas 库作为 Python 进行数据分析的灵魂工具,提供了一系列数据类型、结构、函数、方法 和可视化特性。本章首先在第 8 章的基础上进行了总结回顾,并对其他一些数据类型进行了拓展。 然后本章对函数应用和 DataFrame 的迭代计算进行了比较详细的介绍,包括按行和按列等不同的 方式。分组计算是非常好的技术,推荐读者多多练习和使用。后面介绍的缺失值内容读者按照自己的 需求进行掌握,一般情况下通过 dropna() 去掉缺失值数据即可。DataFrame 的连接和级联合并是 非常核心的知识,它与数据库操作非常类似,掌握它们会帮助读者更好地理解和操作数据。可视化 是查看数据分布、进行比较的绝佳方案,Pandas 库基于 Matplotlib 库封装的绘图函数简单易用。 虽然本书使用了两个章节的内容介绍 Pandas 库,但仍有许多未能涵盖的知识点,读者不妨参阅 Pandas 文档和相关技术书籍进行进一步学习。