Pandas-秘籍-四-

71 阅读1小时+

Pandas 秘籍(四)

原文:Pandas Cookbook

协议:CC BY-NC-SA 4.0

九、组合 Pandas 对象

在本章中,我们将介绍以下主题:

  • 将新行追加到数据帧
  • 将多个数据帧连接在一起
  • 比较特朗普总统和奥巴马总统的支持率
  • 了解concatjoinmerge之间的区别
  • 连接到 SQL 数据库

介绍

可以使用多种选项将两个或多个数据帧或序列组合在一起。append方法最不灵活,仅允许将新行附加到数据帧。concat方法非常通用,可以在任一轴上组合任意数量的数据帧或序列。join方法通过将一个数据帧的列与其他数据帧的索引对齐来提供快速查找。merge方法提供了类似 SQL 的功能,可以将两个数据帧结合在一起。

将新行追加到数据帧

在执行数据分析时,创建新列比创建新行更为常见。 这是因为新的数据行通常代表新的观察结果,而作为分析人员,连续捕获新数据通常不是您的工作。 数据捕获通常留给其他平台,如关系数据库管理系统。 但是,这是一个必不可少的功能,因为它会不时出现。

准备

在本秘籍中,我们将首先使用.loc索引器将行追加到小型数据集,然后过渡到使用append方法。

操作步骤

  1. 读入名称数据集,并将其输出:
>>> names = pd.read_csv('data/names.csv')
>>> names

  1. 让我们创建一个包含一些新数据的列表,并使用.loc索引器设置一个等于该新数据的行标签:
>>> new_data_list = ['Aria', 1]
>>> names.loc[4] = new_data_list
>>> names

  1. .loc索引器使用标签来引用行。 在这种情况下,行标签与整数位置完全匹配。 可以使用非整数标签附加更多行:
>>> names.loc['five'] = ['Zach', 3]
>>> names

  1. 为了更明确地将变量与值相关联,可以使用字典。 同样,在这一步中,我们可以动态选择新的索引标签作为数据帧的长度:
>>> names.loc[len(names)] = {'Name':'Zayd', 'Age':2}
>>> names

  1. 序列还可以保存新数据,并且与字典完全相同:
>>> names.loc[len(names)] = pd.Series({'Age':32,
                                       'Name':'Dean'})
>>> names

  1. 前面的操作全部使用.loc索引运算符就地更改names数据帧。 没有返回的数据帧的单独副本。 在接下来的几个步骤中,我们将研究append方法,该方法不会修改调用数据帧的方法。 而是返回带有附加行的数据帧的新副本。 让我们从原始的names数据帧开始,并尝试追加一行。append的第一个参数必须是另一个数据帧,序列,字典或它们的列表,但不能是步骤 2 中的列表。让我们看看当尝试将字典与append一起使用时会发生什么:
>>> names = pd.read_csv('data/names.csv')
>>> names.append({'Name':'Aria', 'Age':1})
TypeError: Can only append a Series if ignore_index=True or if the Series has a name
  1. 此错误消息似乎有点不正确。 我们正在传递一个数据帧而不是一个序列,但是它为我们提供了如何更正它的说明:
>>> names.append({'Name':'Aria', 'Age':1}, ignore_index=True)

  1. 这有效,但是ignore_index是一个偷偷摸摸的参数。 当设置为True时,旧索引将被完全删除并替换为 0 至n-1之间的RangeIndex。 例如,让我们为names数据帧指定一个索引:
>>> names.index = ['Canada', 'Canada', 'USA', 'USA']
>>> names

  1. 重新运行步骤 7 中的代码,您将获得相同的结果。 原始索引被完全忽略。
  2. 让我们继续使用在索引中包含这些国家/地区字符串的names数据集,并通过append方法使用具有name属性的序列:
>>> s = pd.Series({'Name': 'Zach', 'Age': 3}, name=len(names))
>>> s
Age        3
Name    Zach
Name: 4, dtype: object

>>> names.append(s)

  1. append方法比.loc索引器更灵活。 它支持同时添加多行。 实现此目的的一种方法是使用序列的列表:
>>> s1 = pd.Series({'Name': 'Zach', 'Age': 3}, name=len(names))
>>> s2 = pd.Series({'Name': 'Zayd', 'Age': 2}, name='USA')
>>> names.append([s1, s2])

  1. 仅具有两列的小型数据帧非常简单,可以手动写出所有列名称和值。 当它们变大时,此过程将非常痛苦。 例如,让我们看一下 2016 年棒球数据集:
>>> bball_16 = pd.read_csv('data/baseball16.csv')
>>> bball_16.head()

  1. 该数据集包含 22 列,如果您手动输入新的数据行,则很容易输错列名称或完全忘记其中的一个。 为了帮助避免这些错误,让我们选择一行作为序列,并将to_dict方法链接到该行,以获取示例行作为字典:
>>> data_dict = bball_16.iloc[0].to_dict()
>>> print(data_dict)
{'playerID': 'altuvjo01', 'yearID': 2016, 'stint': 1, 'teamID': 'HOU', 'lgID': 'AL', 'G': 161, 'AB': 640, 'R': 108, 'H': 216, '2B': 42, '3B': 5, 'HR': 24, 'RBI': 96.0, 'SB': 30.0, 'CS': 10.0, 'BB': 60, 'SO': 70.0, 'IBB': 11.0, 'HBP': 7.0, 'SH': 3.0, 'SF': 7.0, 'GIDP': 15.0}
  1. 用字典理解清除旧值,将任何先前的字符串值分配为空字符串,将所有其他字符串值分配为缺失值。 现在,该词典可以用作您要输入的任何新数据的模板:
>>> new_data_dict = {k: '' if isinstance(v, str) else 
                        np.nan for k, v in data_dict.items()}
>>> print(new_data_dict)
{'playerID': '', 'yearID': nan, 'stint': nan, 'teamID': '', 'lgID': '', 'G': nan, 'AB': nan, 'R': nan, 'H': nan, '2B': nan, '3B': nan, 'HR': nan, 'RBI': nan, 'SB': nan, 'CS': nan, 'BB': nan, 'SO': nan, 'IBB': nan, 'HBP': nan, 'SH': nan, 'SF': nan, 'GIDP': nan}

工作原理

.loc索引运算符用于根据行和列标签选择和分配数据。 传递给它的第一个值表示行标签。 在步骤 2 中,names.loc[4]引用带有等于整数 4 的标签的行。此标签当前在数据帧中不存在。 赋值语句使用列表提供的数据创建新行。 如秘籍中所述,此操作将修改names数据帧本身。 如果以前存在标签等于整数 4 的行,则该命令将覆盖该行。 与append方法相比,就地进行此修改使此索引运算符的使用风险更高,该方法从未修改原始调用数据帧。

任何有效的标签都可以与.loc索引运算符一起使用,如步骤 3 所示。不管实际的新标签值是多少,新行始终将附加在最后。 即使使用列表分配也可以,但为清楚起见,最好使用字典,以便我们准确地知道与每个值关联的列,如步骤 4 所示。

步骤 5 显示了一个小技巧,可以动态地将新标签设置为数据帧中的当前行数。 只要索引标签与列名匹配,存储在序列中的数据也将得到正确分配。

其余步骤使用append方法,这是一种仅将新行追加到数据帧的简单方法。 大多数数据帧方法都允许通过axis参数进行行和列操作。append是一个例外,它只能将行追加到数据帧。

如步骤 6 中的错误消息所示,使用映射到值的列名字典不足以进行追加操作,如步骤 6 中的错误消息所示。要正确地追加没有行名的字典,您必须将ignore_index参数设置为True。 步骤 10 向您展示如何通过简单地将字典转换为序列来保持旧索引。 确保使用name参数,该参数随后将用作新的索引标签。 通过将序列列表作为第一个参数传递,可以用append方法添加任意数量的行。

当想要以更大的数据帧以这种方式附加行时,可以通过使用to_dict方法将单行转换为字典,然后使用字典推导式和一些默认值来清除所有旧值,从而避免大量键入和错误。

更多

将单行添加到数据帧是相当昂贵的操作,如果您发现自己编写了将单行数据附加到数据帧的循环,那么您做错了。 让我们首先创建 1,000 行新数据作为序列列表:

>>> random_data = []
>>> for i in range(1000):
        d = dict()
        for k, v in data_dict.items():
            if isinstance(v, str):
                d[k] = np.random.choice(list('abcde'))
            else:
                d[k] = np.random.randint(10)
        random_data.append(pd.Series(d, name=i + len(bball_16)))

>>> random_data[0].head()
2B    3
3B    9
AB    3
BB    9
CS    4
Name: 16, dtype: object

让我们花时间遍历每个项目一次添加一个附件需要花费多长时间:

>>> %%timeit
>>> bball_16_copy = bball_16.copy()
>>> for row in random_data:
        bball_16_copy = bball_16_copy.append(row)
4.88 s ± 190 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

仅花了 1,000 排就花了将近五秒钟。 如果我们改为通过整个序列列表,则速度会大大提高:

>>> %%timeit
>>> bball_16_copy = bball_16.copy()
>>> bball_16_copy = bball_16_copy.append(random_data)
78.4 ms ± 6.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

通过传递序列列表,时间已减少到十分之一秒以下。 在内部,pandas 将序列列表转换为单个数据帧,然后进行追加。

将多个数据帧连接在一起

通用的concat函数可将两个或多个数据帧(或序列)垂直和水平连接在一起。 通常,当同时处理多个 Pandas 对象时,连接并不是偶然发生的,而是通过它们的索引对齐每个对象。

准备

在此秘籍中,我们将水平和垂直方向的数据帧与concat函数结合在一起,然后更改参数值以产生不同的结果。

操作步骤

  1. 读取 2016 年和 2017 年的股票数据集,并将其股票代码作为索引:
>>> stocks_2016 = pd.read_csv('data/stocks_2016.csv', 
                              index_col='Symbol')
>>> stocks_2017 = pd.read_csv('data/stocks_2017.csv',
                              index_col='Symbol')

    

  1. 将所有stock数据集放在一个列表中,然后调用concat函数将它们连接在一起:
>>> s_list = [stocks_2016, stocks_2017]
>>> pd.concat(s_list)

  1. 默认情况下,concat函数垂直连接数据帧,一个接一个。 前面的数据帧的一个问题是无法识别每一行的年份。concat函数允许使用keys参数标记每个结果数据帧。 该标签将显示在级联框架的最外层索引级别中,并强制创建多重索引。 同样,为了清楚起见,names参数还可以重命名每个索引级别:
>>> pd.concat(s_list, keys=['2016', '2017'], 
              names=['Year', 'Symbol'])

  1. 也可以通过将axis参数更改为column或 1 来水平连接:
>>> pd.concat(s_list, keys=['2016', '2017'],
              axis='columns', names=['Year', None])

  1. 请注意,当一年中存在股票代号而另一年不存在时,会出现缺失值。 默认情况下,concat函数使用外连接,将列表中每个数据帧的所有行保留在列表中。 但是,它为我们提供了仅在两个数据帧中保留具有相同索引值的行的选项。 这称为内连接。 我们将join参数设置为inner,以更改行为:
>>> pd.concat(s_list, join='inner', keys=['2016', '2017'],
              axis='columns', names=['Year', None])

工作原理

第一个参数是concat函数所需的唯一参数,它必须是 Pandas 对象的列表,通常是数据帧或序列的列表或字典。 默认情况下,所有这些对象将垂直堆叠在另一个之上。 在此秘籍中,仅连接了两个数据帧,但是任何数量的 Pandas 对象都可以工作。 当我们垂直连接时,数据帧通过其列名称对齐。

在此数据集中,所有列名称均相同,因此 2017 年数据中的每个列均在 2016 年数据中的同一列名称下精确对齐。 但是,如步骤 4 所示,将它们水平连接时,只有两个年份的索引标签相匹配 - AAPLTSLA。 因此,这些股票代号在任何一年中都没有缺失值。 可以使用concat进行两种对齐方式,join参数引用的outer(默认)和inner

更多

append方法是concat的精简版本,只能将新行附加到数据帧。 在内部,append仅调用concat函数。 例如,此秘籍中的第 2 步可以复制以下内容:

>>> stocks_2016.append(stocks_2017)

比较特朗普总统和奥巴马总统的支持率

现任美国总统的公众支持是一个经常成为新闻头条的话题,并通过民意测验进行正式衡量。 近年来,这些民意调查的频率迅速增加,并且每周都有大量新的数据发布。 有许多不同的民意测验者都有各自的问题和方法来捕获其数据,因此,数据之间存在相当多的可变性。 来自加利福尼亚大学圣塔芭芭拉分校的美国总统职位项目每天提供的总批准评级低至单个数据点。

与本书中的大多数秘籍不同,该数据在 CSV 文件中不易获得。 通常,作为数据分析师,您将需要在 Web 上查找数据,并使用可以将其抓取为可通过本地工作站解析的格式的工具。

准备

在本秘籍中,我们将使用read_html函数,该函数功能强大,可以在线从表中抓取数据并将其转换为数据帧。 您还将学习如何检查网页以查找某些元素的基础 HTML。 我使用 Google Chrome 浏览器作为浏览器,建议您将其或 Firefox 用于基于 Web 的步骤。

操作步骤

  1. 导航至唐纳德·特朗普总统的美国总统职位批准页。 您应该获得一个包含时间序列图的页面,该页面紧随其后的是表格中的数据:

  1. read_html函数能够从网页上抓取表格并将其数据放入数据帧中。 它最适合简单的 HTML 表,并提供一些有用的参数来选择所需的确切表,以防同一页上有多个表。 让我们继续使用read_html作为其默认值,它将以列表形式将所有表作为数据帧返回:
>>> base_url = 'http://www.presidency.ucsb.edu/data/popularity.php?pres={}'
>>> trump_url = base_url.format(45)
>>> df_list = pd.read_html(trump_url)
>>> len(df_list)
14
  1. 该函数返回了 14 个表,乍一看似乎很荒谬,因为该网页似乎只显示了大多数人会识别为表的单个元素。read_html函数正式搜索以<table开头的 HTML 表元素。 我们通过右键单击批准数据表并选择查看或查看元素来检查 HTML 页面:

  1. 这将打开控制台,这是用于 Web 开发的非常强大的工具。 对于本秘籍,我们仅需要几个任务即可使用。 所有控制台都允许您在 HTML 中搜索特定的单词。 让我们搜索单词table。 我的浏览器找到 15 个不同的 HTML 表格,非常接近read_html返回的数字:

  1. 让我们开始检查df_list中的数据帧:
>>> df0 = df_list[0]
>>> df0.shape
(308, 1794)

>>> df0.head(7)

  1. 回顾网页,从 2017 年 1 月 22 日开始直到批准数据收集的那一天(即 2017 年 9 月 25 日),几乎每天都有批准表存在。这是八个多月或 250 行数据,该数据在某种程度上接近第一个表中的 308 行。 扫描其余的表,您会发现发现了许多空的,毫无意义的表,以及网页中实际上与表不相似的不同部分的表。 让我们使用read_html函数的一些参数来帮助我们选择所需的表。 我们可以使用match参数在表中搜索特定的字符串。 让我们搜索其中带有单词Start Date的表:
>>> df_list = pd.read_html(trump_url, match='Start Date')
>>> len(df_list)
3
  1. 通过在表中搜索特定的字符串,我们将表的数量减少到只有三个。 另一个有用的参数是attrs,它接受 HTML 属性及其值配对的字典。 我们想为我们的特定表找到一些独特的属性。 为此,让我们再次在数据表中单击鼠标右键。 这次,请确保单击表格标题之一的最上方。 例如,右键单击President,然后再次选择检查或检查元素:

  1. 您选择的元素应突出显示。 实际上,这不是我们感兴趣的元素。继续查看,直到遇到以<table开头的 HTML 标签。 等号左边的所有单词都是属性或attrs,右边的是值。 让我们在搜索中使用align属性及其值center
>>> df_list = pd.read_html(trump_url, match='Start Date',
                           attrs={'align':'center'})
>>> len(df_list)
1

>>> trump = df_list[0]
>>> trump.shape
(249, 19)

>>> trump.head(8)

  1. 我们仅与一个表匹配,并且行数非常接近起始日期和最后日期之间的总天数。 查看数据,似乎我们确实找到了要查找的表。 六个列的名称似乎在第 4 行。我们可以走得更远,更精确地选择要跳过的行以及要使用skiprowsheader参数的列名称。 我们还可以使用parse_dates参数确保将开始日期和结束日期正确地强制为正确的数据类型:
>>> df_list = pd.read_html(trump_url, match='Start Date',
                           attrs={'align':'center'}, 
                           header=0, skiprows=[0,1,2,3,5], 
                           parse_dates=['Start Date',
                                        'End Date'])
>>> trump = df_list[0]
>>> trump.head()

  1. 这几乎是我们想要的,除了缺少值的列。 让我们使用dropna方法删除缺少所有值的列:
>>> trump = trump.dropna(axis=1, how='all')
>>> trump.head()

  1. 让我们用ffill方法向前填充President列中的缺失值。 首先让我们检查其他列中是否缺少任何值:
>>> trump.isnull().sum()
President         242
Start Date          0
End Date            0
Approving           0
Disapproving        0
unsure/no data      0
dtype: int64

>>> trump = trump.ffill()
trump.head()

  1. 最后,检查数据类型以确保它们正确是很重要的:
>>> trump.dtypes
President                 object
Start Date        datetime64[ns]
End Date          datetime64[ns]
Approving                  int64
Disapproving               int64
unsure/no data             int64
dtype: object
  1. 让我们构建一个将所有步骤组合在一起的函数,以自动化检索任何总裁的批准数据的过程:
>>> def get_pres_appr(pres_num):
        base_url = 'http://www.presidency.ucsb.edu/data/popularity.php?pres={}'
        pres_url = base_url.format(pres_num)
        df_list = pd.read_html(pres_url, match='Start Date',
                               attrs={'align':'center'}, 
                               header=0, skiprows=[0,1,2,3,5], 
                               parse_dates=['Start Date',
                                            'End Date'])
        pres = df_list[0].copy()
        pres = pres.dropna(axis=1, how='all')
        pres['President'] = pres['President'].ffill()
        return pres.sort_values('End Date') \
                   .reset_index(drop=True)
  1. 唯一的参数pres_num表示每个总统的批准号。 巴拉克·奥巴马是美国第 44 任总统; 将 44 传递给get_pres_appr函数以获取其批准号:
>>> obama = get_pres_appr(44)
>>> obama.head()

  1. 在总统富兰克林·罗斯福第三任期期间,有总统支持率的评级数据可追溯到 1941 年。 通过我们的自定义函数以及concat函数,可以从该站点获取所有总统批准评级数据。 现在,让我们获取最后五位总统的支持率数据,并输出每位总统的前三行:
>>> pres_41_45 = pd.concat([get_pres_appr(x) for x in range(41,46)],
                            ignore_index=True)
>>> pres_41_45.groupby('President').head(3)

  1. 在继续之前,让我们确定是否有多个批准评级的日期:
>>> pres_41_45['End Date'].value_counts().head(8)
1990-08-26    2
1990-03-11    2
1999-02-09    2
2013-10-10    2
1990-08-12    2
1992-11-22    2
1990-05-22    2
1991-09-30    1
Name: End Date, dtype: int64
  1. 只有几天有重复的值。 为了简化分析,让我们仅保留重复日期存在的第一行:
>>> pres_41_45 = pres_41_45.drop_duplicates(subset='End Date')
  1. 让我们获得一些关于数据的摘要统计信息:
>>> pres_41_45.shape
(3679, 6)

>>> pres_41_45['President'].value_counts()
Barack Obama          2786
George W. Bush         270
Donald J. Trump        243
William J. Clinton     227
George Bush            153
Name: President, dtype: int64

>>> pres_41_45.groupby('President', sort=False) \
                       .median().round(1)

  1. 让我们在同一张图表上绘制每个总裁的支持率。 为此,我们将按每位总裁分组,遍历每组,并分别绘制每个日期的批准等级:
>>> from matplotlib import cm
>>> fig, ax = plt.subplots(figsize=(16,6))

>>> styles = ['-.', '-', ':', '-', ':']
>>> colors = [.9, .3, .7, .3, .9]
>>> groups = pres_41_45.groupby('President', sort=False)

>>> for style, color, (pres, df) in zip(styles, colors, groups):
        df.plot('End Date', 'Approving', ax=ax,
                label=pres, style=style, color=cm.Greys(color), 
                title='Presedential Approval Rating')

  1. 此图表将所有总统依次排列。 通过将批准等级与在职天数作图,我们可以更简单地比较它们。 让我们创建一个新变量来代表上班天数:
>>> days_func = lambda x: x - x.iloc[0]
>>> pres_41_45['Days in Office'] = pres_41_45.groupby('President') \
                                             ['End Date'] \
                                             .transform(days_func)
>>> pres_41_45.groupby('President').head(3)

  1. 自总统任期以来,我们已经成功地为每一行分配了相对天数。 有趣的是,新列Days in Office具有其值的字符串表示形式。 让我们检查其数据类型:
>>> pres_41_45.dtypes
...
Days in Office    timedelta64[ns]
dtype: object
  1. Days in Office列是具有纳秒精度的timedelta64对象。 这比所需的精度要高得多。 让我们通过仅获取日期将数据类型更改为整数:
>>> pres_41_45['Days in Office'] = pres_41_45['Days in Office'] \
                                             .dt.days
>>> pres_41_45['Days in Office'].head()
0     0
1    32
2    35
3    43
4    46
Name: Days in Office, dtype: int64
  1. 我们可以按照与步骤 19 中相似的方式来绘制此数据,但是存在一种完全不涉及任何循环的方法。 默认情况下,在数据帧上调用plot方法时,pandas 尝试将数据的每一列绘制为线图,并使用索引作为 x 轴。 知道了这一点之后,我们就来讨论一下数据,以便每位总裁都拥有自己的专栏以进行审批:
>>> pres_pivot = pres_41_45.pivot(index='Days in Office',
                                  columns='President',
                                  values='Approving')
>>> pres_pivot.head()

  1. 现在,每个总裁都有自己的批准等级列,我们可以直接对每个列进行绘制而无需分组。 为了减少剧情中的混乱情况,我们将仅绘制巴拉克·奥巴马和唐纳德·特朗普:
