Skip to content

Latest commit

 

History

History
1019 lines (728 loc) · 49.2 KB

File metadata and controls

1019 lines (728 loc) · 49.2 KB

九、组合 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)

另见