>>> plot_kwargs = dict(figsize=(16,6), color=cm.gray([.3, .7]), 
                       style=['-', '--'], title='Approval Rating')
>>> pres_pivot.loc[:250, ['Donald J. Trump', 'Barack Obama']] \
              .ffill().plot(**plot_kwargs)

工作原理

通常在到达所需的一个或多个表之前多次调用read_html。 您可以使用两个主要参数来指定表matchattrs。 提供给match的字符串用于查找表中实际文本的精确匹配。 这是将显示在网页本身上的文本。 另一方面,attrs参数搜索在表标记<table开始之后直接找到的 HTML 表属性。 要查看更多表格属性,请访问 W3Schools

在步骤 8 中找到表格后,我们仍然可以利用其他一些参数来简化操作。 HTML 表通常不会直接转换为漂亮的数据帧。 通常缺少列名,多余的行和未对齐的数据。 在此秘籍中,skiprows传递了行号列表,以便在读取文件时跳过。 它们对应于步骤 8 的数据帧输出中缺少值的行。header参数还用于指定列名称的位置。 请注意,header等于零,乍一看似乎是错误的。 每当header参数与skiprows结合使用时,将首先跳过各行,从而为每行产生一个新的整数标签。 正确的列名称位于第 4 行中,但是当我们跳过第 0 至 3 行时,其新的整数标签为 0。

在步骤 11 中,ffill方法垂直填充所有丢失的值,并向下填充最后一个非丢失的值。 该方法只是fillna(method='ffill')的快捷方式。

第 13 步构建了一个由所有先前步骤组成的函数,可以自动获得任何总裁的批准等级,前提是您拥有批准号。 功能上有一些差异。 并非将ffill方法应用于整个数据帧,我们仅将其应用于President列。 在 Trump 的数据帧中,其他列没有丢失数据,但这不能保证所有抓取的表在其他列中都不会丢失数据。 函数的最后一行以更自然的方式对日期进行排序,以便从最旧到最新进行数据分析。 这也改变了索引的顺序,因此我们将其与reset_index丢弃,以使其再次从零开始。

步骤 16 显示了一个常见的 Pandas 习惯用法,用于在将它们与concat函数组合在一起之前,将多个类似索引的数据帧收集到一个列表中。 连接到单个数据帧后,我们应该目视检查它以确保其准确性。 一种方法是通过对数据进行分组然后在每组上使用head方法来浏览每位总裁部分的前几行。

第 18 步的汇总统计数据很有趣,因为每位继任总统的中位数批准率均低于上一任总统。 推断数据会导致天真的预测未来几位总统的支持率为负面。

步骤 19 中的绘图代码相当复杂。 您可能想知道为什么我们首先需要遍历groupby对象。 在数据帧的当前结构中,它无法基于单个列中的值绘制不同的组。 但是,第 23 步显示了如何设置数据帧,以便 Pandas 可以直接绘制每个总统的数据,而不会像这样循环。

要了解步骤 19 中的绘图代码,您必须首先意识到groupby对象是可迭代的,并且在迭代过程中会产生一个包含当前组的元组(此处仅是总统的名字)和该组的子数据帧。 该groupby对象与控制绘图的颜色和线条样式的值一起压缩。 我们从 matplotlib 导入了调色板模块cm,该模块包含数十种不同的调色板。 在 0 到 1 之间传递一个float值会从该调色板中选择一种特定的颜色,我们在plot方法中将其与color参数一起使用。 同样重要的是要注意,我们必须创建图形fig和绘图表面ax,以确保将每个批准线放置在同一图形上。 在循环的每次迭代中,我们使用具有相同名称的参数ax的相同绘图表面。

为了更好地比较总统之间的差异,我们创建了一个新列,该列等于上任天数。 我们从每个主席组的其余日期中减去第一个日期。 当减去两个datetime64列时,结果是一个timedelta64对象,该对象表示一段时间,在这种情况下为几天。 如果我们将列的精度保留为纳秒,则通过使用特殊的dt访问器返回天数,x 轴将同样显示过多的精度。

至关重要的一步出现在步骤 23 中。我们对数据进行结构设计,以使每位总裁在其批准等级上都有一个唯一的列。 Pandas 为每一列单独一行。 最后,在第 24 步中,我们使用.loc索引器同时选择前 250 天(行)以及仅特朗普和奥巴马的列。ffill方法用于少数总统在特定日期缺少值的情况。 在 Python 中,可以通过在包含字典解压缩的过程中在它们前面加上**来将包含参数名称及其值的字典传递给函数。

更多

步骤 19 中的图显示了大量噪声,如果对其进行了平滑处理,则数据可能更易于解释。 一种常见的平滑方法称为滚动平均值。 Pandas 为数据帧和groupby对象提供了rolling方法。 它通过返回一个对象以等待对其执行附加操作,从而类似于groupby方法。 创建它时,必须将窗口的大小作为第一个参数传递,它可以是整数或日期偏移量字符串。

在此示例中,我们使用日期偏移字符串90D进行 90 天移动平均。on参数指定从中计算滚动窗口的列:

>>> pres_rm = pres_41_45.groupby('President', sort=False) \
                        .rolling('90D', on='End Date')['Approving'] \
                        .mean()
>>> pres_rm.head()
President    End Date   
George Bush  1989-01-26    51.000000
             1989-02-27    55.500000
             1989-03-02    57.666667
             1989-03-10    58.750000
             1989-03-13    58.200000
Name: Approving, dtype: float64

在这里,我们可以使用unstack方法重新构造数据,使其看起来与步骤 23 的输出类似,然后进行绘制:

>>> styles = ['-.', '-', ':', '-', ':']
>>> colors = [.9, .3, .7, .3, .9]
>>> color = cm.Greys(colors)
>>> title='90 Day Approval Rating Rolling Average'
>>> plot_kwargs = dict(figsize=(16,6), style=styles,
                       color = color, title=title)
>>> correct_col_order = pres_41_45.President.unique()

>>> pres_rm.unstack('President')[correct_col_order].plot(**plot_kwargs)

另见

了解concatjoinmerge之间的区别

mergejoin数据帧(而不是序列)方法以及concat函数都提供了非常相似的功能,可以将多个 Pandas 对象组合在一起。 由于它们是如此相似,并且它们在某些情况下可以相互复制,因此何时以及如何正确使用它们会变得非常混乱。 为了帮助弄清它们之间的差异,请查看以下概述:

  • concat
    • Pandas 函数
    • 垂直或水平组合两个或多个 Pandas 对象
    • 仅在索引上对齐
    • 每当索引中出现重复项时发生错误
    • 默认为外连接,带有内连接选项
  • join
    • 数据帧方法
    • 水平组合两个或多个 Pandas 对象
    • 将调用的数据帧的列或索引与其他对象的索引(而不是列)对齐
    • 通过执行笛卡尔积来处理连接列/索引上的重复值
    • 默认为左连接,带有内,外和右选项
  • merge
    • 数据帧方法
    • 准确地水平合并两个数据帧
    • 将调用的数据帧的列/索引与其他数据帧的列/索引对齐
    • 通过执行笛卡尔积来处理连接列/索引上的重复值
    • 默认为内连接,带有左,外和右选项

join方法的第一个参数是other,它可以是单个数据帧/序列,也可以是任意数量的数据帧/序列的列表。

准备

在此秘籍中,我们将执行组合数据帧所需的。 第一种情况使用concat更简单,而第二种情况使用merge更简单。

操作步骤

  1. 让我们使用循环而不是对read_csv函数的三个不同调用将 2016 年,2017 年和 2018 年的股票数据读入数据帧的列表中。 Jupyter 笔记本当前仅允许将一个数据帧显示在一行上。 但是,有一种方法可以在IPython库的帮助下自定义 HTML 输出。 用户定义的display_frames函数接受数据帧的列表并将它们全部输出到一行:
>>> from IPython.display import display_html

>>> years = 2016, 2017, 2018
>>> stock_tables = [pd.read_csv('data/stocks_{}.csv'.format(year),
                                index_col='Symbol') 
                    for year in years]

>>> def display_frames(frames, num_spaces=0):
        t_style = '<table style="display: inline;"'
        tables_html = [df.to_html().replace('<table', t_style) 
                       for df in frames]

        space = '&nbsp;' * num_spaces
        display_html(space.join(tables_html), raw=True)

>>> display_frames(stock_tables, 30)
>>> stocks_2016, stocks_2017, stocks_2018 = stock_tables

  1. concat函数是唯一能够垂直组合数据帧的函数。 让我们通过将列表stock_tables传递给它:
>>> pd.concat(stock_tables, keys=[2016, 2017, 2018])

  1. 通过将axis参数更改为columns,它也可以水平组合数据帧:
>>> pd.concat(dict(zip(years,stock_tables)), axis='columns')

  1. 现在我们已经开始水平组合数据帧了,我们可以使用joinmerge方法来复制concat的功能。 在这里,我们使用join方法来组合stock_2016stock_2017数据帧。 默认情况下,数据帧按其索引对齐。 如果任何一列具有相同的名称,则必须为lsuffixrsuffix参数提供一个值,以在结果中区分它们:
>>> stocks_2016.join(stocks_2017, lsuffix='_2016',
                     rsuffix='_2017', how='outer')

  1. 为了精确复制步骤 3 中concat函数的输出,我们可以将数据帧的列表传递给join方法:
>>> other = [stocks_2017.add_suffix('_2017'),
             stocks_2018.add_suffix('_2018')]
>>> stocks_2016.add_suffix('_2016').join(other, how='outer')

  1. 让我们检查一下它们是否实际上完全相等:
>>> stock_join = stocks_2016.add_suffix('_2016').join(other, 
                                                      how='outer')
>>> stock_concat = pd.concat(dict(zip(years,stock_tables)),
                             axis='columns')
>>> level_1 = stock_concat.columns.get_level_values(1)
>>> level_0 = stock_concat.columns.get_level_values(0).astype(str)
>>> stock_concat.columns = level_1 + '_' + level_0
>>> stock_join.equals(stock_concat)
True
  1. 现在,让我们转向merge,与concatjoin不同,它可以将两个数据帧恰好结合在一起。 默认情况下,merge尝试对齐每个数据帧中具有相同名称的列中的值。 但是,您可以通过将布尔参数left_indexright_index设置为True来选择使其与索引对齐。 让我们将 2016 年和 2017 年的股票数据合并在一起:
>>> stocks_2016.merge(stocks_2017, left_index=True, 
                      right_index=True)

  1. 默认情况下,合并使用内连接,并自动为名称相同的列提供后缀。 让我们更改为外连接,然后执行 2018 数据的另一个外连接以完全复制concat
>>> step1 = stocks_2016.merge(stocks_2017, left_index=True, 
                              right_index=True, how='outer',
                              suffixes=('_2016', '_2017'))

>>> stock_merge = step1.merge(stocks_2018.add_suffix('_2018'), 
                              left_index=True, right_index=True,
                              how='outer')

>>> stock_concat.equals(stock_merge)
True
  1. 现在,让我们将比较转到我们希望将列的值对齐而不是索引或列标签本身对齐的数据集。merge方法正是针对这种情况而构建的。 让我们看一下两个新的小型数据集food_pricesfood_transactions
>>> names = ['prices', 'transactions']
>>> food_tables = [pd.read_csv('data/food_{}.csv'.format(name)) 
                    for name in names]
>>> food_prices, food_transactions = food_tables
>>> display_frames(food_tables, 30)

  1. 如果我们想查找每笔交易的总金额,则需要在itemstore列上加入以下表格:
>>> food_transactions.merge(food_prices, on=['item', 'store'])

  1. 现在,价格已正确与其对应的物料和商店对齐,但是存在问题。 客户 2 共有四个steak项目。 由于steak项目在每个表中针对B的存储表都出现两次,因此在它们之间会产生笛卡尔积,导致四行。 此外,请注意缺少coconut项目,因为没有相应的价格。 让我们解决这两个问题:
>>> food_transactions.merge(food_prices.query('Date == 2017'),
                            how='left')

  1. 我们可以使用join方法复制它,但是我们必须首先将food_prices数据帧的连接列放入索引中:
>>> food_prices_join = food_prices.query('Date == 2017') \
                                  .set_index(['item', 'store'])
>>> food_prices_join

  1. join方法仅与传递的数据帧的索引对齐,但可以使用调用数据帧的索引或列。 要使用列在调用数据帧上对齐,您需要将它们传递给on参数:
>>> food_transactions.join(food_prices_join, on=['item', 'store'])
  1. 输出与步骤 11 的结果完全匹配。 要使用concat方法复制此内容,您需要将该项放置并存储列到两个数据帧的索引中。 但是,在此特定情况下,由于在至少一个数据帧(具有项steak和存储B中)出现重复的索引值,将产生错误:
>>> pd.concat([food_transactions.set_index(['item', 'store']), 
               food_prices.set_index(['item', 'store'])],
              axis='columns')
Exception: cannot handle a non-unique multi-index!

工作原理

同时导入多个数据帧时,重复编写read_csv函数可能很麻烦。 自动执行此过程的一种方法是将所有文件名放在列表中,并使用for循环遍历它们。 这是在步骤 1 中通过列表理解完成的。

此步骤的其余部分将构建一个函数,以在 Jupyter 笔记本的同一行输出中显示多个数据帧。 所有数据帧都有一个to_html方法,该方法返回表的原始 HTML 字符串表示形式。 通过将display属性更改为inline,可以更改每个表的 CSS(级联样式表),以便元素在水平方向上彼此相邻而不是垂直显示。 要在笔记本中正确呈现表格,您必须使用 IPython 库提供的辅助函数read_html

在第 1 步结束时,我们将数据帧的列表解压缩为它们自己的适当命名的变量,以便可以轻松,清晰地引用每个表。 关于数据帧的列表的好处是,它是concat函数的确切要求,如步骤 2 所示。请注意,步骤 2 如何使用keys参数命名每个数据块。 也可以通过将字典传递给concat来完成,如步骤 3 所示。

在步骤 4 中,我们必须将join的类型更改为outer,以包括所传递的数据帧中所有在调用数据帧中不存在索引的行。 在步骤 5 中,传递的数据帧的列表不能有任何共同的列。 尽管有rsuffix参数,但仅在传递单个数据帧而不是它们的列表时才起作用。 为了解决此限制,我们预先使用add_suffix方法更改列的名称,然后调用join方法。

在第 7 步中,我们使用merge,默认情况下,将对齐两个数据帧中相同的所有列名称。 要更改此默认行为,并对齐一个或两个的索引,请将left_indexright_index参数设置为True。 步骤 8 通过两个合并请求完成复制。 如您所见,当在其索引上对齐多个数据帧时,concat通常比合并好得多。

在第 9 步中,我们切换档位以关注merge具有优势的情况。merge方法是唯一能够按列值对齐调用和传递的数据帧的方法。 第 10 步向您展示了合并两个数据帧有多么容易。on参数不是必需的,但为清楚起见而提供。

不幸的是,如第 10 步所示,在合并数据帧时复制或删除数据非常容易。在合并数据后花一些时间进行健全性检查至关重要。 在这种情况下,food_prices数据集在商店B中具有steak的重复价格,因此我们通过在步骤 11 中仅查询当前年份来消除该行。我们还更改为左连接,以确保每笔交易无论是否存在价格,都会保留。

在这些实例中可以使用join,但是必须首先将传递的数据帧中的所有列移入索引。 最后,每当您打算按列中的值对齐数据时,concat都不是一个好的选择。

更多

可以在不知道文件名的情况下将所有文件从特定目录读取到数据帧中。 Python 提供了几种遍历目录的方法,其中glob模块是一种流行的选择。 汽油价格目录包含五个不同的 CSV 文件,每个文件具有从 2007 年开始的特定等级汽油的每周价格。每个文件只有两列-星期几和价格。 这是一种遍历所有文件,将它们读入数据帧并将它们全部与concat函数组合在一起的理想情况。glob模块具有glob函数,该函数采用一个参数-您要作为字符串迭代的目录的位置。 要获取目录中的所有文件,请使用字符串*。 在此示例中,*.csv仅返回以.csv结尾的文件。glob函数的结果是一个字符串文件名列表,可以直接将其传递给read_csv函数:

>>> import glob

>>> df_list = []
>>> for filename in glob.glob('data/gas prices/*.csv'):
        df_list.append(pd.read_csv(filename, index_col='Week',
                       parse_dates=['Week']))

>>> gas = pd.concat(df_list, axis='columns')
>>> gas.head()

另见

连接到 SQL 数据库

要成为一名认真的数据分析师,几乎可以肯定,您必须学习一些 SQL。 世界上许多数据都存储在接受 SQL 语句的数据库中。 关系数据库管理系统有许多种,其中 SQLite 是最受欢迎和易于使用的系统之一。

准备

我们将探索 SQLite 提供的 Chinook 示例数据库,其中包含音乐商店的 11 个数据表。 首先进入适当的关系数据库时,最好的事情之一就是研究数据库图(有时称为实体关系图) ,以更好地了解表之间的关系。 下图在导航此秘籍时将非常有帮助:

为了使此秘籍生效,您将需要安装sqlalchemy Python 包。 如果您安装了 Anaconda 发行版,则应该已经可以使用它。 与数据库建立连接时,SQLAlchemy 是首选的 Pandas 工具。 在本秘籍中,您将学习如何连接到 SQLite 数据库。 然后,您将问两个不同的查询,并通过使用merge方法将表连接在一起来回答它们。

操作步骤

  1. 在开始从chinook数据库中读取表之前,我们需要设置我们的 SQLAlchemy 引擎:
>>> from sqlalchemy import create_engine
>>> engine = create_engine('sqlite:///data/chinook.db')
  1. 现在,我们可以回到 Pandas 世界,并在剩下的秘籍中呆在那里。 让我们完成一个简单的命令,并使用read_sql_table函数读取tracks表。 表的名称是第一个参数,SQLAlchemy 引擎是第二个参数:
>>> tracks = pd.read_sql_table('tracks', engine)
>>> tracks.head()

  1. 对于本秘籍的其余部分,我们将在数据库图的帮助下回答几个不同的特定查询。 首先,让我们找到每种流派的平均歌曲长度:
>>> genre_track = genres.merge(tracks[['GenreId', 'Milliseconds']], 
                               on='GenreId', how='left') \
                        .drop('GenreId', axis='columns')

>>> genre_track.head()

  1. 现在我们可以轻松找到每种流派的每首歌曲的平均长度。 为了帮助简化解释,我们将Milliseconds列转换为timedelta数据类型:
>>> genre_time = genre_track.groupby('Name')['Milliseconds'].mean()
>>> pd.to_timedelta(genre_time, unit='ms').dt.floor('s')
                                             .sort_values()
Name
Rock And Roll        00:02:14
Opera                00:02:54
Hip Hop/Rap          00:02:58
...
Drama                00:42:55
Science Fiction      00:43:45
Sci Fi & Fantasy     00:48:31
Name: Milliseconds, dtype: timedelta64[ns]
  1. 现在,让我们找出每个客户花费的总金额。 我们需要将customersinvoicesinvoice_items表都相互连接:
>>> cust = pd.read_sql_table('customers', engine, 
                             columns=['CustomerId','FirstName',
                                      'LastName'])
>>> invoice = pd.read_sql_table('invoices', engine, 
                                 columns=['InvoiceId','CustomerId'])
>>> ii = pd.read_sql_table('invoice_items', engine, 
                            columns=['InvoiceId', 'UnitPrice',
                                     'Quantity'])

>>> cust_inv = cust.merge(invoice, on='CustomerId') \
                   .merge(ii, on='InvoiceId')
>>> cust_inv.head()

  1. 现在,我们可以将数量乘以单价,然后找到每个客户花费的总金额:
>>> total = cust_inv['Quantity'] * cust_inv['UnitPrice']
>>> cols = ['CustomerId', 'FirstName', 'LastName']
>>> cust_inv.assign(Total = total).groupby(cols)['Total'] \
                                  .sum() \
                                  .sort_values(ascending=False) \
                                  .head()
CustomerId  FirstName  LastName  
6           Helena     Holý          49.62
26          Richard    Cunningham    47.62
57          Luis       Rojas         46.62
46          Hugh       O'Reilly      45.62
45          Ladislav   Kovács        45.62
Name: Total, dtype: float64

工作原理

create_engine函数需要连接字符串才能正常工作。 SQLite 的连接字符串非常简单,它只是数据库的位置,位于数据目录中。 其他关系数据库管理系统具有更复杂的连接字符串。 您将需要提供用户名,密码,主机名,端口以及(可选)数据库。 您还需要提供 SQL 方言和驱动程序。 连接字符串的一般格式如下:dialect+driver://username:password@host:port/database。 您特定的关系数据库的驱动程序可能需要单独安装。

一旦创建了引擎,就可以使用步骤 2 中的read_sql_table函数将整个表选择到数据帧中非常容易。数据库中的每个表都有一个主键,该主键唯一地标识每一行。 在图中用图形符号标识它。 在第 3 步中,我们通过GenreId将流派链接到曲目。 因为我们只关心轨道长度,所以在执行合并之前,将轨道数据帧修剪为仅需要的列。 合并表格后,我们可以使用基本的groupby操作来回答查询。

我们进一步走了一步,将整数毫秒转换为更容易阅读的时间增量对象。 键以字符串形式传入正确的度量单位。 现在我们有了时间增量序列,我们可以使用dt属性访问floor方法,该方法将时间向下舍入到最接近的秒。

回答步骤 5 所需的查询涉及三个表。 通过将表传递给columns参数,可以将表显着减少到仅需要的列。 使用merge时,具有相同名称的连接列将不保留。 在第 6 步中,我们可以为价格乘以数量分配一列,内容如下:

cust_inv['Total'] = cust_inv['Quantity'] * cust_inv['UnitPrice']

以这种方式分配列没有错。 我们选择使用assign方法动态创建新列,以允许连续的方法链。

更多

如果您精通 SQL,则可以将 SQL 查询作为字符串编写,并将其传递给read_sql_query函数。 例如,以下将重现步骤 4 的输出:

>>> sql_string1 = '''
    select 
        Name, 
        time(avg(Milliseconds) / 1000, 'unixepoch') as avg_time
    from (
            select 
                g.Name, 
                t.Milliseconds
            from 
                genres as g 
            join
                tracks as t
                on 
                    g.genreid == t.genreid
         )
    group by 
        Name
    order by 
         avg_time
'''
>>> pd.read_sql_query(sql_string1, engine)

要重现步骤 6 的答案,请使用以下 SQL 查询:

>>> sql_string2 = '''
    select 
          c.customerid, 
          c.FirstName, 
          c.LastName, 
          sum(ii.quantity * ii.unitprice) as Total
    from
         customers as c
    join
         invoices as i
              on c.customerid = i.customerid
    join
        invoice_items as ii
              on i.invoiceid = ii.invoiceid
    group by
        c.customerid, c.FirstName, c.LastName
    order by
        Total desc
'''
>>> pd.read_sql_query(sql_string2, engine)

另见

十、时间序列分析

在本章中,我们将介绍以下主题:

  • 了解 Python 和 Pandas 日期工具之间的区别
  • 智能分割时间序列
  • 使用仅适用于日期时间索引的方法
  • 计算每周的犯罪数量
  • 分别汇总每周犯罪和交通事故
  • 按工作日和年份衡量犯罪
  • 使用日期时间索引和匿名函数进行分组
  • 按时间戳和另一列分组
  • 使用merge_asof,发现上次犯罪率降低了 20%

介绍

Pandas 的根源在于分析金融时间序列数据。 作者 Wes McKinney 当时对可用的 Python 工具并不满意,因此决定在他工作的对冲基金中建立 Pandas 来满足自己的需求。 从广义上讲,时间序列只是随时间推移收集的数据点。 最典型地,时间在每个数据点之间平均间隔。 Pandas 在处理日期,在不同时间段内进行汇总,对不同时间段进行采样等方面具有出色的功能。

了解 Python 和 Pandas 日期工具之间的区别

在介绍 Pandas 之前,了解并了解 Python 核心的日期和时间功能可能会有所帮助。datetime模块提供了三种不同的数据类型,datetimedatetime。 正式而言,date是一个由年,月和日组成的时刻。 例如,2013 年 6 月 7 日为日期。time由小时,分钟,秒和微秒(百万分之一秒)组成,并且未附加到任何日期。 时间的示例是 12 小时 30 分钟。datetime由日期和时间这两个元素共同组成。

另一方面,Pandas 有一个封装日期和时间的对象,称为Timestamp。 它具有纳秒级(十亿分之一秒)的精度,并且源自 NumPy 的datetime64数据类型。 Python 和 Pandas 都具有timedelta对象,在进行日期加/减时很有用。

准备

在本秘籍中,我们将首先探索 Python 的datetime模块,然后转向 Pandas 中相应的高级日期工具。

操作步骤

  1. 首先,将datetime模块导入我们的名称空间并创建datetimedatetime对象:
>>> import datetime

>>> date = datetime.date(year=2013, month=6, day=7)
>>> time = datetime.time(hour=12, minute=30, 
                         second=19, microsecond=463198)
>>> dt = datetime.datetime(year=2013, month=6, day=7, 
                           hour=12, minute=30, second=19, 
                           microsecond=463198)

>>> print("date is ", date)
>>> print("time is", time)
>>> print("datetime is", dt)

date is 2013-06-07 
time is 12:30:19.463198 
datetime is 2013-06-07 12:30:19.463198
  1. 让我们构造并打印出timedelta对象,这是datetime模块中的另一种主要数据类型:
>>> td = datetime.timedelta(weeks=2, days=5, hours=10,
                            minutes=20, seconds=6.73,
                            milliseconds=99, microseconds=8)
>>> print(td)
19 days, 10:20:06.829008
  1. 将此timedelta添加/减去到步骤 1 中的datedatetime对象中:
>>> print('new date is', date + td)
>>> print('new datetime is', dt + td)
new date is 2013-06-26
new datetime is 2013-06-26 22:50:26.292206
  1. 尝试将timedelta添加到time对象是不可能的:
>>> time + td
TypeError: unsupported operand type(s) for +: 'datetime.time' and 'datetime.timedelta'
  1. 让我们看一下 Pandas 及其Timestamp对象,这是具有纳秒精度的时间片刻。Timestamp构造器非常灵活,可以处理各种输入:
>>> pd.Timestamp(year=2012, month=12, day=21, hour=5,
                 minute=10, second=8, microsecond=99)
Timestamp('2012-12-21 05:10:08.000099')

>>> pd.Timestamp('2016/1/10') Timestamp('2016-01-10 00:00:00')

>>> pd.Timestamp('2014-5/10') Timestamp('2014-05-10 00:00:00')

>>> pd.Timestamp('Jan 3, 2019 20:45.56') Timestamp('2019-01-03 20:45:33')

>>> pd.Timestamp('2016-01-05T05:34:43.123456789') Timestamp('2016-01-05 05:34:43.123456789')
  1. 也可以将单个整数或浮点数传递给Timestamp构造器,该构造器返回的日期等于 Unix 纪元(即 1970 年 1 月 1 日)之后的纳秒数:
>>> pd.Timestamp(500)
Timestamp('1970-01-01 00:00:00.000000500')

>>> pd.Timestamp(5000, unit='D')
Timestamp('1983-09-10 00:00:00')
  1. Pandas 提供了to_datetime函数,其功能与Timestamp构造器非常相似,但在特殊情况下带有一些不同的参数。 请参阅以下示例:
>>> pd.to_datetime('2015-5-13')
Timestamp('2015-05-13 00:00:00')

>>> pd.to_datetime('2015-13-5', dayfirst=True)
Timestamp('2015-05-13 00:00:00')

>>> pd.to_datetime('Start Date: Sep 30, 2017 Start Time: 1:30 pm', 
               format='Start Date: %b %d, %Y Start Time: %I:%M %p')
Timestamp('2017-09-30 13:30:00')

>>> pd.to_datetime(100, unit='D', origin='2013-1-1')
Timestamp('2013-04-11 00:00:00')
  1. to_datetime函数具有更多功能。 它能够将整个列表或字符串序列或整数转换为时间戳。 由于我们更可能与序列或数据帧交互,而不是与单个标量值交互,因此您比Timestamp更可能使用to_datetime
>>> s = pd.Series([10, 100, 1000, 10000])
>>> pd.to_datetime(s, unit='D')
0   1970-01-11
1   1970-04-11
2   1972-09-27
3   1997-05-19
dtype: datetime64[ns]

>>> s = pd.Series(['12-5-2015', '14-1-2013',
                   '20/12/2017', '40/23/2017'])
>>> pd.to_datetime(s, dayfirst=True, errors='coerce')
0   2015-05-12
1   2013-01-14
2   2017-12-20
3          NaT
dtype: datetime64[ns]

>>> pd.to_datetime(['Aug 3 1999 3:45:56', '10/31/2017'])
DatetimeIndex(['1999-08-03 03:45:56', 
               '2017-10-31 00:00:00'], dtype='datetime64[ns]', freq=None)
  1. 类似于Timestamp构造器和to_datetime函数,pandas 具有Timedeltato_timedelta来表示时间量。Timedelta构造器和to_timedelta函数都可以创建一个Timedelta对象。 与to_datetime一样,to_timedelta具有更多功能,可以将整个列表或序列转换为Timedelta对象。
>>> pd.Timedelta('12 days 5 hours 3 minutes 123456789 nanoseconds')
Timedelta('12 days 05:03:00.123456')

>>> pd.Timedelta(days=5, minutes=7.34)
Timedelta('5 days 00:07:20.400000')

>>> pd.Timedelta(100, unit='W')
Timedelta('700 days 00:00:00')

>>> pd.to_timedelta('67:15:45.454')
Timedelta('2 days 19:15:45.454000')

>>> s = pd.Series([10, 100])
>>> pd.to_timedelta(s, unit='s')
0   00:00:10
1   00:01:40
dtype: timedelta64[ns]

>>> time_strings = ['2 days 24 minutes 89.67 seconds',
                    '00:45:23.6']
>>> pd.to_timedelta(time_strings)
TimedeltaIndex(['2 days 00:25:29.670000', 
                '0 days 00:45:23.600000'], dtype='timedelta64[ns]', freq=None)
  1. 可以将时间戳添加到时间戳中或从时间戳中减去。 它们甚至可以彼此分开以返回浮点数:
>>> pd.Timedelta('12 days 5 hours 3 minutes') * 2
Timedelta('24 days 10:06:00')

>>> pd.Timestamp('1/1/2017') + \
    pd.Timedelta('12 days 5 hours 3 minutes') * 2
Timestamp('2017-01-25 10:06:00')

>>> td1 = pd.to_timedelta([10, 100], unit='s')
>>> td2 = pd.to_timedelta(['3 hours', '4 hours'])
>>> td1 + td2
TimedeltaIndex(['03:00:10', '04:01:40'],
               dtype='timedelta64[ns]', freq=None)

>>> pd.Timedelta('12 days') / pd.Timedelta('3 days')
4.0
  1. 时间戳和时间增量都有大量可用作属性和方法的功能。 让我们采样其中的一些:
>>> ts = pd.Timestamp('2016-10-1 4:23:23.9')

>>> ts.ceil('h')
Timestamp('2016-10-01 05:00:00'

>>> ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second
(2016, 10, 1, 4, 23, 23)

>>> ts.dayofweek, ts.dayofyear, ts.daysinmonth
(5, 275, 31)

>>> ts.to_pydatetime()
datetime.datetime(2016, 10, 1, 4, 23, 23, 900000)

>>> td = pd.Timedelta(125.8723, unit='h')
>>> td
Timedelta('5 days 05:52:20.280000')

>>> td.round('min')
Timedelta('5 days 05:52:00')

>>> td.components
Components(days=5, hours=5, minutes=52, seconds=20, milliseconds=280, microseconds=0, nanoseconds=0)

>>> td.total_seconds()
453140.28

工作原理

datetime模块是 Python 标准库的一部分,非常流行并且被广泛使用。 因此,最好对它有所了解,因为您可能会跨过它。datetime模块实际上非常简单,总共只有六种类型的对象:datetimedatetimetimedelta以及时区上的其他两个对象。 Pandas TimestampTimedelta对象具有datetime模块对应物的所有功能以及更多功能。 在处理时间序列时,将有可能完全保留在 Pandas 中。

步骤 1 显示了如何使用datetime模块创建日期时间,日期,时间和时间增量。 只有整数可以用作日期或时间的每个组成部分,并作为单独的参数传递。 将此与第 5 步进行比较,在第 5 步中,pandas Timestamp构造器可以接受与参数相同的组件,以及各种日期字符串。 除了整数部分和字符串,第 6 步还显示了如何将单个数字标量用作日期。 此标量的单位默认为纳秒ns),但在第二条语句中将其更改为D),其他选项为小时h),分钟m),s),毫秒ms)和微秒µs)。

步骤 2 详细说明了datetime模块的timedelta对象及其所有参数的构造。 再次,将其与步骤 9 中显示的 pandas Timedelta构造器进行比较,该构造器接受这些相同的参数以及字符串和标量数字。

除了仅能创建单个对象的TimestampTimedelta构造器之外,to_datetimeto_timedelta函数还可以将整数或字符串的整个序列转换为所需的类型 。 这些函数还提供了构造器不可用的其他几个参数。 这些参数之一是errors,默认为字符串值raise,但也可以设置为ignorecoerce。 每当无法转换字符串日期时,errors参数都会确定要采取的措施。 当设置为raise时,引发异常并且程序执行停止。 当设置为ignore时,将返回原始序列,就像进入函数之前一样。 当设置为coerce时,NaT(不是时间)对象用于表示新值。 步骤 8 的第二条语句将所有值正确转换为Timestamp,最后一个被强制变为NaT

仅可用于to_datetime的这些参数中的另一个参数是format,当字符串包含 Pandas 无法自动识别的特定日期模式时,该参数特别有用。 在步骤 7 的第三条语句中,我们在其他一些字符中嵌入了日期时间。 我们用它们各自的格式指令替换字符串的日期和时间。

日期格式指令以单个百分号%开头,后跟单个字符。 每个指令都指定日期或时间的某些部分。 有关所有指令的表格,请参见 Python 官方文档

更多

当将大量字符串转换为时间戳时,日期格式指令实际上可以产生很大的不同。 每当 Pandas 使用to_datetime将字符串序列转换为时间戳时,它都会搜索代表日期的大量不同字符串组合。 即使所有字符串都具有相同的格式,也是如此。 通过format参数,我们可以指定确切的日期格式,这样 Pandas 不必每次都搜索正确的日期格式。 让我们创建一个日期列表作为字符串,并使用和不使用格式指令将它们转换为时间戳的时间:

>>> date_string_list = ['Sep 30 1984'] * 10000

>>> %timeit pd.to_datetime(date_string_list, format='%b %d %Y')
35.6 ms ± 1.47 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

>>> %timeit pd.to_datetime(date_string_list)
1.31 s ± 63.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

提供格式化指令可使性能提高 40 倍。

另见

智能分割时间序列

在第 4 章,“选择数据子集”中,彻底介绍了数据帧的选择和切片。 当数据帧具有DatetimeIndex时,将出现更多选择和切片的机会。

准备

在本秘籍中,我们将使用部分日期匹配来选择和切片带有DatetimeIndex的数据帧。

操作步骤

  1. hdf5文件crimes.h5读取丹佛crimes数据集,并输出列数据类型和前几行。hdf5文件格式允许有效地存储大量科学数据,并且与 CSV 文本文件完全不同。
>>> crime = pd.read_hdf('data/crime.h5', 'crime')
>>> crime.dtypes
OFFENSE_TYPE_ID              category
OFFENSE_CATEGORY_ID          category
REPORTED_DATE          datetime64[ns]
GEO_LON                       float64
GEO_LAT                       float64
NEIGHBORHOOD_ID              category
IS_CRIME                        int64
IS_TRAFFIC                      int64
dtype: object
  1. 请注意,有三个类别列和一个Timestamp(由 NumPy 的datetime64对象表示)。 这些数据类型是在创建数据文件时存储的,这与仅存储原始文本的 CSV 文件不同。 设置REPORTED_DATE列作为索引,以便进行智能时间戳切片:
>>> crime = crime.set_index('REPORTED_DATE')
>>> crime.head()

  1. 像往常一样,可以通过将值传递给.loc索引运算符来选择等于单个索引的所有行:
>>> crime.loc['2016-05-12 16:45:00']

  1. 在索引中使用Timestamp时,可以选择部分匹配索引值的所有行。 例如,如果我们要获取 2016 年 5 月 5 日以后的所有罪行,则只需选择以下内容:
>>> crime.loc['2016-05-12']

  1. 您不仅可以选择不正确的日期,而且可以选择整个月,一年甚至一天的小时:
>>> crime.loc['2016-05'].shape
(8012, 7)

>>> crime.loc['2016'].shape
(91076, 7)

>>> crime.loc['2016-05-12 03'].shape
(4, 7)
  1. 选择字符串还可以包含月份名称:
>>> crime.loc['Dec 2015'].sort_index()

  1. 包含月份名称的许多其他字符串模式也可以使用:
>>> crime.loc['2016 Sep, 15'].shape
(252, 7)

>>> crime.loc['21st October 2014 05'].shape
(4, 7)
  1. 除了选择之外,您还可以使用切片符号来选择精确的数据范围:
>>> crime.loc['2015-3-4':'2016-1-1'].sort_index()

  1. 请注意,无论何时何地,在结束日期实现的所有犯罪都包含在返回的结果中。 对于使用基于标签的.loc索引器的任何结果,都是如此。 您可以为切片的任何开始或结束部分提供尽可能多的精度(或缺乏精度):
>>> crime.loc['2015-3-4 22':'2016-1-1 11:45:00'].sort_index()

工作原理

hdf5文件的许多不错的功能之一是它们保留每一列的数据类型的能力,从而大大减少了所需的内存。 在这种情况下,这些列中的三列存储为 pandas 类别而不是对象。 将它们存储为对象将导致内存使用量增加四倍:

>>> mem_cat = crime.memory_usage().sum()
>>> mem_obj = crime.astype({'OFFENSE_TYPE_ID':'object', 
                            'OFFENSE_CATEGORY_ID':'object', 
                            'NEIGHBORHOOD_ID':'object'}) \
                   .memory_usage(deep=True).sum()
>>> mb = 2 ** 20
>>> round(mem_cat / mb, 1), round(mem_obj / mb, 1)
(29.4, 122.7)

为了使用索引运算符按日期智能地选择和切片行,索引必须包含日期值。 在步骤 2 中,我们将REPORTED_DATE列移到索引中,并正式创建DatetimeIndex作为新索引:

>>> crime.index[:2]
DatetimeIndex(['2014-06-29 02:01:00', '2014-06-29 01:54:00'],
dtype='datetime64[ns]', name='REPORTED_DATE', freq=None)

使用DatetimeIndex时,可以使用.loc索引器使用多种字符串选择行。 实际上,所有可以发送到 pandas Timestamp构造器的字符串都将在这里工作。 出乎意料的是,对于该秘籍中的任何选择或切片,实际上都没有必要使用.loc索引器。 索引运算符本身将以完全相同的方式工作。 例如,步骤 6 的第二条语句可以写为crime['21st October 2014 05']。 索引运算符通常为列保留,但只要存在DatetimeIndex,就可以灵活地使用时间戳。

就个人而言,我更喜欢在选择行时使用.loc索引器,并且始终将其本身用于索引运算符。.loc索引器是显式的,传递给它的第一个值始终用于选择行。

步骤 8 和 9 显示切片的工作方式与从先前步骤中选择的相同。 结果中将包括与片段的开始或结束值部分匹配的任何日期。

更多

我们原始的犯罪数据帧未排序,并且切片仍按预期工作。 对索引进行排序将导致性能大幅提高。 让我们看一下与第 8 步完成的切片的区别:

>>> %timeit crime.loc['2015-3-4':'2016-1-1']
39.6 ms ± 2.77 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

>>> crime_sort = crime.sort_index()
>>> %timeit crime_sort.loc['2015-3-4':'2016-1-1']
758 µs ± 42.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

排序后的数据帧与原始数据相比,性能提高了 50 倍。

另见

  • 请参阅第 4 章,“选择数据子集”

使用仅适用于日期时间索引的方法

有许多仅适用于日期时间索引的数据帧/序列方法。 如果索引为任何其他类型,则这些方法将失败。

准备

在本秘籍中,我们将首先使用方法按照时间成分选择数据行。 然后,我们将学习功能强大的日期偏移对象及其别名。

操作步骤

  1. 读取犯罪 HDF5 数据集,将索引设置为REPORTED_DATE,并确保我们具有日期时间索引:
>>> crime = pd.read_hdf('data/crime.h5', 'crime') \
              .set_index('REPORTED_DATE')
>>> print(type(crime.index))
<class 'pandas.core.indexes.datetimes.DatetimeIndex'>
  1. 使用between_time方法选择在凌晨 2 点到凌晨 5 点之间发生的所有犯罪,无论日期如何:
>>> crime.between_time('2:00', '5:00', include_end=False).head()

  1. 使用at_time选择特定时间的所有日期:
>>> crime.at_time('5:47').head()

  1. first方法提供了一种选择前n个时间段的优雅方法,其中n是整数。 这些时间段由可以在pd.offsets模块中的DateOffset对象正式表示。 必须按其索引对数据帧进行排序,以确保此方法可以工作。 让我们选择犯罪数据的前六个月:
>>> crime_sort = crime.sort_index()
>>> crime_sort.first(pd.offsets.MonthBegin(6))

  1. 这捕获了从 1 月到 6 月的数据,但令人惊讶的是,在 7 月选择了一行。 原因是 Pandas 实际上使用了索引中第一个元素的时间分量,在此示例中为6分钟。 让我们使用MonthEnd,这是一个稍微不同的偏移量:
>>> crime_sort.first(pd.offsets.MonthEnd(6))

  1. 这捕获了几乎相同数量的数据,但是如果仔细观察,仅捕获了 6 月 30 日以来的一行。 同样,这是因为保留了第一个索引的时间部分。 确切的搜索结果为2012-06-30 00:06:00。 那么,我们如何才能准确地获得六个月的数据呢? 有两种方法。 所有DateOffset都有一个normalize参数,当设置为True时,会将所有时间分量设置为零。 以下应该使我们非常接近我们想要的:
>>> crime_sort.first(pd.offsets.MonthBegin(6, normalize=True))

  1. 此方法已成功捕获了一年前六个月的所有数据。 在将normalize设置为True的情况下,搜索到2012-07-01 00:00:00,它实际上将包括该日期和时间确切报告的任何犯罪。 实际上,无法使用第一种方法来确保仅捕获从一月到六月的数据。 以下非常简单的切片将产生准确的结果:
>>> crime_sort.loc[:'2012-06']
  1. 有十二个日期偏移对象,可以非常精确地向前或向后移动到下一个最近的偏移量。 您可以使用名为偏移别名的字符串代替在pd.offsets中查找日期偏移对象。 例如,月末的字符串是M,月初的字符串是MS。 要表示这些偏移别名的数量,只需在其前面放置一个整数。 使用此表查找所有别名。 让我们看一下偏移别名的一些示例,其中包含对注释中所选内容的描述:
>>> crime_sort.first('5D') # 5 days
>>> crime_sort.first('5B') # 5 business days
>>> crime_sort.first('7W') # 7 weeks, with weeks ending on Sunday
>>> crime_sort.first('3QS') # 3rd quarter start
>>> crime_sort.first('A') # one year end

工作原理

一旦确保索引为日期时间索引,就可以利用本秘籍中的所有方法。 使用.loc索引器无法仅根据Timestamp的时间成分进行选择或切片。 要按时间范围选择所有日期,必须使用between_time方法,或者要选择确切的时间,请使用at_time。 确保为开始时间和结束时间传递的字符串至少包含小时和分钟。 也可以使用datetime模块中的time对象。 例如,以下命令将产生与步骤 2 相同的结果:

>>> import datetime
>>> crime.between_time(datetime.time(2,0), datetime.time(5,0),
                       include_end=False)

在第 4 步中,我们开始使用简单的first方法,但使用复杂的参数offset。 它必须是日期偏移对象,也可以是字符串的偏移别名。 为了帮助理解日期偏移对象,最好查看它们对单个Timestamp的作用。 例如,让我们采用索引的第一个元素,并以两种不同的方式为其添加六个月的时间:

>>> first_date = crime_sort.index[0]
>>> first_date
Timestamp('2012-01-02 00:06:00')

>>> first_date + pd.offsets.MonthBegin(6)
Timestamp('2012-07-01 00:06:00')

>>> first_date + pd.offsets.MonthEnd(6)
Timestamp('2012-06-30 00:06:00')

MonthBeginMonthEnd偏移量都不会增加或减少确切的时间量,而是有效地向上舍入到下个月的下一个月初或下个月,而不管它是在哪一天。 在内部,first方法使用数据帧的第一个索引元素,并添加传递给它的日期偏移。 然后切成片直到这个新日期。 例如,步骤 4 等效于以下内容:

>>> step4 = crime_sort.first(pd.offsets.MonthEnd(6))

>>> end_dt = crime_sort.index[0] + pd.offsets.MonthEnd(6)
>>> step4_internal = crime_sort[:end_dt]
>>> step4.equals(step4_internal)
True

步骤 5 至 7 直接从前面的等效操作开始。 在步骤 8 中,偏移别名使引用 DateOffsets 的方法更加紧凑。

first方法相对应的是last方法,该方法从给定日期偏移的数据帧中选择最后n个时间段。分组对象具有两个名称完全相同但功能完全不同的方法。 它们返回每个组的第一个或最后一个元素,与拥有日期时间索引无关。

更多

当可用的那些不能完全满足您的需求时,可以构建一个自定义的日期偏移:

>>> dt = pd.Timestamp('2012-1-16 13:40')
>>> dt + pd.DateOffset(months=1)
Timestamp('2012-02-16 13:40:00')

请注意,此自定义日期偏移使Timestamp精确增加了一个月。 让我们再看一个使用更多日期和时间组件的示例:

>>> do = pd.DateOffset(years=2, months=5, days=3,
                       hours=8, seconds=10)
>>> pd.Timestamp('2012-1-22 03:22') + do
Timestamp('2014-06-25 11:22:10')

另见

计算每周的犯罪数量

原始的丹佛犯罪数据集非常庞大,有 460,000 多行标记有报告日期。 计算每周犯罪的数量是可以通过根据一段时间进行分组来回答的许多查询之一。resample方法提供了一个简单的接口,可以按任何可能的时间跨度进行分组。

准备

在本秘籍中,我们将同时使用resamplegroupby方法来计算每周犯罪的数量。

操作步骤

  1. 读取犯罪 HDF5 数据集,将索引设置为REPORTED_DATE,然后对其进行排序以提高其余秘籍的性能:
>>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \
                   .set_index('REPORTED_DATE') \
                   .sort_index()
  1. 为了计算每周的犯罪数量,我们需要每周组成一个小组。resample方法采用日期偏移对象或别名,并返回准备对所有组执行操作的对象。 从resample方法返回的对象与调用groupby方法后产生的对象非常相似:
>>> crime_sort.resample('W')
DatetimeIndexResampler [freq=<Week: weekday=6>, axis=0, closed=right, label=right, convention=start, base=0]
  1. 偏移别名W, 用来通知 Pandas 我们要按周分组。 在上一步中没有发生太多事情。 Pandas 只是简单地验证了我们的偏移量,并返回了一个对象,该对象准备好每周作为一组执行操作。 调用resample返回一些数据后,可以链接几种方法。 让我们链接size方法以计算每周犯罪数量:
>>> weekly_crimes = crime_sort.resample('W').size()
>>> weekly_crimes.head()
REPORTED_DATE
2012-01-08     877
2012-01-15    1071
2012-01-22     991
2012-01-29     988
2012-02-05     888
Freq: W-SUN, dtype: int64
  1. 现在,我们将每周犯罪计数列为一个序列,而新索引一次增加一周。 默认情况下,有些事情是很重要的,要理解。 选择周日作为一周的最后一天,并且该日期也是用来标记所得序列中每个元素的日期。 例如,第一个索引值 2012 年 1 月 8 日是星期日。 在截至 8 日的那一周内,共发生了 877 起犯罪。 1 月 9 日星期一至 1 月 15 日星期日这周记录了 1,071 起犯罪。 让我们做一些健全性检查,并确保我们的重采样正是这样做的:
>>> len(crime_sort.loc[:'2012-1-8'])
877

>>> len(crime_sort.loc['2012-1-9':'2012-1-15'])
1071
  1. 让我们选择除周日之外的另一天,以固定偏移结束一周:
>>> crime_sort.resample('W-THU').size().head()
REPORTED_DATE
2012-01-05     462
2012-01-12    1116
2012-01-19     924
2012-01-26    1061
2012-02-02     926
Freq: W-THU, dtype: int64
  1. resample的几乎所有功能都可以通过groupby方法再现。 唯一的区别是必须在pd.Grouper对象中传递偏移量:
>>> weekly_crimes_gby = crime_sort.groupby(pd.Grouper(freq='W')) \
                                  .size()
>>> weekly_crimes_gby.head()
REPORTED_DATE
2012-01-08     877
2012-01-15    1071
2012-01-22     991
2012-01-29     988
2012-02-05     888
Freq: W-SUN, dtype: int64

>>> weekly_crimes.equal(weekly_crimes_gby)
True

工作原理

默认情况下,resample方法与日期时间索引隐式工作,这就是为什么我们在步骤 1 中将其设置为REPORTED_DATE的原因。在步骤 2 中,我们创建了一个中间对象,可帮助我们了解如何在数据内形成组。resample的第一个参数是rule,用于确定如何对索引中的时间戳进行分组。 在这种情况下,我们使用偏移别名W来形成长度为一周的组,该组在周日结束。 默认的结束日期是星期日,但可以通过在星期几的前面加上破折号和前三个字母来更改锚定的偏移量。

一旦我们与resample组成了小组,我们就必须链接一个方法以对每个小组采取行动。 在第 3 步中,我们使用size方法来计算每周的犯罪数量。 您可能想知道调用resample之后可以使用哪些所有可能的属性和方法。 下面检查resample对象并输出它们:

>>> r = crime_sort.resample('W')
>>> resample_methods = [attr for attr in dir(r) if attr[0].islower()]
>>> print(resample_methods)
['agg', 'aggregate', 'apply', 'asfreq', 'ax', 'backfill', 'bfill', 'count', 'ffill', 'fillna', 'first', 'get_group', 'groups', 'indices', 'interpolate', 'last', 'max', 'mean', 'median', 'min', 'ndim', 'ngroups', 'nunique', 'obj', 'ohlc', 'pad', 'plot', 'prod', 'sem', 'size', 'std', 'sum', 'transform', 'var']

步骤 4 通过按周手动切片数据并计算行数来验证步骤 3 中计数的准确性。 实际上,甚至不需要按Timestamp分组resample方法,因为该功能可以直接从groupby方法本身获得。 但是,必须使用freq参数将偏移量pd.Grouper的实例传递给groupby方法,如步骤 6 所示。

一个非常类似的名为pd.TimeGrouper的对象能够按照与pd.Grouper完全相同的方式按时间进行分组,但是从熊猫 0.21 版本开始,它已被弃用,不应使用。 不幸的是,在线上有很多使用pd.TimeGrouper的例子,但不要让它们诱惑您。

更多

即使索引不包含Timestamp,也可以使用resample。 您可以使用on参数选择带有时间戳的列,这些列将用于形成组:

>>> crime = pd.read_hdf('data/crime.h5', 'crime')
>>> weekly_crimes2 = crime.resample('W', on='REPORTED_DATE').size()
>>> weekly_crimes2.equals(weekly_crimes)
True

同样,通过选择key参数的Timestamp列,可以将groupbypd.Grouper结合使用:

>>> weekly_crimes_gby2 = crime.groupby(pd.Grouper(key='REPORTED_DATE', 
                                                  freq='W')).size()
>>> weekly_crimes_gby2.equals(weekly_crimes_gby)
True

通过调用每周犯罪序列中的plot方法,我们还可以轻松地绘制丹佛所有犯罪(包括交通事故)的线图:

>>> weekly_crimes.plot(figsize=(16, 4), title='All Denver Crimes')

另见

分别汇总每周犯罪和交通事故

丹佛犯罪数据集将所有犯罪和交通事故汇总在一个表格中,并通过二进制列IS_CRIMEIS_TRAFFIC将它们分开。resample方法允许您按一段时间分组并分别汇总特定的列。

准备

在本秘籍中,我们将使用resample方法对一年中的每个季度进行分组,然后分别汇总犯罪和交通事故的数量。

操作步骤

  1. 读取犯罪 HDF5 数据集,将索引设置为REPORTED_DATE,然后对其进行排序以提高其余秘籍的性能:
>>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \
                   .set_index('REPORTED_DATE') \
                   .sort_index()
  1. 使用resample方法按一年中的每个季度进行分组,然后将各组的IS_CRIMEIS_TRAFFIC列求和:
>>> crime_quarterly = crime_sort.resample('Q')['IS_CRIME',
                                               'IS_TRAFFIC'].sum()
>>> crime_quarterly.head()

  1. 请注意,所有日期均显示为该季度的最后一天。 这是因为偏移别名Q代表该季度末。 让我们使用偏移别名QS代表季度的开始:
>>> crime_sort.resample('QS')['IS_CRIME', 'IS_TRAFFIC'].sum().head()

  1. 让我们通过检查第二季度的数据是否正确来验证这些结果:
>>> crime_sort.loc['2012-4-1':'2012-6-30', 
                   ['IS_CRIME', 'IS_TRAFFIC']].sum()
IS_CRIME      9641
IS_TRAFFIC    5255
dtype: int64
  1. 可以使用groupby方法复制此操作:
>>> crime_quarterly2 = crime_sort.groupby(pd.Grouper(freq='Q')) \
                                 ['IS_CRIME', 'IS_TRAFFIC'].sum()
>>> crime_quarterly2.equals(crime_quarterly)
True
  1. 让我们作图以更好地分析一段时间内犯罪和交通事故的趋势:
>>> plot_kwargs = dict(figsize=(16,4), 
                       color=['black', 'lightgrey'], 
                       title='Denver Crimes and Traffic Accidents')
>>> crime_quarterly.plot(**plot_kwargs)

工作原理

在第 1 步中读取并准备好数据后,我们在第 2 步中开始分组和聚合。调用resample方法后,我们可以通过链接方法或选择一组要聚合的列来继续进行操作。 我们选择选择IS_CRIMEIS_TRAFFIC列进行汇总。 如果我们不只是选择这两个,那么所有数字列的总和将具有以下结果:

>>> crime_sort.resample('Q').sum().head()

默认情况下,偏移别名Q在技术上使用 12 月 31 日作为一年的最后一天。 代表一个季度的日期范围全部使用此结束日期计算。 汇总结果使用该季度的最后一天作为标签。 步骤 3 使用偏移别名QS,默认情况下,它使用 1 月 1 日作为一年的第一天来计算季度。

大多数公共企业都报告季度收入,但是从一月开始,它们都没有相同的日历年。 例如,如果我们希望季度开始于 3 月 1 日,则可以使用QS-MAR来锚定偏移别名:

>>> crime_sort.resample('QS-MAR')['IS_CRIME', 'IS_TRAFFIC'] \
              .sum().head()

与前面的秘籍一样,我们通过手动切片来验证结果,并使用pd.Grouper使用groupby方法复制结果以设置组长。 在第 6 步中,我们仅调用数据帧的plot方法。 默认情况下,为每列数据绘制一条线。 该图清楚地表明,在今年的前三个季度,报告的犯罪数量急剧增加。 犯罪和贩运似乎都是季节性因素,在较冷的月份数字较低,在较暖的月份数字较高。

更多

为了获得不同的视觉角度,我们可以绘制犯罪和交通增加百分比,而不是原始计数。 让我们将所有数据除以第一行并再次绘图:

>>> crime_begin = crime_quarterly.iloc[0]
>>> crime_begin
IS_CRIME      7882
IS_TRAFFIC    4726
Name: 2012-03-31 00:00:00, dtype: int64

>>> crime_quarterly.div(crime_begin) \
                   .sub(1) \
                   .round(2) \
                   .plot(**plot_kwargs)

按工作日和年份衡量犯罪

通过按工作日和按年衡量犯罪的同时,必须具有直接从时间戳中提取此信息的函数。 值得庆幸的是,此函数内置于任何包含dt访问器的时间戳组成的列中。

准备

在本秘籍中,我们将使用dt访问器为我们提供每个犯罪的工作日名称和年份(序列)。 我们通过使用这两个序列的小组来计算所有犯罪。 最后,我们在创建犯罪总量热图之前,调整数据以考虑部分年份和人口。

操作步骤

  1. 读入丹佛犯罪 HDF5 数据集,将REPORTED_DATE保留为一列:
>>> crime = pd.read_hdf('data/crime.h5', 'crime')
>>> crime.head()

  1. 所有“时间戳”列均具有称为dt访问器的特殊属性,该属性可访问为它们专门设计的各种其他属性和方法。 让我们找到每个REPORTED_DATE的工作日名称,然后计算这些值:
>>> wd_counts = crime['REPORTED_DATE'].dt.weekday_name \
                                         .value_counts()
>>> wd_counts
Monday       70024
Friday       69621
Wednesday    69538
Thursday     69287
Tuesday      68394
Saturday     58834
Sunday       55213
Name: REPORTED_DATE, dtype: int64
  1. 周末看来,犯罪和交通事故的发生率大大降低。 让我们按正确的工作日顺序排列此数据,并绘制水平条形图:
>>> days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 
            'Friday', 'Saturday', 'Sunday']
>>> title = 'Denver Crimes and Traffic Accidents per Weekday'
>>> wd_counts.reindex(days).plot(kind='barh', title=title)

  1. 我们可以执行非常类似的过程来按年份绘制计数:
>>> title = 'Denver Crimes and Traffic Accidents per Year' 
>>> crime['REPORTED_DATE'].dt.year.value_counts() \
                             .sort_index() \
                             .plot(kind='barh', title=title)

  1. 我们需要按工作日和年份分组。 一种方法是将工作日和年份序列保存为单独的变量,然后将这些变量与groupby方法一起使用:
>>> weekday = crime['REPORTED_DATE'].dt.weekday_name
>>> year = crime['REPORTED_DATE'].dt.year

>>> crime_wd_y = crime.groupby([year, weekday]).size()
>>> crime_wd_y.head(10)
REPORTED_DATE  REPORTED_DATE
2012           Friday            8549
               Monday            8786
               Saturday          7442
               Sunday            7189
               Thursday          8440
               Tuesday           8191
               Wednesday         8440
2013           Friday           10380
               Monday           10627
               Saturday          8875
dtype: int64
  1. 我们已经正确汇总了数据,但是结构并不完全有利于轻松进行比较。 让我们先重命名那些无意义的索引级别名称,然后再将unstack重命名为工作日级别,以使我们的表更具可读性:
>>> crime_table = crime_wd_y.rename_axis(['Year', 'Weekday']) \
                            .unstack('Weekday')
>>> crime_table

  1. 现在,我们有了更好的表示形式,更易于阅读,但值得注意的是,2017 年的数字并不完整。 为了更公平地进行比较,我们可以进行简单的线性外推法来估算犯罪的最终数量。 首先让我们找到 2017 年数据的最后一天:
>>> criteria = crime['REPORTED_DATE'].dt.year == 2017
>>> crime.loc[criteria, 'REPORTED_DATE'].dt.dayofyear.max()
272
  1. 天真的估计是假设全年犯罪率保持不变,并将 2017 年表中的所有值乘以 365/272。 但是,我们可以做得更好,查看历史数据并计算在一年的前 272 天中发生的犯罪的平均百分比:
>>> round(272 / 365, 3)
.745

>>> crime_pct = crime['REPORTED_DATE'].dt.dayofyear.le(272) \
                                      .groupby(year) \
                                      .mean() \
                                      .round(3)
>>> crime_pct
REPORTED_DATE
2012    0.748
2013    0.725
2014    0.751
2015    0.748
2016    0.752
2017    1.000
Name: REPORTED_DATE, dtype: float64

>>> crime_pct.loc[2012:2016].median()
.748
  1. 事实证明,也许非常巧合的是,在一年的前 272 天发生的犯罪百分比几乎与该年过去的天数百分比成正比。 现在让我们更新 2017 年的行,并更改列顺序以匹配工作日顺序:
>>> crime_table.loc[2017] = crime_table.loc[2017].div(.748) \
                                        .astype('int')
>>> crime_table = crime_table.reindex(columns=days)
>>> crime_table

  1. 我们可以绘制条形图或折线图,但这对于热图也是一个很好的情况,seaborn 库中提供了该图:
>>> import seaborn as sns
>>> sns.heatmap(crime_table, cmap='Greys')

  1. 犯罪似乎每年都在增加,但是该数据并未说明人口的增长。 让我们读一下有数据的每年丹佛人口的表格:
>>> denver_pop = pd.read_csv('data/denver_pop.csv',
                             index_col='Year')
>>> denver_pop

  1. 据报告,许多犯罪指标是每 100,000 名居民的比率。 让我们将人口除以 100,000,然后将原始犯罪计数除以该数字即可得出每 100,000 居民的犯罪率:
>>> den_100k = denver_pop.div(100000).squeeze()
>>> crime_table2 = crime_table.div(den_100k, axis='index') \
                              .astype('int')
>>> crime_table2

  1. 再一次,我们可以制作一个热图,即使在调整了人口增长之后,该热图看起来也几乎与第一个相同:
>>> sns.heatmap(crime_table2, cmap='Greys')

工作原理

所有包含时间戳的数据帧的列都可以使用dt访问器访问许多其他属性和方法。 实际上,从dt访问器可用的所有这些方法和属性也可以直接从单个时间戳对象获得。

在第 2 步中,我们使用仅适用于序列的dt访问器来提取工作日名称并简单地计算发生次数。 在执行步骤 3 之前,我们使用reindex方法手动重新排列索引的顺序,在最基本的使用情况下,该方法接受包含所需顺序的列表。 也可以使用.loc索引器完成此任务,如下所示:

>>> wd_counts.loc[days]
Monday       70024
Tuesday      68394
Wednesday    69538
Thursday     69287
Friday       69621
Saturday     58834
Sunday       55213
Name: REPORTED_DATE, dtype: int64

.loc相比,reindex方法实际上性能更高,并且在更多情况下具有许多参数。 然后,我们使用dt访问器的weekday_name属性检索一周中每一天的名称,并在制作水平条形图之前对出现的次数进行计数。

在第 4 步中,我们执行一个非常相似的过程,并再次使用dt访问器检索年份,然后使用value_counts方法对发生次数进行计数。 在这种情况下,我们使用sort_index而不是reindex,因为年份自然会按所需顺序排序。

秘籍的目标是将工作日和年份进行分组,因此这正是我们在第 5 步中所做的。groupby方法非常灵活,可以通过多种方式进行分组。 在此秘籍中,我们将两个序列yearweekday传递给它们,所有唯一的组合从中组成一个组。 然后,我们将size方法链接到该方法,该方法返回单个值,即每个组的长度。

在第 5 步之后,我们的序列很长,只有一列数据,这使得很难按年和工作日进行比较。 为了简化可读性,我们将工作日级别使用unstack旋转为水平列名称。

在步骤 7 中,我们使用布尔索引来仅选择 2017 年的犯罪,然后再次使用dt访问器中的dayofyear查找从年初开始经过的总天数。 该序列的最大值应告诉我们 2017 年有多少天的数据。

步骤 8 非常复杂。 我们首先通过使用crime['REPORTED_DATE'].dt.dayofyear.le(272)测试每个犯罪是否在每年的第 272 天或之前犯下来创建布尔值序列。 从这里开始,我们再次使用灵活的groupby方法按照先前计算的year序列来分组,然后使用mean方法来查找每年第 272 天或之前的犯罪百分比。

.loc索引器在步骤 9 中选择整个 2017 年数据行。我们用该行除以在步骤 8 中找到的中位数百分比来调整该行。

许多犯罪的可视化都是通过热图完成的,其中一个步骤是在第 10 步借助seaborn可视化库完成的。cmap参数采用几十个可用 matplotlib 调色板的字符串名称

在第 12 步中,我们将100k居民的犯罪率除以该年的人口。 这实际上是一个相当棘手的操作。 通常,将一个数据帧除以另一个时,它们在其列和索引上对齐。 但是,在此步骤中,crime_table没有公用的denver_pop列,因此,如果我们尝试对它们进行划分,则没有值会对齐。 要解决此问题,我们使用squeeze方法创建了den_100k序列。 我们仍然不能简单地划分这两个对象,因为默认情况下,数据帧和序列之间的划分会将数据帧的列与序列的索引对齐,如下所示:

>>> crime_table / den_100k

我们需要数据帧的索引与序列的索引对齐,并且为此,我们使用div方法,该方法允许我们使用axis参数更改对齐方向。 在步骤 13 中绘制已调整犯罪率的heatmap

更多

让我们通过编写一个函数来一次完成此秘籍的所有步骤并添加选择特定类型犯罪的功能来完成此分析的完成:

>>> ADJ_2017 = .748

>>> def count_crime(df, offense_cat):
        df = df[df['OFFENSE_CATEGORY_ID'] == offense_cat]
        weekday = df['REPORTED_DATE'].dt.weekday_name
        year = df['REPORTED_DATE'].dt.year

        ct = df.groupby([year, weekday]).size().unstack()
        ct.loc[2017] = ct.loc[2017].div(ADJ_2017).astype('int')

        pop = pd.read_csv('data/denver_pop.csv', index_col='Year')
        pop = pop.squeeze().div(100000)

        ct = ct.div(pop, axis=0).astype('int')
        ct = ct.reindex(columns=days)
        sns.heatmap(ct, cmap='Greys')
        return ct

>>> count_crime(crime, 'auto-theft')

另见

使用日期时间索引和匿名函数进行分组

将数据帧与DatetimeIndex一起使用将为许多新的和不同的操作打开一扇门,如本章中的几个秘籍所示。

准备

在本秘籍中,我们将展示对具有DatetimeIndex的数据帧使用groupby方法的多功能性。

操作步骤

  1. 读入丹佛crime hdf5文件,将REPORTED_DATE列放在索引中,然后对其进行排序:
>>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \
                   .set_index('REPORTED_DATE') \
                   .sort_index()
  1. DatetimeIndex本身具有许多与 Pandas Timestamp相同的属性和方法。 让我们看一下它们的共同点:
>>> common_attrs = set(dir(crime_sort.index)) & \
                   set(dir(pd.Timestamp))
>>> print([attr for attr in common_attrs if attr[0] != '_'])

['to_pydatetime', 'normalize', 'day', 'dayofyear', 'freq', 'ceil', 
'microsecond', 'tzinfo', 'weekday_name', 'min', 'quarter', 'month', 
'tz_convert', 'tz_localize', 'is_month_start', 'nanosecond', 'tz', 
'to_datetime', 'dayofweek', 'year', 'date', 'resolution', 'is_quarter_end', 
'weekofyear', 'is_quarter_start', 'max', 'is_year_end', 'week', 'round', 
'strftime', 'offset', 'second', 'is_leap_year', 'is_year_start', 
'is_month_end', 'to_period', 'minute', 'weekday', 'hour', 'freqstr', 
'floor', 'time', 'to_julian_date', 'days_in_month', 'daysinmonth']
  1. 然后,我们可以使用索引来查找工作日名称,类似于上一秘籍的步骤 2 中所做的操作:
>>> crime_sort.index.weekday_name.value_counts()
Monday       70024
Friday       69621
Wednesday    69538
Thursday     69287
Tuesday      68394
Saturday     58834
Sunday       55213
Name: REPORTED_DATE, dtype: int64
  1. 令人惊讶的是,groupby方法具有接受函数作为参数的能力。 该函数将隐式传递给索引,并且其返回值用于形成组。 让我们通过使用将索引转换为工作日名称的函数进行分组,然后分别计算犯罪和交通事故的数量,来了解这一点:
>>> crime_sort.groupby(lambda x: x.weekday_name) \
               ['IS_CRIME', 'IS_TRAFFIC'].sum()

  1. 您可以使用函数列表按年中的小时和年进行分组,然后对表进行整形以使其更具可读性:
>>> funcs = [lambda x: x.round('2h').hour, lambda x: x.year]
>>> cr_group = crime_sort.groupby(funcs) \
                          ['IS_CRIME', 'IS_TRAFFIC'].sum()
>>> cr_final = cr_group.unstack()
>>> cr_final.style.highlight_max(color='lightgrey')

工作原理

在第 1 步中,我们读入数据并将一列时间戳放入索引中以创建日期时间索引。 在第 2 步中,我们看到日期时间索引具有许多与单个时间戳对象相同的函数。 在第 3 步中,我们直接使用日期时间索引的这些额外函数提取工作日名称。

在步骤 4 中,我们利用groupby方法的特殊功能来接受通过日期时间索引传递的函数。 匿名函数中的x实际上是日期时间索引,我们使用它来检索工作日名称。 可以传递groupby任意数量的自定义函数的列表,如步骤 5 所示。这里,第一个函数使用日期时间索引的round方法将每个值四舍五入到最接近的第二小时。 第二个函数检索年份。 在分组和汇总之后,我们将unstack年作为列。 然后,我们突出显示每列的最大值。 犯罪率最高的报告时间是下午 3 点至 5 点。 大多数交通事故发生在下午 5 点之间。 晚上 7 点

更多

此秘籍的最终结果是带有多重索引列的数据帧。 使用此数据帧,可以仅选择犯罪或交通事故。xs方法允许您从任何索引级别中选择一个值。 让我们看一个示例,其中我们仅选择处理流量的数据部分:

>>> cr_final.xs('IS_TRAFFIC', axis='columns', level=0).head()

这称为在 Pandas 中截取的横截面。 我们必须使用axislevel参数专门表示我们的值所在的位置。 让我们再次使用xs仅选择 2016 年中处于不同级别的数据:

>>> cr_final.xs(2016, axis='columns', level=1).head()

另见

按时间戳和另一列分组

resample方法本身无法按时间段进行分组。 但是,groupby方法可以按时间段和其他列进行分组。

准备

在此秘籍中,我们将展示两种非常相似但不同的方法来按时间戳分组,并在另一列中进行。

操作步骤

  1. 读取employee数据集,并使用HIRE_DATE列创建日期时间索引:
>>> employee = pd.read_csv('data/employee.csv', 
                           parse_dates=['JOB_DATE', 'HIRE_DATE'], 
                           index_col='HIRE_DATE')
>>> employee.head()

  1. 首先,让我们按性别进行简单分组,然后找到每个分组的平均工资:
>>> employee.groupby('GENDER')['BASE_SALARY'].mean().round(-2)
GENDER
Female    52200.0
Male      57400.0
Name: BASE_SALARY, dtype: float64
  1. 让我们根据租用日期找到平均薪水,然后将每个人归类为 10 年:
>>> employee.resample('10AS')['BASE_SALARY'].mean().round(-2)
HIRE_DATE
1958-01-01     81200.0
1968-01-01    106500.0
1978-01-01     69600.0
1988-01-01     62300.0
1998-01-01     58200.0
2008-01-01     47200.0
Freq: 10AS-JAN, Name: BASE_SALARY, dtype: float64
  1. 如果我们想按性别和五年时间跨度分组,可以在致电groupby之后直接致电resample
>>> employee.groupby('GENDER').resample('10AS')['BASE_SALARY'] \
            .mean().round(-2)
GENDER  HIRE_DATE 
Female  1975-01-01     51600.0
        1985-01-01     57600.0
        1995-01-01     55500.0
        2005-01-01     51700.0
        2015-01-01     38600.0
Male    1958-01-01     81200.0
        1968-01-01    106500.0
        1978-01-01     72300.0
        1988-01-01     64600.0
        1998-01-01     59700.0
        2008-01-01     47200.0
Name: BASE_SALARY, dtype: float64
  1. 现在,这已经完成了我们打算要做的工作,但是每当我们要比较男性和女性的工资时,我们都会遇到一个小问题。 让我们unstack性别级别,看看会发生什么:
>>> sal_avg.unstack('GENDER')

  1. 男性和女性的 10 年期限不在同一日期开始。 发生这种情况的原因是,数据首先按性别分组,然后在每种性别内,根据雇用日期组成了更多的组。 让我们验证一下第一位雇用的男性是 1958 年,第一位雇用的女性是 1975 年:
>>> employee[employee['GENDER'] == 'Male'].index.min()
Timestamp('1958-12-29 00:00:00')

>>> employee[employee['GENDER'] == 'Female'].index.min()
Timestamp('1975-06-09 00:00:00')
  1. 要解决此问题,我们必须将日期与性别一起分组,并且只有通过groupby方法才能做到这一点:
>>> sal_avg2 = employee.groupby(['GENDER', 
                                 pd.Grouper(freq='10AS')]) \
                        ['BASE_SALARY'].mean().round(-2)
>>> sal_avg2
GENDER  HIRE_DATE 
Female  1968-01-01         NaN
        1978-01-01     57100.0
        1988-01-01     57100.0
        1998-01-01     54700.0
        2008-01-01     47300.0
Male    1958-01-01     81200.0
        1968-01-01    106500.0
        1978-01-01     72300.0
        1988-01-01     64600.0
        1998-01-01     59700.0
        2008-01-01     47200.0
Name: BASE_SALARY, dtype: float64
  1. 现在我们可以unstack性别,使行完美对齐:
>>> sal_final = sal_avg2.unstack('GENDER')
>>> sal_final

工作原理

步骤 1 中的read_csv函数允许将列都转换为时间戳,并同时将它们放入索引中,以创建日期时间索引。 第 2 步使用单个分组列GENDER执行简单的groupby操作。 步骤 3 使用resample方法和偏移别名10AS以 10 年的时间增量形成组。A是年份的别名,S通知我们该时期的开始用作标签。 例如,标签1988-01-01的数据跨越该日期,直到 1997 年 12 月 31 日为止。

有趣的是,从对groupby方法的调用返回的对象具有其自己的resample方法,但反之则不成立:

>>> 'resample' in dir(employee.groupby('GENDER'))
True

>>> 'groupby' in dir(employee.resample('10AS'))
False

在第 4 步中,根据最早雇用的员工,计算出男女的 10 年完全不同的开始日期。 步骤 6 验证每种性别最早雇用的雇员的年份与步骤 4 的输出相匹配。步骤 5 显示了当我们尝试将女性的工资与男性的工资进行比较时,这如何导致不一致。 他们没有相同的 10 年期限。

要缓解此问题,我们必须将“性别”和“时间戳”归为一组。resample方法仅能按单个时间戳分组。 我们只能使用groupby方法完成此操作。 使用pd.Grouper,我们可以复制resample的功能。 我们只需将偏移别名传递给freq参数,然后将对象与我们希望分组的所有其他列一起放在列表中,如步骤 7 所示。由于现在男性和女性的开始日期都相同 10 年期间,步骤 8 中的重塑数据将针对每种性别进行调整,从而使比较变得更加容易。 看起来,随着工作时间的延长,男性的工资往往会更高,尽管在 10 年以下的工作中,男性和女性的平均工资相同。

更多

从局外人的角度来看,步骤 8 中输出的行代表 10 年的间隔并不明显。 改善索引标签的一种方法是显示每个时间间隔的开始和结束。 我们可以通过将当前索引年份与自身添加的 9 连接来实现此目的:

>>> years = sal_final.index.year
>>> years_right = years + 9
>>> sal_final.index = years.astype(str) + '-' + years_right.astype(str)
>>> sal_final

实际上,有一种完全不同的方法来制作此秘籍。 我们可以使用cut函数根据每位员工的受聘年限并从中形成组来创建等宽间隔:

>>> cuts = pd.cut(employee.index.year, bins=5, precision=0)
>>> cuts.categories.values
array([Interval(1958.0, 1970.0, closed='right'),
       Interval(1970.0, 1981.0, closed='right'),
       Interval(1981.0, 1993.0, closed='right'),
       Interval(1993.0, 2004.0, closed='right'),
       Interval(2004.0, 2016.0, closed='right')], dtype=object)

>>> employee.groupby([cuts, 'GENDER'])['BASE_SALARY'] \
            .mean().unstack('GENDER').round(-2)

使用merge_asof查找相比上次降低了 20% 的犯罪率

很多时候,我们想知道上一次发生什么事情的时间。 例如,我们可能对上一次失业率低于 5% 或上一次股市连续五天上涨或上一次睡眠八个小时感兴趣。merge_asof函数为这些类型的问题提供答案。

准备

在此秘籍中,我们将找到每种犯罪类别当月的犯罪总数,然后找到上次发生率降低 20% 的时间。

操作步骤

  1. 读入丹佛犯罪数据集,将REPORTED_DATE放在索引中,然后对其进行排序:
>>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \
               .set_index('REPORTED_DATE') \
               .sort_index()
  1. 查找最近一个月的数据:
>>> crime_sort.index.max()
Timestamp('2017-09-29 06:16:00')
  1. 由于我们没有 9 月份的全部数据,因此将其从数据集中删除:
>>> crime_sort = crime_sort[:'2017-8']
>>> crime_sort.index.max()
Timestamp('2017-08-31 23:52:00')
  1. 让我们计算每个月的犯罪和交通事故数量:
>>> all_data = crime_sort.groupby([pd.Grouper(freq='M'),
                                   'OFFENSE_CATEGORY_ID']).size()
>>> all_data.head()
REPORTED_DATE  OFFENSE_CATEGORY_ID
2012-01-31     aggravated-assault     113
               all-other-crimes       124
               arson                    5
               auto-theft             275
               burglary               343
dtype: int64
  1. 尽管merge_asof函数可以使用索引,但重置它会更容易:
>>> all_data = all_data.sort_values().reset_index(name='Total')
>>> all_data.head()

  1. 让我们获取当前月份的犯罪计数,并新建一个列来表示目标:
>>> goal = all_data[all_data['REPORTED_DATE'] == '2017-8-31'] \
                   .reset_index(drop=True)
>>> goal['Total_Goal'] = goal['Total'].mul(.8).astype(int)
>>> goal.head()

  1. 现在使用merge_asof函数查找每个犯罪类别的每月犯罪总数上一次小于Total_Goal列的时间:
>>> pd.merge_asof(goal, all_data, left_on='Total_Goal',
                  right_on='Total', by='OFFENSE_CATEGORY_ID',
                  suffixes=('_Current', '_Last'))

工作原理

读完我们的数据后,我们决定不包括 2017 年 9 月的数据,因为它不是一个完整的月份。 我们使用部分日期字符串对直至 2017 年 8 月的所有犯罪进行分割,在第 4 步中,我们统计每月每个犯罪类别的所有犯罪,在第 5 步中,我们按此总数进行排序,这对于merge_asof是必需的。

在第 6 步中,我们将最新数据选择到单独的数据帧中。 我们将以 8 月的这个月为基准,并创建Total_Goal列,该列比当前少 20% 。 在第 7 步中,我们使用merge_asof查找上一次每月犯罪计数少于Total_Goal列的时间。

更多

除了时间戳和时间增量数据类型外,pandas 还提供了时间段类型来表示确切的时间段。 例如,2012-05代表 2012 年 5 月的整个月份。您可以通过以下方式手动构建时间段:

>>> pd.Period(year=2012, month=5, day=17, hour=14, minute=20, freq='T')
Period('2012-05-17 14:20', 'T')

该对象表示 2012 年 5 月 17 日下午 2:20 的整个分钟。 可以在步骤 4 中使用这些期间,而不用pd.Grouper按日期分组。 具有日期时间索引的数据帧具有to_period方法,可以将时间戳转换为期间。 它接受偏移别名来确定时间段的确切长度。

>>> ad_period = crime_sort.groupby([lambda x: x.to_period('M'), 
                                    'OFFENSE_CATEGORY_ID']).size()
>>> ad_period = ad_period.sort_values() \
                         .reset_index(name='Total') \
                         .rename(columns={'level_0':'REPORTED_DATE'})
>>> ad_period.head()

让我们验证此数据帧的最后两列是否等效于步骤 5 中的all_data

>>> cols = ['OFFENSE_CATEGORY_ID', 'Total']
>>> all_data[cols].equals(ad_period[cols])
True

现在,可以使用以下代码以几乎完全相同的方式复制步骤 6 和 7:

>>> aug_2018 = pd.Period('2017-8', freq='M')
>>> goal_period = ad_period[ad_period['REPORTED_DATE'] == aug_2018] \
                           .reset_index(drop=True)
>>> goal_period['Total_Goal'] = goal_period['Total'].mul(.8).astype(int)

>>> pd.merge_asof(goal_period, ad_period, left_on='Total_Goal',
                  right_on='Total', by='OFFENSE_CATEGORY_ID',
                  suffixes=('_Current', '_Last')).head()

十一、Pandas,Matplotlib 和 Seaborn 的可视化

在本章中,我们将介绍以下主题:

  • matplotlib 入门
  • 使用 matplotlib 可视化数据
  • Pandas 绘图的基础知识
  • 可视化航班数据集
  • 堆叠面积图以发现新兴趋势
  • 了解 Pandas 与 Pandas 的区别
  • 使用 Seaborn 网格进行多元分析
  • 在 Seaborn 钻石数据集中发现辛普森悖论

介绍

可视化是探索性数据分析以及演示和应用中的关键组成部分。 在探索性数据分析过程中,您通常是一个人或成小组工作,需要快速创建绘图以帮助您更好地理解数据。 它可以帮助您识别异常值和丢失的数据,也可以引发其他令人感兴趣的问题,这些问题将导致进一步的分析和更直观的显示。 通常不会在考虑最终用户的情况下完成这种类型的可视化。 严格来说是为了帮助您更好地了解当前情况。 绘图不一定是完美的。

在为报表或应用准备可视化文件时,必须使用其他方法。 注意小细节。 此外,通常您必须将所有可能的可视化范围缩小到仅最能代表您数据的少数几个。 良好的数据可视化使观看者享受提取信息的体验。 就像使观众迷失的电影一样,好的可视化效果将包含大量真正引起人们兴趣的信息。

Python 中主要的数据可视化库是 matplotlib,该项目始于 2000 年代初期,旨在模仿 Matlab 的绘图函数。 Matplotlib 具有极大的能力来绘制您可以想象的大多数事物,它为用户提供了强大的功能来控制绘制表面的各个方面。 也就是说,对于初学者来说,它并不是最友好的库。 值得庆幸的是,Pandas 使我们对数据的可视化变得非常容易,并且通常只需单击plot方法即可绘制出我们想要的内容。 Pandas 实际上并没有独自策划。 它在内部调用 matplotlib 函数来创建图。 我认为,Pandas 还添加了自己的样式,该样式比 matplotlib 中的默认样式好一些。

Seaborn 还是一个可视化库,它在内部调用 matplotlib 函数,并且自身不进行任何实际绘制。 Seaborn 可以轻松轻松地制作漂亮的绘图,并允许创建许多新类型的绘图,而这些新绘图无法直接从 matplotlib 或 Pandas 获得。 Seaborn 处理整洁(长)数据,而 Pandas 处理汇总(宽)数据效果最佳。 Seaborn 在其绘图函数中还接受了 Pandas 数据帧对象。

尽管可以在不直接运行任何 matplotlib 代码的情况下创建图,但有时仍需要使用它来手动调整更精细的图细节。 因此,前两个秘籍将介绍 matplotlib 的一些基础知识,如果您需要直接使用它,将非常有用。 除了前两个秘籍外,所有绘图示例都将使用 Pandas 或海生豆。

Python 中的可视化不一定必须依赖于 matplotlib。 Bokeh 迅速成为针对 Web 的非常流行的交互式可视化库。 它完全独立于 matplotlib,并且能够生成整个应用。

matplotlib 入门

对于许多数据科学家而言,他们绝大部分的绘图命令将直接来自 Pandas 或海生动物,它们都完全依赖于 matplotlib 进行实际的绘图。 但是,pandas 和 seaborn 都不提供 matplotlib 的完整替代品,有时您需要直接使用它。 因此,本秘籍将简要介绍 matplotlib 的最关键方面。

准备

让我们从下图中的 matplotlib 图的解剖开始我们的介绍:

Matplotlib 使用对象层次结构在输出中显示其所有绘图项。 该层次结构是了解有关 matplotlib 的一切的关键。 图形对象是层次结构的两个主要组成部分。 图形对象位于层次结构的顶部。 它是将要绘制的所有内容的容器。 图中包含一个或多个轴对象。 轴是使用 matplotlib 时将与之交互的主要对象,通常可以将其视为实际的绘图表面。 轴包含 x/y 轴,点,线,标记,标签,图例以及其他任何绘制的有用项目。

2017 年初,matplotlib 在发布版本 2.0 时进行了重大更改。 许多默认的绘图参数已更改。 解剖图实际上来自版本 1 的文档,但与版本 2 中更新的解剖图相比,在区分图形和轴方面做得更好

需要在轴对象和轴之间进行非常明显的区分。 它们是完全独立的对象。 使用 matplotlib 术语的轴域对象不是轴的复数,而是如前所述,该对象创建并控制了大多数有用的绘图元素。 轴仅指图的 xy (甚至 z)轴。

不幸的是,matplotlib 选择使用轴域(Axes,即单词轴的复数)来指代完全不同的对象,但是它对于库来说是至关重要的,因此目前不太可能更改。

由轴域对象创建的所有这些有用的绘图元素都称为艺术家。 甚至图形和轴域对象本身也是艺术家。 对艺术家的这种区分对本秘籍而言并不重要,但在进行更高级的 matplotlib 绘图时,尤其是在阅读文档时,将很有用。

Matplotlib 的面向对象指南

Matplotlib 为用户提供了两个不同的接口来进行绘图。 有状态接口直接通过pyplot模块进行所有调用。 此接口称为有状态,因为 matplotlib 隐式跟踪绘图环境的当前状态。 每当在有状态接口中创建图时,matplotlib 都会找到当前图形或当前轴并对其进行更改。 这种方法可以快速绘制一些东西,但是当处理多个图形和轴时可能变得笨拙。

Matplotlib 还提供了无状态或面向对象的接口,您可以在其中显式使用引用特定绘图对象的变量。 然后可以使用每个变量来更改绘图的某些属性。 面向对象的方法是显式的,您始终清楚地知道要修改的对象。

不幸的是,同时使用这两个选项会导致很多混乱,并且 matplotlib 以难以学习而著称。 该文档提供了使用这两种方法的示例。 教程,博客文章, Stack Overflow 文章在网络上比比皆是,这使这种混乱永久化。 本秘籍仅专注于面向对象的方法,因为它具有更多的 Python 风格,并且与我们与 Pandas 互动的方式更加相似。

如果您不熟悉 matplotlib,则可能不知道如何识别每种方法之间的差异。 通过有状态接口,所有命令将直接从pyplot发出,通常是别名plt。 制作简单的线图并在每个轴上添加一些标签如下所示:

>>> import matplotlib.pyplot as plt

>>> x = [-3, 5, 7]
>>> y = [10, 2, 5]

>>> plt.figure(figsize=(15,3))
>>> plt.plot(x, y)
>>> plt.xlim(0, 10)
>>> plt.ylim(-3, 8)
>>> plt.xlabel('X Axis')
>>> plt.ylabel('Y axis')
>>> plt.title('Line Plot')
>>> plt.suptitle('Figure Title', size=20, y=1.03)

面向对象的方法仍然使用pyplot,但是通常,它只是在第一步中创建图形和轴域对象。 创建后,将直接调用这些对象的方法来更改绘图。 以下代码使用面向对象的方法对上一个图进行精确复制:

>>> fig, ax = plt.subplots(figsize=(15,3))
>>> ax.plot(x, y)
>>> ax.set_xlim(0, 10)
>>> ax.set_ylim(-3, 8)
>>> ax.set_xlabel('X axis')
>>> ax.set_ylabel('Y axis')
>>> ax.set_title('Line Plot')
>>> fig.suptitle('Figure Title', size=20, y=1.03)

在这个简单的示例中,我们仅直接使用两个对象,即图形和轴,但是通常,图可以包含数百个对象; 可以使用每一种都以非常精细的方式进行修改,而使用状态接口则不容易做到。 在本章中,我们将构建一个空图并使用面向对象的接口修改其一些基本属性。

操作步骤

  1. 要使用面向对象的方法开始使用 matplotlib,您将需要导入pyplot模块和别名plt
>>> import matplotlib.pyplot as plt
  1. 通常,当使用面向对象的方法时,我们将创建一个图形和一个或多个轴域对象。 让我们使用subplots函数创建具有单个轴的图形:
>>> fig, ax = plt.subplots(nrows=1, ncols=1)

  1. subplots函数返回一个包含图形和一个或多个轴域对象(这里只是一个)的两个项目元组对象,这些对象被解包到变量figax中。 从现在开始,我们将通过常规的面向对象方法调用方法来直接使用这些对象。 让我们看一下每个对象的类型,以确保我们实际使用的是图形和轴域:
>>> type(fig)
matplotlib.figure.Figure

>>> type(ax)
matplotlib.axes._subplots.AxesSubplot
  1. 尽管您将调用比图形方法更多的轴域,但您可能仍需要与它们交互。 让我们找到图的大小,然后将其放大:
>>> fig.get_size_inches()
array([ 6.,  4.])

>>> fig.set_size_inches(14, 4)
>>> fig

  1. 在开始绘制之前,让我们检查一下 matplotlib 层次结构。 您可以使用axes属性收集图中的所有轴:
>>> fig.axes
[<matplotlib.axes._subplots.AxesSubplot at 0x112705ba8>]
  1. 此命令返回所有轴对象的列表。 但是,我们已经将轴对象存储在ax变量中。 让我们确认它们实际上是同一对象:
>>> fig.axes[0] is ax
True
  1. 为了明显地将图形与轴区分开,我们可以给每个图形一个唯一的facecolor。 Matplotlib 接受各种不同的颜色输入类型。 字符串名称支持大约 140 种 HTML 颜色(请参见此列表)。 您还可以使用包含从零到一的浮点数的字符串来表示灰色阴影:
>>> fig.set_facecolor('.9')
>>> ax.set_facecolor('.7')
>>> fig

  1. 现在我们已经区分了图形和轴域,让我们用get_children方法查看轴域的所有直接子代:
>>> ax_children = ax.get_children()
>>> ax_children
[<matplotlib.spines.Spine at 0x11145b358>,
 <matplotlib.spines.Spine at 0x11145b0f0>,
 <matplotlib.spines.Spine at 0x11145ae80>,
 <matplotlib.spines.Spine at 0x11145ac50>,
 <matplotlib.axis.XAxis at 0x11145aa90>,
 <matplotlib.axis.YAxis at 0x110fa8d30>,
 ...]
  1. 每个基本图都有四个刺和两个轴对象。 脊线代表数据边界,是您看到的与较深的灰色矩形(“轴”)接壤的四根物理线。 xy 轴对象包含更多的绘图对象,例如刻度和它们的标签以及整个轴的标签。 我们可以从该列表中选择刺,但这通常不是这样做的。 我们可以使用spines属性直接访问它们:
>>> spines = ax.spines
>>> spines
OrderedDict([('left', <matplotlib.spines.Spine at 0x11279e320>),
             ('right', <matplotlib.spines.Spine at 0x11279e0b8>),
             ('bottom', <matplotlib.spines.Spine at 0x11279e048>),
             ('top', <matplotlib.spines.Spine at 0x1127eb5c0>)])
  1. 刺包含在有序字典中。 让我们选择左侧的脊椎,并更改其位置和宽度,使其更加突出,并使底部的脊椎不可见:
>>> spine_left = spines['left']
>>> spine_left.set_position(('outward', -100))
>>> spine_left.set_linewidth(5)

>>> spine_bottom = spines['bottom']
>>> spine_bottom.set_visible(False)
>>> fig

  1. 现在,让我们集中讨论轴对象。 我们可以通过xaxisyaxis属性直接访问每个轴。Axes对象也可以直接使用某些轴属性。 在此步骤中,我们以两种方式更改每个轴的某些属性:
>>> ax.xaxis.grid(True, which='major', linewidth=2,
                  color='black', linestyle='--')
>>> ax.xaxis.set_ticks([.2, .4, .55, .93])
>>> ax.xaxis.set_label_text('X Axis', family='Verdana', fontsize=15)

>>> ax.set_ylabel('Y Axis', family='Calibri', fontsize=20)
>>> ax.set_yticks([.1, .9])
>>> ax.set_yticklabels(['point 1', 'point 9'], rotation=45)
>>> fig

工作原理

面向对象方法要掌握的关键思想之一是每个绘图元素都具有获取器设置器方法。 获取器方法均以get_开头,并检索特定属性或检索其他绘图对象。 例如,ax.get_yscale()检索绘制 y 轴以字符串形式绘制的比例类型(默认为linear),而ax.get_xticklabels()检索 matplotlib 文本对象列表,每个都有自己的获取器和设置器方法。 设置方法修改特定的属性或整个对象组。 许多 matplotlib 归结为锁存到特定的绘图元素上,然后通过获取器和设置器方法进行检查和修改。

把 matplotlib 层次结构类比为家可能是有用的。 家及其所有内容将是图形。 每个房间都是轴域,房间的内容是艺术家。

开始使用面向对象接口的最简单方法是使用pyplot模块,该模块通常是步骤 1 中的别名plt和。步骤 2 显示了面向对象的方法,是最常见的启动方法之一。plt.subplots函数创建一个图形,以及一个轴域对象网格。 前两个参数nrowsncols和定义了统一的轴对象网格。 例如,plt.subplots(2,4)在一个图形中创建了八个相同大小的轴对象。

plt.subplots函数有点奇怪,因为它返回一个两个项的元组。 第一个元素是图形,第二个元素是轴域对象。 该元组被解压缩为两个不同的变量figax。 如果您不习惯于拆开元组,则可能会看到步骤 2 如下所示:

>>> plot_objects = plt.subplots(nrows=1, ncols=1)
>>> type(plot_objects)
tuple

>>> fig = plot_objects[0]
>>> ax = plot_objects[1]

如果使用plt.subplots和创建多个轴,则元组中的第二项是包含所有轴的 NumPy 数组。 让我们在这里演示一下:

>>> plot_objects = plt.subplots(2, 4)

plot_objects变量是一个元组,其中包含一个数字作为其第一个元素,并包含一个 Numpy 数组作为其第二个元素:

>>> plot_objects[1]
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x133b70a20>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x135d6f9e8>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x1310e4668>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x133565ac8>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x133f67898>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x1326d30b8>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x1335d5eb8>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x133f78f28>]], dtype=object)

步骤 3 验证我们确实有适当变量引用的图形和轴域对象。 在第 4 步中,我们遇到了获取器和设置器方法的第一个示例。 Matplotlib 将所有图形的默认宽度设置为 6 英寸乘以 4 英寸高,这不是屏幕上的实际大小,但是如果将图形保存到文件中,则将是确切大小。

步骤 5 显示,除了获取器方法之外,有时您还可以通过其属性直接访问另一个绘图对象。 通常,同时存在属性和获取方法来检索同一对象。 例如,查看以下示例:

>>> fig.axes == fig.get_axes()
True

>>> ax.xaxis == ax.get_xaxis()
True

>>> ax.yaxis == ax.get_yaxis()
True

许多美术师都具有facecolor属性,可以将其设置为覆盖一种特定颜色的整个表面,如步骤 7 所示。在步骤 8 中,可以使用get_children方法更好地了解对象层次。 返回轴正下方所有对象的列表。 可以从此列表中选择所有对象,然后开始使用设置器方法来修改属性,但这不是惯例。 通常,我们通常直接从属性或获取器方法中收集对象。

通常,在检索绘图对象时,它们会在列表或字典之类的容器中返回。 这就是在步骤 9 中收集刺时发生的情况。您必须从它们各自的容器中选择单个对象,以便在它们上使用获取器或设置器方法,如在步骤 10 中所做的那样。通常也使用for循环一次迭代一个。

步骤 11 以特殊方式添加网格线。 我们期望有get_gridset_grid方法,但是,只有grid方法,该方法接受布尔值作为打开/关闭网格线的第一个参数。 每个轴都有主刻度和次刻度,但默认情况下,副刻度是关闭的。which参数用于选择带有网格线的刻度线类型。

请注意,步骤 11 的前三行选择xaxis属性并从中调用方法,而后三行直接从轴域对象本身调用等效方法。 第二组方法是 matplotlib 提供的一种方便方式,可以节省一些击键。 通常,大多数对象只能设置自己的属性,而不能设置其子级的属性。 无法通过轴设置许多轴级属性,但是在此步骤中,可以设置一些属性。 两种方法都可以接受。

在步骤 11 中将网格线与第一行添加在一起时,我们设置属性linewidthcolor,,和linestyle。 这些都是 matplotlib 线(正式为Line2D对象)的所有属性。 您可以在此处查看所有可用属性。set_ticks方法接受一个浮点序列,并仅在那些位置绘制刻度线。 使用空列表将完全删除所有刻度。

每个轴可能都标有一些文本,为此 matplotlib 正式使用了Text对象。 所有可用文本属性中仅更改了几个。set_yticklabels轴方法接收一个字符串列表,用作每个刻度的标签。 您可以设置任意数量的文本属性。

更多

为了帮助找到每个绘图对象的所有可能的属性,只需调用properties方法,该方法会将所有它们显示为字典。 让我们看一下轴对象的属性的精选列表:

>>> ax.xaxis.properties()
{'alpha': None,
 'gridlines': <a list of 4 Line2D gridline objects>,
 'label': Text(0.5,22.2,'X Axis'),
 'label_position': 'bottom',
 'label_text': 'X Axis',
 'tick_padding': 3.5,
 'tick_space': 26,
 'ticklabels': <a list of 4 Text major ticklabel objects>,
 'ticklocs': array([ 0.2 , 0.4 , 0.55, 0.93]),
 'ticks_position': 'bottom',
 'visible': True}

另见

使用 matplotlib 可视化数据

Matplotlib 有几十种绘图方法,几乎​​可以想象任何一种绘图。 线,条,直方图,散点图,方格,小提琴,轮廓,饼图以及许多其他图都可以从“轴”对象中用作方法。 只有在 1.5 版(2015 年发布)中,matplotlib 才开始接受来自 Pandas 数据帧的数据。 在此之前,必须将数据从 NumPy 数组或 Python 列表传递给它。

准备

在本秘籍中,我们将通过将 Pandas 数据帧中的数据减少到 NumPy 数组来可视化电影预算随时间的趋势,然后将其传递给 matplotlib 绘图函数。

操作步骤

  1. 既然我们知道如何选择绘图元素并更改其属性,那么让我们实际创建数据可视化。 让我们阅读电影数据集,计算每年的预算中位数,然后找到五年滚动平均值以使数据平滑:
>>> movie = pd.read_csv('data/movie.csv')
>>> med_budget = movie.groupby('title_year')['budget'].median() / 1e6
>>> med_budget_roll = med_budget.rolling(5, min_periods=1).mean()
>>> med_budget_roll.tail()
title_year
2012.0    20.893
2013.0    19.893
2014.0    19.100
2015.0    17.980
2016.0    17.780
Name: budget, dtype: float64
  1. 让我们将数据放入 NumPy 数组中:
>>> years = med_budget_roll.index.values
>>> years[-5:]
array([ 2012.,  2013.,  2014.,  2015.,  2016.])

>>> budget = med_budget_roll.values
>>> budget[-5:]
array([ 20.893,  19.893,  19.1  ,  17.98 ,  17.78 ])
  1. plot方法用于创建折线图。 让我们用它在新图中绘制预算随时间推移的滚动中位数:
>>> fig, ax = plt.subplots(figsize=(14,4), linewidth=5,
                           edgecolor='.5')
>>> ax.plot(years, budget, linestyle='--', 
            linewidth=3, color='.2', label='All Movies')

>>> text_kwargs=dict(fontsize=20, family='cursive')
>>> ax.set_title('Median Movie Budget', **text_kwargs)
>>> ax.set_ylabel('Millions of Dollars', **text_kwargs)

  1. 有趣的是,电影预算中位数在 2000 年达到顶峰,随后又下降了。 也许这只是数据集的人工产物,其中近年来我们拥有的所有电影的数据都更多,而不仅仅是最受欢迎的电影。 让我们找出每年的电影数量:
>>> movie_count = movie.groupby('title_year')['budget'].count()
>>> movie_count.tail()
title_year
2012.0    191
2013.0    208
2014.0    221
2015.0    192
2016.0     86
Name: budget, dtype: int64
  1. 一个轴上可以放置任意数量的图,这些计数可以直接用中位数预算作为条形图绘制。 由于两个图的单位完全不同(美元与计数),因此我们可以创建辅助 y 轴,也可以将计数缩放到与预算相同的范围内。 我们选择后者,并在其前面直接将每个条的值标记为文本。 由于绝大多数数据都包含在最近几年中,因此我们也可以将数据限制为从 1970 年开始拍摄的电影:
>>> ct = movie_count.values
>>> ct_norm = ct / ct.max() * budget.max()

>>> fifth_year = (years % 5 == 0) & (years >= 1970)
>>> years_5 = years[fifth_year]
>>> ct_5 = ct[fifth_year]
>>> ct_norm_5 = ct_norm[fifth_year]

>>> ax.bar(years_5, ct_norm_5, 3, facecolor='.5', 
           alpha=.3, label='Movies per Year')
>>> ax.set_xlim(1968, 2017)
>>> for x, y, v in zip(years    _5, ct_norm_5, ct_5):
        ax.text(x, y + .5, str(v), ha='center')
>>> ax.legend()
>>> fig

  1. 如果仅查看每年预算最高的 10 部电影,这种趋势可能不会成立。 让我们找出每年仅前十部电影的五年滚动中位数:
>>> top10 = movie.sort_values('budget', ascending=False) \
                 .groupby('title_year')['budget'] \
                 .apply(lambda x: x.iloc[:10].median() / 1e6)

>>> top10_roll = top10.rolling(5, min_periods=1).mean()
>>> top10_roll.tail()
title_year
2012.0    192.9
2013.0    195.9
2014.0    191.7
2015.0    186.8
2016.0    189.1
Name: budget, dtype: float64
  1. 对于所有数据,这些数字表示一个比在步骤 13 中发现的数字高一个数量级。 以相同的比例绘制两条线看起来并不好。 让我们创建一个带有两个子图(轴)的全新图形,并在第二个轴中绘制上一步的数据:
>>> fig2, ax_array = plt.subplots(2, 1, figsize=(14,8), sharex=True)
>>> ax1 = ax_array[0]
>>> ax2 = ax_array[1]

>>> ax1.plot(years, budget, linestyle='--', linewidth=3, 
             color='.2', label='All Movies')
>>> ax1.bar(years_5, ct_norm_5, 3, facecolor='.5', 
            alpha=.3, label='Movies per Year')
>>> ax1.legend(loc='upper left')
>>> ax1.set_xlim(1968, 2017)
>>> plt.setp(ax1.get_xticklines(), visible=False)

>>> for x, y, v in zip(years_5, ct_norm_5, ct_5):
        ax1.text(x, y + .5, str(v), ha='center')

>>> ax2.plot(years, top10_roll.values, color='.2',
             label='Top 10 Movies')
>>> ax2.legend(loc='upper left')

>>> fig2.tight_layout()
>>> fig2.suptitle('Median Movie Budget', y=1.02, **text_kwargs)
>>> fig2.text(0, .6, 'Millions of Dollars', rotation='vertical', 
              ha='center', **text_kwargs)

>>> import os
>>> path = os.path.expanduser('~/Desktop/movie_budget.png')
>>> fig2.savefig(path, bbox_inches='tight')

工作原理

在第 1 步中,我们开始寻求分析电影预算的方法,方法是找出每年的预算中位数(百万美元)。 找到每年的预算中位数后,我们决定对其进行平滑处理,因为每年之间会有很大的差异。 我们选择对数据进行平滑处理是因为我们正在寻找一个总体趋势,而不必对任何一年的确切值感兴趣。

在此步骤中,我们使用rolling方法根据最近五年数据的平均值来计算每年的新值。 例如,将 2011 年至 2015 年的预算中位数进行分组并取平均值。 结果是 2015 年的新值。rolling方法唯一需要的参数是窗口的大小,默认情况下,窗口的大小将在当年结束。

rolling方法返回一个类似分组的对象,该对象必须使其组与另一个函数共同作用才能产生结果。 让我们手动验证rolling方法是否能像往年一样工作:

>>> med_budget.loc[2012:2016].mean()
17.78

>>> med_budget.loc[2011:2015].mean()
17.98

>>> med_budget.loc[2010:2014].mean()
19.1

这些值与步骤 1 的输出相同。在步骤 2 中,通过将数据放入 NumPy 数组中,我们准备使用 matplotlib。 在第 3 步中,我们创建图形和轴以设置面向对象的接口。plt.subplots方法支持大量输入。 请参阅此文档以查看此函数和figure函数的所有可能参数

plot方法中的前两个参数表示折线图的 x 和 y 值。 所有行属性都可以在plot的调用中进行更改。轴域的set_title方法提供标题,并可以在其调用内设置所有可用的文本属性。set_ylablel方法也是如此。 如果要为许多对象设置相同的属性,则可以将它们打包在一起作为字典,然后将该字典作为参数之一传递,如**text_kwargs一样。

在第 4 步中,我们注意到 2000 年左右开始的预算中值出现意外下降的趋势,并怀疑每年收集的电影数量可能起到解释作用。 我们选择通过从 1970 年开始每隔五年创建一个条形图来向图表添加此维度。我们对 NumPy 数据数组使用布尔选择的方式与在步骤 5 中对 Pandas 序列的处理方式相同。

bar方法将 x 值的高度和条形的宽度作为其前三个参数,并将条形的中心直接放在每个 x 值处。 条形高度是从电影计数中得出的,电影计数首先被缩小到零到一之间,然后乘以最大中位数预算。 这些钢筋高度存储在变量ct_norm_5中。 为了正确标记每个条形图,我们首先将条形图中心,其高度和实际影片数压缩在一起。 然后,我们遍历此压缩对象,并使用text方法将计数放在小节之前,该方法接受 x 值,y 值和字符串。 我们将 y 值略微向上调整,并使用水平对齐参数ha将文本居中。

回顾步骤 3,您会注意到label参数等于All Moviesplot方法。 这是为绘图创建图例时 matplotlib 使用的值。 调用legend Axes 方法会将所有带有指定标签的图放置在图例中。

为了调查预算中位数的意外下降,我们可以仅关注每年预算最高的 10 部电影。 在按年份分组后,第 6 步使用自定义聚合函数,然后以与以前相同的方式对结果进行平滑处理。 这些结果可以直接绘制在同一张图上,但是由于值要大得多,因此我们选择创建一个带有两个轴的全新图形。

我们通过在两个两行一列的网格中创建具有两个子图的图形来开始执行步骤 7。 请记住,当创建多个子图时,所有轴都存储在 NumPy 数组中。 步骤 5 的最终结果将在顶部轴中重新创建。 我们在底部的轴上绘制预算最高的 10 部电影。 请注意,年份与底部和顶部轴都对齐,因为在图形创建中sharex参数设置为True。 共享轴时,matplotlib 会删除所有刻度线的标签,但会保留每个刻度线的细小垂直线。 要删除这些刻度线,我们使用pyplotsetp函数。 尽管这不是直接面向对象的,但是当我们要为整个绘制对象序列设置属性时,它是显式的并且非常有用。 通过此有用的函数,我们将所有刻度线设置为不可见。

最后,我们然后多次调用图形方法。 这与我们通常调用的轴域方法不同。tight_layout方法通过删除多余的空间并确保不同的轴不会重叠来将子图调整为更好的外观。suptitle方法为整个图形创建标题,而set_title轴方法则为单个轴创建标题。 它接受 x 和 y 位置来表示图形坐标系中的位置,其中(0, 0)表示左下,而(1, 1)表示右上。 默认情况下,y 值为 0.98,但我们将其上移了几个点至 1.02。

每个轴域还具有一个坐标系,其中(0, 0)用于左下角,而(1, 1)用于右上角。 除了那些坐标系之外,每个轴还具有一个数据坐标系,这对于大多数人来说更自然,并表示 xy 轴的边界。 这些界限可以分别通过ax.get_xlim()ax.get_ylim()获取。 在此之前的所有绘图均使用数据坐标系。 请参阅“变换教程”以了解有关坐标系的更多信息。

由于两个轴的 y 轴使用相同的单位,因此我们使用图形的text方法使用图形坐标系将自定义 y 轴标签直接放置在每个轴之间。 最后,我们将图形保存到桌面。 路径中的波浪符号~代表主目录,但是savefig方法无法理解这意味着什么。 您必须使用os库中的expanduser函数来创建完整路径。 例如,path变量在我的机器上变为:

>>> os.path.expanduser('~/Desktop/movie_budget.png')
'/Users/Ted/Desktop/movie_budget.png'

savefig方法现在可以在正确的位置创建文件。 默认情况下,savefig将仅保存在图形坐标系的(0, 0))(1, 1)中绘制的内容。 由于我们的标题略微超出该区域,因此其中一些将被裁剪。 将bbox_inches参数设置为yight,matplotlib 将包含扩展到该区域之外的所有标题或标签。

更多

在 1.5 版发布之后,Matplotlib 开始接受其所有绘图函数的 pandas 数据帧。数据帧通过data参数传递给绘图方法。 这样做使您可以引用具有字符串名称的列。 以下脚本创建了从 2000 年开始随机选择的 100 部电影的 IMDB 分数与年份的散点图。 每个点的大小与预算成比例:

>>> cols = ['budget', 'title_year', 'imdb_score', 'movie_title']
>>> m = movie[cols].dropna()
>>> m['budget2'] = m['budget'] / 1e6
>>> np.random.seed(0)
>>> movie_samp = m.query('title_year >= 2000').sample(100)

>>> fig, ax = plt.subplots(figsize=(14,6))
>>> ax.scatter(x='title_year', y='imdb_score',
               s='budget2', data=movie_samp)

>>> idx_min = movie_samp['imdb_score'].idxmin()
>>> idx_max = movie_samp['imdb_score'].idxmax()
>>> for idx, offset in zip([idx_min, idx_max], [.5, -.5]):
        year = movie_samp.loc[idx, 'title_year']
        score = movie_samp.loc[idx, 'imdb_score']
        title = movie_samp.loc[idx, 'movie_title']
        ax.annotate(xy=(year, score), 
        xytext=(year + 1, score + offset), 
        s=title + ' ({})'.format(score),
        ha='center',
        size=16,
        arrowprops=dict(arrowstyle="fancy"))
>>> ax.set_title('IMDB Score by Year', size=25)
>>> ax.grid(True)

创建散点图后,最高得分的电影和最低得分的电影都用annotate方法标记。xy参数是我们要注释的点的元组。xytext参数是文本位置的另一个元组坐标。 由于ha设置为center,因此文本居中。

另见

Pandas 绘图的基础知识

Pandas 通过自动执行许多步骤使绘制过程变得非常容易。 所有 Pandas 绘图均由 matplotlib 内部处理,并通过数据帧或序列的plot方法公开访问。 我们说 Pandasplot方法是围绕 matplotlib 的包装器。 在 Pandas 中创建图时,将返回 matplotlib 轴或图。 您可以使用 matplotlib 的全部函数来修改该对象,直到获得所需的结果。

Pandas 仅能生成 matplotlib 可用的一小部分图,例如线图,条形图,方框图和散点图,以及核密度估计值KDE)和直方图。 Pandas 通过使过程变得非常简单和高效而擅长于其创建的绘图,通常只需要一行代码,从而节省了探索数据的大量时间。

准备

了解 Pandas 绘图的关键之一就是要知道绘图方法是否需要一个或两个变量来进行绘图。 例如,线图和散点图需要两个变量来绘制每个点。 对于条形图也是如此,后者需要一些 x 坐标来定位条形,并需要另一个变量来设置条形的高度。 箱线图,直方图和 KDE 仅使用一个变量进行绘制。

默认情况下,两变量线图和散点图使用索引作为 x 轴,将列的值用作 y 轴。 单变量图忽略索引,并对每个变量应用转换或聚合以制作其图。 在本秘籍中,我们将考察 Pandas 中两变量和一变量绘图之间的差异。

操作步骤

  1. 创建一个具有有意义索引的小型数据帧:
>>> df = pd.DataFrame(index=['Atiya', 'Abbas', 'Cornelia', 
                             'Stephanie', 'Monte'], 
                      data={'Apples':[20, 10, 40, 20, 50],
                            'Oranges':[35, 40, 25, 19, 33]})

  1. 条形图使用 x 轴的标签索引,并将列值用作条形高度。 在kind参数设置为bar的情况下,使用plot方法:
>>> color = ['.2', '.7']
>>> df.plot(kind='bar', color=color, figsize=(16,4))

  1. KDE 图忽略索引,并将每列的值用作 x 轴,并计算 y 值的概率密度:
>>> df.plot(kind='kde', color=color, figsize=(16,4))

  1. 让我们将所有两个变量图一起绘制在一个图中。 散点图是唯一需要您为 x 和 y 值指定列的散点图。 如果希望使用散点图的索引,则必须使用reset_index方法使其成为一列。 其他两个图使用 x 轴的索引,并为每个数字列创建一组新的线/条:
>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16,4))
>>> fig.suptitle('Two Variable Plots', size=20, y=1.02)
>>> df.plot(kind='line', color=color, ax=ax1, title='Line plot')
>>> df.plot(x='Apples', y='Oranges', kind='scatter', color=color, 
            ax=ax2, title='Scatterplot')
>>> df.plot(kind='bar', color=color, ax=ax3, title='Bar plot')

  1. 让我们也将所有一变量图放在同一张图中:
>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16,4))
>>> fig.suptitle('One Variable Plots', size=20, y=1.02)
>>> df.plot(kind='kde', color=color, ax=ax1, title='KDE plot')
>>> df.plot(kind='box', ax=ax2, title='Boxplot')
>>> df.plot(kind='hist', color=color, ax=ax3, title='Histogram')

工作原理

第 1 步创建了一个小的样本数据帧,它将帮助我们说明使用 Pandas 进行的两个变量绘制和一变量绘制之间的差异。 默认情况下,Pandas 将使用数据帧的每个数字列制作一组新的条形,线形,KDE,盒形图或直方图,并在将其作为两变量图时将索引用作 x 值。 散点图是例外之一,必须明确为 x 和 y 值指定一列。

pandas plot方法非常通用,并具有大量参数,可让您根据自己的喜好自定义结果。 例如,您可以设置图形大小,打开和关闭网格线,设置 xy 轴的范围,为图形着色,旋转刻度线,以及更多。

您还可以使用特定 matplotlib 绘图方法可用的任何参数。 多余的参数将由plot方法的**kwds参数收集,并正确传递给基础的 matplotlib 函数。 例如,在第 2 步中,我们创建一个条形图。 这意味着我们可以使用 matplotlib bar函数中可用的所有参数,以及 Pandas plot方法中可用的参数

在第 3 步中,我们创建一个单变量 KDE 图,该图将为数据帧中的每个数字列创建一个密度估计。 步骤 4 将所有两个变量图放置在同一图中。 同样,第 5 步将所有一变量图放置在一起。 第 4 步和第 5 步中的每个步骤都会创建一个具有三个轴对象的图形。 命令plt.subplots(1, 3)创建一个图形,该图形具有分布在一行和三列上的三个轴。 它返回一个由图和包含轴的一维 NumPy 数组组成的两元组。 元组的第一项被解包到变量fig中。 元组的第二个项目被解包为另外三个变量,每个变量一个。 Pandasplot方法方便地带有ax参数,使我们可以将绘图结果放入图中的特定轴中。

更多

除散点图外,所有图均未指定要使用的列。 Pandas 默认使用每一个数字列,并且在使用双变量图的情况下默认使用索引。 当然,您可以指定要用于每个 x 或 y 值的确切列:

>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16,4))
>>> df.sort_values('Apples').plot(x='Apples', y='Oranges', 
                                  kind='line', ax=ax1)
>>> df.plot(x='Apples', y='Oranges', kind='bar', ax=ax2)
>>> df.plot(x='Apples', kind='kde', ax=ax3)

另见

可视化航班数据集

探索性数据分析主要由可视化指导,而 Pandas 为快速,轻松地创建它们提供了一个很好的接口。 开始可视化任何数据集时的一种简单策略是仅关注单变量图。 最受欢迎的单变量图往往是用于分类数据(通常是字符串)的条形图,以及用于连续数据(总是数字)的直方图,箱形图或 KDE。 直接在项目开始时尝试同时分析多个变量可能会很困难。

准备

在本秘籍中,我们通过直接用 Pandas 创建单变量和多变量图来对航班数据集进行一些基本的探索性数据分析。

操作步骤

  1. 读取航班数据集,并输出前五行:
>>> flights = pd.read_csv('data/flights.csv')
>>> flights.head()

  1. 在开始绘制之前,让我们计算转向,取消,延迟和准时飞行的数量。 我们已经有用于转移和取消的二进制列。 只要航班到达时间晚于预定时间 15 分钟或更长时间,便视为航班延误。 让我们创建两个新的二进制列来跟踪延迟到达和准时到达:
>>> flights['DELAYED'] = flights['ARR_DELAY'].ge(15).astype(int)
>>> cols = ['DIVERTED', 'CANCELLED', 'DELAYED']
>>> flights['ON_TIME'] = 1 - flights[cols].any(axis=1)

>>> cols.append('ON_TIME')
>>> status = flights[cols].sum()
>>> status
DIVERTED       137
CANCELLED      881
DELAYED      11685
ON_TIME      45789
dtype: int64
  1. 现在,让我们在同一图上为分类列和连续列绘制几个图:
>>> fig, ax_array = plt.subplots(2, 3, figsize=(18,8))
>>> (ax1, ax2, ax3), (ax4, ax5, ax6) = ax_array
>>> fig.suptitle('2015 US Flights - Univariate Summary', size=20)

>>> ac = flights['AIRLINE'].value_counts()
>>> ac.plot(kind='barh', ax=ax1, title='Airline')

>>> oc = flights['ORG_AIR'].value_counts()
>>> oc.plot(kind='bar', ax=ax2, rot=0, title='Origin City')

>>> dc = flights['DEST_AIR'].value_counts().head(10)
>>> dc.plot(kind='bar', ax=ax3, rot=0, title='Destination City')

>>> status.plot(kind='bar', ax=ax4, rot=0, 
                log=True, title='Flight Status')
>>> flights['DIST'].plot(kind='kde', ax=ax5, xlim=(0, 3000),
                         title='Distance KDE')
>>> flights['ARR_DELAY'].plot(kind='hist', ax=ax6, 
                              title='Arrival Delay',
                              range=(0,200))

  1. 这不是对所有单变量统计信息的详尽研究,但为我们提供了一些变量的详细信息。 在继续进行多变量图绘制之前,让我们绘制出每周的飞行次数。 使用带有 x 轴上日期的时间序列图的正确情况。 不幸的是,我们在任何列中都没有 Pandas 时间戳,但确实有月和日。to_datetime函数有一个巧妙的技巧,可以识别与时间戳组件匹配的列名。 例如,如果您有一个数据帧架,其中的标题栏正好为三列yearmonth,day,,则将该数据帧传递给to_datetime函数将返回时间戳序列。 要准备我们当前的数据帧,我们需要为年份添加一列,并使用计划的出发时间来获取小时和分钟:
>>> hour = flights['SCHED_DEP'] // 100
>>> minute = flights['SCHED_DEP'] % 100
>>> df_date = flights[['MONTH', 'DAY']].assign(YEAR=2015, HOUR=hour,
                                               MINUTE=minute)
>>> df_date.head()

  1. 然后,几乎可以用to_datetime函数将这个数据帧转换为适当的时间戳序列:
>>> flight_dep = pd.to_datetime(df_date)
>>> flight_dep.head()
0   2015-01-01 16:25:00
1   2015-01-01 08:23:00
2   2015-01-01 13:05:00
3   2015-01-01 15:55:00
4   2015-01-01 17:20:00
dtype: datetime64[ns]
  1. 让我们将此结果用作新索引,然后使用resample方法查找每周的航班计数:
>>> flights.index = flight_dep
>>> fc = flights.resample('W').size()
>>> fc.plot(figsize=(12,3), title='Flights per Week', grid=True)

  1. 这个绘图很有启发性。 看来我们没有十月份的数据。 由于缺少这些数据,如果存在趋势,则很难通过视觉分析任何趋势。 前几周和后几周也低于正常水平,可能是因为没有整周的数据。 让我们每周进行一次缺少少于 1,000 个航班的数据。 然后,我们可以使用interpolate方法填写此丢失的数据:
>>> fc_miss = fc.where(fc > 1000)
>>> fc_intp = fc_miss.interpolate(limit_direction='both')

>>> ax = fc_intp.plot(color='black', figsize=(16,4))
>>> fc_intp[fc < 500].plot(linewidth=10, grid=True, 
                           color='.8', ax=ax)

>>> ax.annotate(xy=(.8, .55), xytext=(.8, .77), 
                xycoords='axes fraction', s='missing data', 
                ha='center', size=20, arrowprops=dict())
>>> ax.set_title('Flights per Week (Interpolated Missing Data)')

  1. 让我们改变方向,专注于多变量绘图。 让我们找到以下 10 个机场:
    • 入境航班旅行的平均距离最长
    • 至少有 100 个航班:
>>> flights.groupby('DEST_AIR')['DIST'] \
           .agg(['mean', 'count']) \
           .query('count > 100') \
           .sort_values('mean') \
           .tail(10) \
           .plot(kind='bar', y='mean', rot=0, legend=False,
                 title='Average Distance per Destination')

  1. 头两个目的地机场在夏威夷也就不足为奇了。 现在,让我们通过对 2,000 英里以下的所有航班的距离和通话时间进行散点图来同时分析两个变量:
>>> fs = flights.reset_index(drop=True)[['DIST', 'AIR_TIME']] \
                .query('DIST <= 2000').dropna()
>>> fs.plot(x='DIST', y='AIR_TIME', kind='scatter',
            s=1, figsize=(16,4))

  1. 正如预期的那样,距离和通话时间之间存在紧密的线性关系,尽管方差似乎随着里程数的增加而增加。 有一些航班不在趋势线之外。 让我们尝试识别它们。 可以使用线性回归模型来正式识别它们,但是由于 Pandas 不直接支持线性回归,因此我们将采用更为手动的方法。 让我们使用cut函数将飞行距离分为八组之一:
>>> fs['DIST_GROUP'] = pd.cut(fs['DIST'], bins=range(0, 2001, 250))
>>> fs['DIST_GROUP'].value_counts().sort_index()
(0, 250]         6529
(250, 500]      12631
(500, 750]      11506
(750, 1000]      8832
(1000, 1250]     5071
(1250, 1500]     3198
(1500, 1750]     3885
(1750, 2000]     1815
Name: DIST_GROUP, dtype: int64
  1. 我们将假设每个组中的所有航班应具有相似的飞行时间,因此,如果飞行时间偏离该组平均值,则为每个航班计算标准差的数量:
>>> normalize = lambda x: (x - x.mean()) / x.std()
>>> fs['TIME_SCORE'] = fs.groupby('DIST_GROUP')['AIR_TIME'] \
                         .transform(normalize)
>>> fs.head()

  1. 现在,我们需要一种发现异常值的方法。 箱形图为检测异常值提供了很好的视觉效果。 不幸的是,尝试使用plot方法绘制箱形图时存在一个错误,但是幸运的是,有一种数据帧的boxplot方法可以正常工作:
>>> ax = fs.boxplot(by='DIST_GROUP', column='TIME_SCORE',
                    figsize=(16,4))
>>> ax.set_title('Z-Scores for Distance Groups')
>>> ax.figure.suptitle('')

  1. 让我们任意选择检查距离均值大于六个标准差的点。 因为我们在步骤 9 中重置了fs数据帧中的索引,所以我们可以使用它来标识广告投放数据帧中的每个唯一行。 让我们创建一个仅包含异常值的单独的数据帧:
>>> outliers = flights.iloc[fs[fs['TIME_SCORE'] > 6].index]
>>> outliers = outliers[['AIRLINE','ORG_AIR', 'DEST_AIR', 'AIR_TIME',
                         'DIST', 'ARR_DELAY', 'DIVERTED']]
>>> outliers['PLOT_NUM'] = range(1, len(outliers) + 1)
>>> outliers

  1. 我们可以使用此表从步骤 9 识别出图中的离群值。Pandas 还提供了一种将表附加到图形底部的方法:
>>> ax = fs.plot(x='DIST', y='AIR_TIME', 
                 kind='scatter', s=1, 
                 figsize=(16,4), table=outliers)
>>> outliers.plot(x='DIST', y='AIR_TIME',
                  kind='scatter', s=25, ax=ax, grid=True)

>>> outs = outliers[['AIR_TIME', 'DIST', 'PLOT_NUM']]
>>> for t, d, n in outs.itertuples(index=False):
        ax.text(d + 5, t + 5, str(n))

>>> plt.setp(ax.get_xticklabels(), y=.1)
>>> plt.setp(ax.get_xticklines(), visible=False)
>>> ax.set_xlabel('')
>>> ax.set_title('Flight Time vs Distance with Outliers')

工作原理

在读取了步骤 1 中的数据并计算了延迟和按时航班的列之后,我们就可以开始制作单变量图了。 在第 3 步中对subplots函数的调用将创建一个大小相等的2 x 3轴网格。 我们将每个轴解压缩到其自己的变量中以进行引用。 对plot方法的每个调用都使用ax参数引用图中的特定轴。value_counts方法用于创建三个序列,这些序列构成了第一行中的绘图。rot参数将刻度标签旋转到给定角度。

左下角的绘图使用 y 轴的对数标度,因为准时航班的数量大约比取消航班的数量大两个数量级。 没有对数刻度,将很难看到左侧的两个条形图。 默认情况下,KDE 图可能会为不可能的值生成正数区域,例如底行中的负数英里。 因此,我们使用xlim参数限制 x 值的范围。

在到达延迟时,在右下角创建的直方图已传递range参数。 这不是 Pandas plot方法的方法签名的直接部分。 相反,此参数由**kwds参数收集,然后传递给 matplotlib hist函数。 在这种情况下,使用xlim不能如上图所示那样工作。可以仅裁剪图而不必重新计算图的该部分的新桶的宽度。 但是,range参数不仅限制了 x 轴,而且仅计算了该范围的箱宽。

第 4 步创建一个特殊的额外数据帧来容纳仅包含日期时间组件的列,以便我们可以在第 5 步中使用to_datetime函数将每一行立即转换为时间戳。resample方法默认情况下,基于传递的日期偏移量使用索引来形成组。 我们以序列返回每周航班数(W),然后在其上调用plot方法,该方法很好地将索引的格式设置为 x 轴。 十月份出现了一个明显的漏洞。

为了填补这个漏洞,我们使用where方法在步骤 7 的第一行中仅将小于 1,000 的值设置为丢失。然后,我们通过线性插值法填充丢失的数据。 默认情况下,interpolate方法仅在正向插值,因此,在数据帧开头的所有丢失值都将保留。 通过将limit_direction参数设置为both,我们确保没有缺失值。 绘制现在存储在fc_intp中的新数据。 为了更清楚地显示缺少的数据,我们选择原始数据中缺少的点,并在前一条线上方的相同轴上绘制线图。 通常,当我们注解绘图时,我们可以使用数据坐标,但是在这种情况下, x 轴的坐标是什么并不明显。 要使用轴坐标系(范围从(0, 0)(1, 1)的坐标系),请将xycoords参数设置为axes fraction。 现在,此新图将错误数据排除在外,这使得发现趋势变得容易得多。 夏季的空中交通流量比一年中其他任何时候都要多。

在第 8 步中,我们使用一长串方法对每个目标机场进行分组,并将meancount两个函数应用于距离列。query方法在方法链中使用时特别好,因为它可以清晰,简洁地选择给定条件的所需数据行。 进入plot方法时,数据帧中有两列,默认情况下,该方法将为每一列绘制条形图。 我们对count列不感兴趣,因此仅选择mean列来形成条形。 此外,在使用数据帧进行打印时,每个列名称都会出现在图例中。 这会将mean一词放在图例中,因此没有用,因此我们通过将legend参数设置为False将其删除。

步骤 9 通过查看行进距离与飞行时间之间的关系来开始新的分析。 由于点的数量众多,我们使用s参数缩小了它们的大小。 为了找到平均需要更长的时间到达目的地的航班,我们在步骤 10 中将每个航班分组为 250 英里,并在步骤 11 中找到与其组平均值的标准差数量。

在步骤 12 中,为by参数的每个唯一值在相同的轴中创建一个新的箱形图。 我们通过在调用boxplot之后将其保存到变量中来捕获轴域对象。 此方法会在图形上方创建不必要的标题,方法是先访问图形然后将suptitle设置为空字符串,然后将其删除。

在第 13 步中,当前数据帧fs包含我们找到最慢航班所需的信息,但它不具备我们可能需要进一步研究的所有原始数据。 因为我们在步骤 9 中重置了fs的索引,所以我们可以使用它来标识与原始行相同的行。 此步骤的第一行为我们做到了这一点。 我们还为每个异常行提供一个唯一的整数,以便以后在绘制时进行标识。

在第 14 步中,我们从与第 9 步中相同的散点图开始,但是使用table参数将离群值表附加到该图的底部。 然后,我们将离群值直接作为散点图绘制在顶部,并确保它们的点较大以轻松识别它们。itertuples方法循环遍历每个数据帧的行,并以元组的形式返回其值。 我们为绘图解压缩相应的 x 和 y 值,并用我们分配给它的编号标记它。

由于工作台直接放置在绘图的下方,因此会干扰 x 轴上的绘图对象。 我们将刻度线标签移动到轴的内部,并删除刻度线和轴标签。 该表向对这些外围事件感兴趣的任何人提供了一些不错的信息。

另见

堆叠面积图以发现新兴趋势

堆积面积图是发现新兴趋势的绝佳可视化工具,尤其是在市场中。 通常会显示诸如互联网浏览器,手机或车辆之类的产品的市场份额百分比。

准备

在本秘籍中,我们将使用从受欢迎的网站 metup.com 收集的数据。 使用堆叠的面积图,我们将显示五个与数据科学相关的聚会组之间的成员分布。

操作步骤

  1. 读取聚会组数据集,将join_date列转换为时间戳,将其放置在索引中,然后输出前五行:
>>> meetup = pd.read_csv('data/meetup_groups.csv', 
                          parse_dates=['join_date'], 
                          index_col='join_date')
>>> meetup.head()

  1. 让我们获取每周加入每个组的人数:
>>> group_count = meetup.groupby([pd.Grouper(freq='W'), 'group']) \
                        .size()
>>> group_count.head()
join_date   group   
2010-11-07  houstonr     5
2010-11-14  houstonr    11
2010-11-21  houstonr     2
2010-12-05  houstonr     1
2011-01-16  houstonr     2
dtype: int64
  1. 取消堆叠组级别,以便每个聚会组都有自己的数据列:
>>> gc2 = group_count.unstack('group', fill_value=0)
>>> gc2.tail()

  1. 此数据代表加入该特定星期的成员数量。 让我们取每一列的累加总和来获得成员的总数:
>>> group_total = gc2.cumsum()
>>> group_total.tail()

  1. 许多堆叠的面积图使用总数的百分比,因此每一行总是相加 100% 。 让我们将每一行除以总行数以得出该百分比:
>>> row_total = group_total.sum(axis='columns')
>>> group_cum_pct = group_total.div(row_total, axis='index')
>>> group_cum_pct.tail()

  1. 现在,我们可以创建堆积面积图,该图将不断累积列,一个列位于另一个列之上:
>>> ax = group_cum_pct.plot(kind='area', figsize=(18,4),
                            cmap='Greys', xlim=('2013-6', None), 
                            ylim=(0, 1), legend=False)
>>> ax.figure.suptitle('Houston Meetup Groups', size=25)
>>> ax.set_xlabel('')
>>> ax.yaxis.tick_right()

>>> plot_kwargs = dict(xycoords='axes fraction', size=15)
>>> ax.annotate(xy=(.1, .7), s='R Users', 
                color='w', **plot_kwargs)
>>> ax.annotate(xy=(.25, .16), s='Data Visualization', 
                color='k', **plot_kwargs)
>>> ax.annotate(xy=(.5, .55), s='Energy Data Science', 
                color='k', **plot_kwargs)
>>> ax.annotate(xy=(.83, .07), s='Data Science',
                color='k', **plot_kwargs)
>>> ax.annotate(xy=(.86, .78), s='Machine Learning',
                color='w', **plot_kwargs)

工作原理

我们的目标是确定休斯敦随时间推移在五个最大的数据科学聚会小组中的成员分布。 为此,我们需要找到自每个小组开始以来的每个时间点的成员总数。 我们有每个人加入每个小组的确切日期和时间。 在第 2 步中,我们按每周分组(偏移别名W)和聚会组,并使用size方法返回该周的签约数量。

所得的序列不适合与 Pandas 作图。 每个聚会组都需要自己的列,因此我们将group索引级别重塑为列。 我们将fill_value选项设置为零,以便在特定星期内没有成员资格的组不会缺少任何值。

我们需要每周的成员总数。 步骤 4 中的cumsum方法为我们提供了此功能。 我们可以在此步骤之后直接创建堆积面积图,这将是可视化原始总成员资格的好方法。 在第 5 步中,通过将每个值除以其行总数,可以找到每个组在所有组中占总数的百分比。 默认情况下,Pandas 会自动按对象的列对齐对象,因此我们不能使用除法运算符。 相反,我们必须使用div方法将对齐轴更改为索引

现在,该数据非常适合我们在步骤 6 中创建的堆积面积图。请注意,pandas 允许您使用日期时间字符串设置轴限制。 如果使用ax.set_xlim方法直接在 matplotlib 中完成此操作将不起作用。 该绘图的开始日期提前了几年,因为休斯顿 R 用户组的成立要早于其他任何组。

更多

尽管数据可视化专家通常对此并不满意,但 Pandas 可以创建饼图。 在这种情况下,我们使用它们来查看整个组随时间分布的快照。 首先,从数据收集结束前的 18 个月开始,每三个月选择一次数据。 我们使用asfreq方法,该方法仅适用于索引中具有日期时间值的数据帧。 偏移别名3MS用于表示每三个月的开始。 由于group_cum_pct是按周汇总的,因此并非总是存在月份的第一天。 我们将method参数设置为bfill,代表回填; 它将及时查看以查找其中包含数据的月份的第一天。 然后,我们使用to_period方法(也仅适用于索引中的日期时间)将索引中的值更改为 Pandas 时间段。 最后,我们对数据进行转置,以便每一列代表该月聚会组中成员的分布:

>>> pie_data = group_cum_pct.asfreq('3MS', method='bfill') \
                            .tail(6).to_period('M').T
>>> pie_data

从这里,我们可以使用plot方法创建饼图:

>>> from matplotlib.cm import Greys
>>> greys = Greys(np.arange(50,250,40))

>>> ax_array = pie_data.plot(kind='pie', subplots=True, 
                             layout=(2,3), labels=None,
                             autopct='%1.0f%%', pctdistance=1.22,
                             colors=greys)
>>> ax1 = ax_array[0, 0]
>>> ax1.figure.legend(ax1.patches, pie_data.index, ncol=3)
>>> for ax in ax_array.flatten():
        ax.xaxis.label.set_visible(True)
        ax.set_xlabel(ax.get_ylabel())
        ax.set_ylabel('')
>>> ax1.figure.subplots_adjust(hspace=.3)

了解 Pandas 与 Seaborn 的区别

在 Pandas 之外,Seaborn 是 Python 数据科学社区中创建可视化效果最广泛的库之一。 像 Pandas 一样,它本身不会进行任何实际的绘制,并且完全依赖于 matplotlib 进行繁重的工作。 Seaborn 绘图函数直接与 pandas 数据帧配合使用,以创建美观的可视化效果。

尽管 seaborn 和 panda 都减少了 matplotlib 的开销,但它们处理数据的方式却完全不同。 几乎所有的 Seaborn 绘图函数都需要整齐(或长)的数据。 当数据采用整齐的格式时,只有将某些函数应用到结果上后,才能准备使用或解释数据。 整洁的数据是使所有其他分析成为可能的原始构建块。 在数据分析过程中处理整洁的数据通常会创建聚合的数据或广泛的数据。 Pandas 使用这种格式的数据进行绘图。

准备

在此秘籍中,我们将使用 seaborn 和 matplotlib 构建相似的图,以明确表明它们接受整齐的数据还是广泛的数据。

操作步骤

  1. 读入员工数据集,并输出前五行:
>>> employee = pd.read_csv('data/employee.csv', 
                           parse_dates=['HIRE_DATE', 'JOB_DATE'])
>>> employee.head()

  1. 导入 seaborn 库,并为其命名为sns
>>> import seaborn as sns
  1. 让我们制作一个条形图,显示每个部门的 Seaborn:
>>> sns.countplot(y='DEPARTMENT', data=employee)

  1. 要使用 Pandas 重现该绘图,我们需要预先汇总数据:
>>> employee['DEPARTMENT'].value_counts().plot('barh')

  1. 现在,让我们使用 seaborn 找到每个种族的平均工资:
>>> ax = sns.barplot(x='RACE', y='BASE_SALARY', data=employee)
>>> ax.figure.set_size_inches(16, 4)

  1. 要用 Pandas 复制它,我们将需要按种族分组:
>>> avg_sal = employee.groupby('RACE', sort=False) \
                      ['BASE_SALARY'].mean()
>>> ax = avg_sal.plot(kind='bar', rot=0, figsize=(16,4), width=.8)
>>> ax.set_xlim(-.5, 5.5)
>>> ax.set_ylabel('Mean Salary')

  1. Seaborn 在大多数绘图函数中,还可以通过第三个变量hue来区分数据中的组。 让我们按种族和性别找到平均工资:
>>> ax = sns.barplot(x='RACE', y='BASE_SALARY', hue='GENDER', 
                     data=employee, palette='Greys')
>>> ax.figure.set_size_inches(16,4)

  1. 对于 Pandas,我们将必须按种族和性别进行分组,然后将性别作为列名称拆开:
>>> employee.groupby(['RACE', 'GENDER'], sort=False) \
            ['BASE_SALARY'].mean().unstack('GENDER') \
            .plot(kind='bar', figsize=(16,4), rot=0,
                  width=.8, cmap='Greys')

  1. 箱形图是 Seaborn 和 Pandas 共同的另一种图。 让我们使用 Seaborn 创建一个按种族和性别划分的薪金箱形图:
>>> sns.boxplot(x='GENDER', y='BASE_SALARY', data=employee,
                hue='RACE', palette='Greys')
>>> ax.figure.set_size_inches(14,4)

  1. Pandas 不容易为该箱形图产生精确的复制。 它可以为性别创建两个单独的轴,然后按种族绘制薪水箱形图:
>>> fig, ax_array = plt.subplots(1, 2, figsize=(14,4), sharey=True)
>>> for g, ax in zip(['Female', 'Male'], ax_array):
        employee.query('GENDER== @g') \
                .boxplot(by='RACE', column='BASE_SALARY',
                         ax=ax, rot=20)
        ax.set_title(g + ' Salary')
        ax.set_xlabel('')
>>> fig.suptitle('')

工作原理

在步骤 2 中导入 seaborn 会更改 matplotlib 的许多默认属性。 在类似字典的对象plt.rcParams中可以访问大约 300 个默认绘图参数。 要恢复 matplotlib 的默认设置,请不带任何参数调用plt.rcdefaults函数。 导入 seaborn 时,Pandas 绘图的样式也会受到影响。 我们的员工数据集满足了整洁数据的要求,因此非常适合用于几乎所有 Seaborn 的绘图函数。

Seaborn 将进行所有汇总; 您只需将数据帧提供给,,data,参数,并使用其字符串名称引用这些列。 例如,在步骤 3 中,countplot函数毫不费力地对DEPARTMENT的每次出现进行计数,以创建条形图。 所有 Seaborn 绘图函数均具有xy参数。 我们可以使用x而不是y绘制垂直条形图。 Pandas 会迫使您做更多的工作来获得相同的绘图。 在第 4 步中,我们必须使用value_counts方法预先计算垃圾箱的高度。

Seaborn 可以使用barplot函数进行更复杂的聚合,如步骤 5 和 7 所示。hue参数进一步在 x 轴上拆分每个组。 通过在步骤 6 和 8 中对xhue变量进行分组,Pandas 能够几乎复制这些图。

箱形图可在海生和 Pandas 中使用,并且可以直接用整洁的数据绘制,而无需任何汇总。 即使没有必要进行聚合,seaborn 仍然具有优势,因为它可以使用hue参数将数据整齐地拆分为单独的组。 如步骤 10 所示,Pandas 无法轻松地从 Seaborn 中复制此功能。每个组都需要使用query方法进行拆分,并绘制在其自己的轴上。 实际上,Pandas 可能会拆分多个变量,从而将列表传递给by参数,但结果却不尽人意:

>>> ax = employee.boxplot(by=['GENDER', 'RACE'], 
                      column='BASE_SALARY', 
                      figsize=(16,4), rot=15)
>>> ax.figure.suptitle('')

另见

使用 Seaborn 网格进行多元分析

要进一步了解 seaborn,了解作为海生网格返回多个轴的函数与返回单个轴的函数之间的层次结构会有所帮助:

网格类型网格函数轴函数变量类型
FacetGridfactorplotstripplotswarmplotboxplotviolinplotlvplotpointplotbarplotcountplot类别
FacetGridlmplotregplot连续
PairGridpairplotregplotdistplotkdeplot连续
JointGridjointplotregplotkdeplotresidplot连续
ClusterGridclustermapheatmap连续

Seaborn 轴函数可以全部独立调用以生成单个图。 在大多数情况下,网格函数使用轴函数来构建网格。 从网格函数返回的最终对象是网格类型,其中有四种不同的类型。 高级用例需要直接使用网格类型,但是在绝大多数情况下,您将调用基础网格函数来生成实际的网格而不是构造器本身。

准备

在本秘籍中,我们将研究性别和种族之间的经验年限与薪水之间的关系。 我们将首先使用 Seaborn 轴函数创建一个简单的回归图,然后使用网格函数为该图添加更多尺寸。

操作步骤

  1. 阅读员工数据集,并创建一个具有多年经验的列:
>>> employee = pd.read_csv('data/employee.csv', 
                       parse_dates=['HIRE_DATE', 'JOB_DATE'])
>>> days_hired = pd.to_datetime('12-1-2016') - employee['HIRE_DATE']

>>> one_year = pd.Timedelta(1, unit='Y')
>>> employee['YEARS_EXPERIENCE'] = days_hired / one_year
>>> employee[['HIRE_DATE', 'YEARS_EXPERIENCE']].head()

  1. 让我们用拟合的回归线创建一个基本的散点图,以表示经验年限和薪水之间的关系:
>>> ax = sns.regplot(x='YEARS_EXPERIENCE', y='BASE_SALARY',
                     data=employee)
>>> ax.figure.set_size_inches(14,4)

  1. regplot函数无法为第三个变量的不同级别绘制多条回归线。 让我们使用其父函数lmplot绘制一个 Seaborn 网格,为男性和女性添加相同的回归线:
>>> g = sns.lmplot('YEARS_EXPERIENCE', 'BASE_SALARY',
                    hue='GENDER', palette='Greys',
                    scatter_kws={'s':10}, data=employee)
>>> g.fig.set_size_inches(14, 4)
>>> type(g)
seaborn.axisgrid.FacetGrid

  1. Seaborn 网格函数的真正功能是它们能够基于另一个变量添加更多轴。 每个 Seaborn 网格都有colrow参数,可用于将数据进一步分为不同的组。 例如,我们可以为数据集中的每个唯一种族创建一个单独的图,并且仍然按性别拟合回归线:
>>> grid = sns.lmplot(x='YEARS_EXPERIENCE', y='BASE_SALARY',
                      hue='GENDER', col='RACE', col_wrap=3,
                      palette='Greys', sharex=False,
                      line_kws = {'linewidth':5},
                      data=employee)
>>> grid.set(ylim=(20000, 120000))

工作原理

在步骤 1 中,我们使用 Pandas 日期函数创建了另一个连续变量。 该数据是 2016 年 12 月 1 日从休斯敦市收集的。我们使用该日期来确定每个员工在该市工作了多长时间。 当减去日期时(如第二行代码所示),我们将返回一个时间增量对象,其最大单位为天。 我们可以简单地将该数字除以 365 来计算经验年限。 取而代之的是,我们使用Timedelta(1, unit='Y')进行更精确的测量,如果您在家数的话,恰好是 365 天,5 小时,42 分钟和 19 秒。

第 2 步使用 seaborn 轴函数regplot来创建带有估计回归线的散点图。 它返回一个轴,我们使用它来更改图形的大小。 为了为每种性别创建两条单独的回归线,我们必须使用其父函数lmplot。 它包含hue参数,该参数为该变量的每个唯一值创建一个新的回归线。 在第 3 步结束时,我们验证lmplot确实确实返回了 Seaborn 网格对象。

Seaborn 网格本质上是整个图形的包装,并提供了一些方便的方法来更改其元素。 所有 Seaborn 网格都可以使用其fig属性访问基础图形。 步骤 4 显示了 Seaborn 网格函数的常见用例,该用例是基于第三个甚至第四个变量创建多个图。 我们将col参数设置为RACE。 为数据集中的六个独特种族中的每个种族创建了六个回归图。 通常,这将返回由 1 行和 6 列组成的网格,但是我们使用col_wrap参数将列数限制为 3。

还有更多可用参数来控制网格的大多数重要方面。 可以从基础线和散点图 matplotlib 函数更改使用参数。 为此,请将scatter_kwsline_kws参数设置为等于具有 matplotlib 参数作为字符串的字典的字典,该字符串与您想要的值配对。

更多

具有分类特征时,我们可以进行类似类型的分析。 首先,让我们将类别变量racedepartment中的级别数分别减少到最常见的前两个和前三个:

>>> deps = employee['DEPARTMENT'].value_counts().index[:2]
>>> races = employee['RACE'].value_counts().index[:3]
>>> is_dep = employee['DEPARTMENT'].isin(deps)
>>> is_race = employee['RACE'].isin(races)
>>> emp2 = employee[is_dep & is_race].copy()
>>> emp2['DEPARTMENT'] = emp2['DEPARTMENT'].str.extract('(HPD|HFD)',
                                                        expand=True)
>>> emp2.shape
(968, 11)

>>> emp2['DEPARTMENT'].value_counts()
HPD    591
HFD    377
Name: DEPARTMENT, dtype: int64

>>> emp2['RACE'].value_counts()
White                        478
Hispanic/Latino              250
Black or African American    240
Name: RACE, dtype: int64

让我们使用一种更简单的轴级函数,例如小提琴图来查看按性别划分的多年工作经验分布:

>>> common_depts = employee.groupby('DEPARTMENT') \
                       .filter(lambda x: len(x) > 50)
>>> ax = sns.violinplot(x='YEARS_EXPERIENCE', y='GENDER',
                        data=common_depts)
>>> ax.figure.set_size_inches(10,4)

然后,我们可以使用网格函数factorplot为带有colrow参数的部门和种族的每个独特组合添加小提琴图:

>>> sns.factorplot(x='YEARS_EXPERIENCE', y='GENDER',
                   col='RACE', row='DEPARTMENT', 
                   size=3, aspect=2,
                   data=emp2, kind='violin')

从秘籍的开头查看表格。factorplot函数必须使用这八个固定轴函数之一。 为此,您可以将其名称作为字符串传递给kind参数。

在 Seaborn 钻石数据集中发现辛普森悖论

不幸的是,在进行数据分析时,报告错误的结果非常容易。 辛普森悖论是一种可以在数据分析中出现的较普遍现象。 当汇总所有数据时,当一组显示的结果高于另一组时,会发生这种情况,但是在将数据细分为不同的细分时,则显示相反的结果。 例如,假设我们有两个学生 A 和 B,他们分别接受了 100 个问题的测试。 学生 A 回答了 50% 正确的问题,而学生 B 回答了 80% 正确的问题。 这显然表明学生 B 具有更高的才能:

假设这两个测试非常不同。 学生 A 的测试包含 95 个困难的问题,只有五个容易解决的问题。 学生 B 接受了完全相反比例的测试。

这描绘了一个完全不同的画面。 现在,学生 A 在困难和容易解决的问题中所占的比例较高,但总体上所占的比例却低得多。 这是辛普森悖论的典型例子。 汇总的整体显示了每个单独部分的相反情况。

准备

在此秘籍中,我们将首先得出一个令人困惑的结果,该结果似乎表明,高品质的钻石比低品质的钻石有价值。 我们通过对数据进行更细粒度的瞥见来揭示辛普森的悖论,这表明事实恰恰相反。

操作步骤

  1. 读入钻石数据集,并输出前五行:
>>> diamonds = pd.read_csv('data/diamonds.csv')
>>> diamonds.head()

  1. 在开始分析之前,让我们将cutcolorclarity列更改为有序的分类变量:
>>> cut_cats = ['Fair', 'Good', 'Very Good', 'Premium', 'Ideal']
>>> color_cats = ['J', 'I', 'H', 'G', 'F', 'E', 'D']
>>> clarity_cats = ['I1', 'SI2', 'SI1', 'VS2',
                    'VS1', 'VVS2', 'VVS1', 'IF']
>>> diamonds['cut'] = pd.Categorical(diamonds['cut'],
                                     categories=cut_cats, 
                                     ordered=True)

>>> diamonds['color'] = pd.Categorical(diamonds['color'],
                                       categories=color_cats, 
                                       ordered=True)

>>> diamonds['clarity'] = pd.Categorical(diamonds['clarity'],
                                         categories=clarity_cats, 
                                         ordered=True)
  1. Seaborn 对其绘图使用类别顺序。 让我们用条形图来表示每个级别的切割,颜色和清晰度的平均价格:
>>> import seaborn as sns
>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(14,4))
>>> sns.barplot(x='color', y='price', data=diamonds, ax=ax1)
>>> sns.barplot(x='cut', y='price', data=diamonds, ax=ax2)
>>> sns.barplot(x='clarity', y='price', data=diamonds, ax=ax3)
>>> fig.suptitle('Price Decreasing with Increasing Quality?') 

  1. 颜色和价格似乎呈下降趋势。 最高质量的切割和净度水平也价格低廉。 怎么会这样? 让我们更深入地挖掘并再次绘制每种钻石颜色的价格,但为每个透明度级别绘制一个新图:
>>> sns.factorplot(x='color', y='price', col='clarity',
                   col_wrap=4, data=diamonds, kind='bar')

  1. 这个绘图更具启发性。 尽管价格似乎随着颜色质量的提高而下降,但是当清晰度达到最高水平时,价格不会下降。 价格实际上有大幅上涨。 我们还没有仅仅关注钻石的价格,而没有关注其尺寸。 让我们从步骤 3 重新创建图,但使用克拉大小代替价格:
>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(14,4))
>>> sns.barplot(x='color', y='carat', data=diamonds, ax=ax1)
>>> sns.barplot(x='cut', y='carat', data=diamonds, ax=ax2)
>>> sns.barplot(x='clarity', y='carat', data=diamonds, ax=ax3)
>>> fig.suptitle('Diamond size decreases with quality')

  1. 现在我们的故事开始变得更有意义了。 高质量的钻石似乎尺寸较小,这在直觉上是有道理的。 让我们创建一个新变量,将carat值分为五个不同的部分,然后创建一个点图。 准确地显示出以下事实表明,按尺寸细分质量更高的钻石确实会花费更多金钱:
>>> diamonds['carat_category'] = pd.qcut(diamonds.carat, 5)

>>> from matplotlib.cm import Greys
>>> greys = Greys(np.arange(50,250,40))

>>> g = sns.factorplot(x='clarity', y='price', data=diamonds,
                       hue='carat_category', col='color', 
                       col_wrap=4, kind='point', palette=greys)
>>> g.fig.suptitle('Diamond price by size, color and clarity',
                   y=1.02, size=20)

工作原理

在此秘籍中,创建分类列非常重要,因为可以对它们进行排序。 Seaborn 使用此顺序将标签放置在绘图上。 第 3 步和第 4 步显示了明显增加钻石质量的下降趋势。 这就是辛普森悖论成为中心焦点的地方。 整体的汇总结果与其他尚未检查的变量混淆。

揭示这一矛盾的关键在于关注克拉的大小。 第 5 步向我们揭示克拉的大小也随着质量的增加而减小。 考虑到这一事实,我们使用qcut函数将钻石尺寸切成五个相等大小的容器。 默认情况下,此函数根据给定的分位数将变量分为离散类别。 通过像此步骤一样将整数传递给它,可以创建等距的分位数。 您还可以选择将显式非规则分位数的序列传递给它。

使用此新变量,我们可以绘制第 6 步中每组每颗钻石尺寸的平均价格的图。seaborn 中的点图将创建一个连接每个类别均值的线图。 每个点的竖线是该组的标准差。 该图证实,只要我们将克拉的大小保持不变,钻石的确会随着质量的提高而变得更加昂贵。

更多

步骤 3 和 5 中的条形图可以使用更高级的 seaborn PairGrid构造器创建的,该构造器可以绘制双变量关系。 使用PairGrid分为两个步骤。 对PairGrid的第一次调用通过提醒网格哪些变量将为 x 和哪些变量将为 y 来准备网格。 第二步将图应用于 x 和 y 列的所有组合:

>>> g = sns.PairGrid(diamonds,size=5,
                 x_vars=["color", "cut", "clarity"],
                 y_vars=["price"])
>>> g.map(sns.barplot)
>>> g.fig.suptitle('Replication of Step 3 with PairGrid', y=1.02)