# 组合 Pandas 对象 在本章中,我们将介绍以下主题: * 将新行追加到 DataFrames * 将多个数据帧连接在一起 * 比较特朗普总统和奥巴马总统的支持率 * 了解`concat`,`join`和`merge`之间的区别 * 连接到 SQL 数据库 # 介绍 可以使用多种选项将两个或多个数据帧或序列组合在一起。 `append`方法最不灵活,仅允许将新行附加到数据帧。 `concat`方法非常通用,可以在任一轴上组合任意数量的数据帧或序列。 `join`方法通过将一个数据帧的列与其他数据帧的索引对齐来提供快速查找。 `merge`方法提供了类似 SQL 的功能,可以将两个数据帧结合在一起。 # 将新行追加到 DataFrames 在执行数据分析时,创建新列比创建新行更为常见。 这是因为新的数据行通常代表新的观察结果,而作为分析人员,连续捕获新数据通常不是您的工作。 数据捕获通常留给其他平台,如关系数据库管理系统。 但是,这是一个必不可少的功能,因为它会不时出现。 # 准备 在本秘籍中,我们将首先使用`.loc`索引器将行追加到小型数据集,然后过渡到使用`append`方法。 # 操作步骤 1. 读入名称数据集,并将其输出: ```py >>> names = pd.read_csv('data/names.csv') >>> names ``` ![](img/00215.jpeg) 2. 让我们创建一个包含一些新数据的列表,并使用`.loc`索引器设置一个等于该新数据的行标签: ```py >>> new_data_list = ['Aria', 1] >>> names.loc[4] = new_data_list >>> names ``` ![](img/00216.jpeg) 3. `.loc`索引器使用标签来引用行。 在这种情况下,行标签与整数位置完全匹配。 可以使用非整数标签附加更多行: ```py >>> names.loc['five'] = ['Zach', 3] >>> names ``` ![](img/00217.jpeg) 4. 为了更明确地将变量与值相关联,可以使用字典。 同样,在这一步中,我们可以动态选择新的索引标签作为数据帧的长度: ```py >>> names.loc[len(names)] = {'Name':'Zayd', 'Age':2} >>> names ``` ![](img/00218.jpeg) 5. 系列还可以保存新数据,并且与字典完全相同: ```py >>> names.loc[len(names)] = pd.Series({'Age':32, 'Name':'Dean'}) >>> names ``` ![](img/00219.jpeg) 6. 前面的操作全部使用`.loc`索引运算符就地更改`names` DataFrame。 没有返回的数据帧的单独副本。 在接下来的几个步骤中,我们将研究`append`方法,该方法不会修改调用数据帧的方法。 而是返回带有附加行的数据帧的新副本。 让我们从原始的`names` DataFrame 开始,并尝试追加一行。 `append`的第一个参数必须是另一个数据帧,序列,dictionary 或它们的列表,但不能是步骤 2 中的列表。让我们看看当尝试将字典与`append`一起使用时会发生什么: ```py >>> 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 ``` 7. 此错误消息似乎有点不正确。 我们正在传递一个数据帧而不是一个序列,但是它为我们提供了如何更正它的说明: ```py >>> names.append({'Name':'Aria', 'Age':1}, ignore_index=True) ``` ![](img/00216.jpeg) 8. 这有效,但是`ignore_index`是一个偷偷摸摸的参数。 当设置为`True`时,旧索引将被完全删除并替换为 0 至 n-1 之间的`RangeIndex`。 例如,让我们为`names` DataFrame 指定一个索引: ```py >>> names.index = ['Canada', 'Canada', 'USA', 'USA'] >>> names ``` ![](img/00220.jpeg) 9. 重新运行步骤 7 中的代码,您将获得相同的结果。 原始索引被完全忽略。 10. 让我们继续使用在索引中包含这些国家/地区字符串的`names`数据集,并通过`append`方法使用具有`name`属性的系列: ```py >>> s = pd.Series({'Name': 'Zach', 'Age': 3}, name=len(names)) >>> s Age 3 Name Zach Name: 4, dtype: object >>> names.append(s) ``` ![](img/00221.jpeg) 11. `append`方法比`.loc`索引器更灵活。 它支持同时添加多行。 实现此目的的一种方法是使用一系列列表: ```py >>> s1 = pd.Series({'Name': 'Zach', 'Age': 3}, name=len(names)) >>> s2 = pd.Series({'Name': 'Zayd', 'Age': 2}, name='USA') >>> names.append([s1, s2]) ``` ![](img/00222.jpeg) 12. 仅具有两列的 Small DataFrame 非常简单,可以手动写出所有列名称和值。 当它们变大时,此过程将非常痛苦。 例如,让我们看一下 2016 年棒球数据集: ```py >>> bball_16 = pd.read_csv('data/baseball16.csv') >>> bball_16.head() ``` ![](img/00223.jpeg) 13. 该数据集包含 22 列,如果您手动输入新的数据行,则很容易输错列名称或完全忘记其中的一个。 为了帮助避免这些错误,让我们选择一行作为“系列”,并将`to_dict`方法链接到该行,以获取示例行作为字典: ```py >>> 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} ``` 14. 用字典理解清除旧值,将任何先前的字符串值分配为空字符串,将所有其他字符串值分配为缺失值。 现在,该词典可以用作您要输入的任何新数据的模板: ```py >>> 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` DataFrame 本身。 如果以前存在标签等于整数 4 的行,则该命令将覆盖该行。 与`append`方法相比,就地进行此修改使此索引运算符的使用风险更高,该方法从未修改原始调用数据帧。 任何有效的标签都可以与`.loc`索引运算符一起使用,如步骤 3 所示。不管实际的新标签值是多少,新行始终将附加在最后。 即使使用列表分配也可以,但为清楚起见,最好使用字典,以便我们准确地知道与每个值关联的列,如步骤 4 所示。 步骤 5 显示了一个小技巧,可以动态地将新标签设置为数据帧中的当前行数。 只要索引标签与列名匹配,存储在序列中的数据也将得到正确分配。 其余步骤使用`append`方法,这是一种仅将新行追加到数据帧的简单方法。 大多数数据帧方法都允许通过`axis`参数进行行和列操作。 `append`是一个例外,它只能将行追加到数据帧。 如步骤 6 中的错误消息所示,使用映射到值的列名字典不足以进行追加操作,如步骤 6 中的错误消息所示。要正确地追加没有行名的字典,您必须将`ignore_index`参数设置为 `True`。 步骤 10 向您展示如何通过简单地将字典转换为系列来保持旧索引。 确保使用`name`参数,该参数随后将用作新的索引标签。 通过将序列列表作为第一个参数传递,可以用 append 方式添加任意数量的行。 当想要以更大的数据帧以这种方式附加行时,可以通过使用`to_dict`方法将单行转换为字典,然后使用字典理解来清除所有旧值,从而避免大量键入和错误 他们有一些默认值。 # 更多 将单行添加到数据帧是相当昂贵的操作,如果您发现自己编写了将单行数据附加到数据帧的循环,那么您做错了。 让我们首先创建 1,000 行新数据作为序列列表: ```py >>> 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 ``` 让我们花时间遍历每个项目一次添加一个附件需要花费多长时间: ```py >>> %%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 排就花了将近五秒钟。 如果我们改为通过整个系列列表,则速度会大大提高: ```py >>> %%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 年的股票数据集,并将其股票代码作为索引: ```py >>> stocks_2016 = pd.read_csv('data/stocks_2016.csv', index_col='Symbol') >>> stocks_2017 = pd.read_csv('data/stocks_2017.csv', index_col='Symbol') ``` ![](img/00224.jpeg) ![](img/00225.jpeg) 2. 将所有`stock`数据集放在一个列表中,然后调用`concat`函数将它们连接在一起: ```py >>> s_list = [stocks_2016, stocks_2017] >>> pd.concat(s_list) ``` ![](img/00226.jpeg) 3. 默认情况下,`concat`函数垂直连接数据帧,一个接一个。 前面的数据帧的一个问题是无法识别每一行的年份。 `concat`函数允许使用`keys`参数标记每个结果数据帧。 该标签将显示在级联框架的最外层索引级别中,并强制创建 MultiIndex。 同样,为了清楚起见,`names`参数还可以重命名每个索引级别: ```py >>> pd.concat(s_list, keys=['2016', '2017'], names=['Year', 'Symbol']) ``` ![](img/00227.jpeg) 4. 也可以通过将`axis`参数更改为*列*或 *1* 来水平连接: ```py >>> pd.concat(s_list, keys=['2016', '2017'], axis='columns', names=['Year', None]) ``` ![](img/00228.jpeg) 5. 请注意,当一年中存在股票代号而另一年不存在时,会出现缺失值。 默认情况下,`concat`函数使用外部联接,将列表中每个数据帧的所有行保留在列表中。 但是,它为我们提供了仅在两个数据帧中保留具有相同索引值的行的选项。 这称为内部联接。 我们将`join`参数设置为*内部*,以更改行为: ```py >>> pd.concat(s_list, join='inner', keys=['2016', '2017'], axis='columns', names=['Year', None]) ``` ![](img/00229.jpeg) # 工作原理 第一个参数是`concat`函数所需的唯一参数,它必须是一系列 Pandas 对象,通常是数据帧或序列的列表或字典。 默认情况下,所有这些对象将垂直堆叠在另一个之上。 在此秘籍中,仅串联了两个数据帧,但是任何数量的 Pandas 对象都可以工作。 当我们垂直连接时,数据帧通过其列名称对齐。 在此数据集中,所有列名称均相同,因此 2017 年数据中的每个列均在 2016 年数据中的同一列名称下精确对齐。 但是,如步骤 4 所示,将它们水平连接时,只有两个年份的索引标签相匹配- *AAPL* 和 *TSLA* 。 因此,这些股票代号在任何一年中都没有缺失值。 可以使用`concat`进行两种对齐方式,`join`参数引用的*外部*(默认)和*内部*。 # 更多 `append`方法是`concat`的精简版本,只能将新行附加到数据帧。 在内部,`append`仅调用`concat`函数。 例如,此秘籍中的第 2 步可以复制以下内容: ```py >>> stocks_2016.append(stocks_2017) ``` # 比较特朗普总统和奥巴马总统的支持率 现任美国总统的公众支持是一个经常成为新闻头条的话题,并通过民意测验进行正式衡量。 近年来,这些民意调查的频率迅速增加,并且每周都有大量新的数据发布。 有许多不同的民意测验者都有各自的问题和方法来捕获其数据,因此,数据之间存在相当多的可变性。 来自加利福尼亚大学圣塔芭芭拉分校的 美国总统职位项目每天提供的总批准评级低至单个数据点。 与本书中的大多数秘籍不同,该数据在 CSV 文件中不易获得。 通常,作为数据分析师,您将需要在 Web 上查找数据,并使用可以将其抓取为可通过本地工作站解析的格式的工具。 # 准备 在本秘籍中,我们将使用`read_html`函数,该函数功能强大,可以在线从表中抓取数据并将其转换为数据帧。 您还将学习如何检查网页以查找某些元素的基础 HTML。 我使用 Google Chrome 浏览器作为浏览器,建议您将其或 Firefox 用于基于 Web 的步骤。 # 操作步骤 1. 导航至唐纳德·特朗普总统的*美国总统职位*批准页( [http://www.presidency.ucsb.edu/data/popularity.php?pres=45](http://www.presidency.ucsb.edu/data/popularity.php?pres=45) )。 您应该获得一个包含时间序列图的页面,该页面紧随其后的是表格中的数据: ![](img/00230.jpeg) 2. `read_html` 函数能够从网页上抓取表格并将其数据放入数据帧中。 它最适合简单的 HTML 表,并提供一些有用的参数来选择所需的确切表,以防同一页上有多个表。 让我们继续使用`read_html`作为其默认值,它将以列表形式将所有表作为数据帧返回: ```py >>> 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 ``` 3. 该函数返回了 14 个表,乍一看似乎很荒谬,因为该网页似乎只显示了大多数人会识别为表的单个元素。 `read_html`函数正式搜索以 *<表*开头的 HTML 表元素。 我们通过右键单击批准数据表并选择 inspect 或 inspect 元素来检查 HTML 页面: ![](img/00231.jpeg) 4. 这将打开控制台,这是用于 Web 开发的非常强大的工具。 对于本秘籍,我们仅需要几个任务即可使用。 所有控制台都允许您在 HTML 中搜索特定的单词。 让我们搜索单词`table`。 我的浏览器找到 15 个不同的 HTML 表格,非常接近`read_html`返回的数字: ![](img/00232.jpeg) 5. 让我们开始检查`df_list`中的数据帧: ```py >>> df0 = df_list[0] >>> df0.shape (308, 1794) >>> df0.head(7) ``` ![](img/00233.jpeg) 6. 回顾网页,从 2017 年 1 月 22 日开始直到批准数据收集的那一天(即 2017 年 9 月 25 日),几乎每天都有批准表存在。这是八个多月或 250 行 数据,该数据在某种程度上接近第一个表中的 308 行。 扫描其余的表,您会发现发现了许多空的,毫无意义的表,以及网页中实际上与表不相似的不同部分的表。 让我们使用`read_html`函数的一些参数来帮助我们选择所需的表。 我们可以使用`match`参数在表中搜索特定的字符串。 让我们搜索其中带有单词 *Start Date* 的表: ```py >>> df_list = pd.read_html(trump_url, match='Start Date') >>> len(df_list) 3 ``` 7. 通过在表中搜索特定的字符串,我们将表的数量减少到只有三个。 另一个有用的参数是`attrs`,它接受 HTML 属性及其值配对的字典。 我们想为我们的特定表找到一些独特的属性。 为此,让我们再次在数据表中单击鼠标右键。 这次,请确保单击表格标题之一的最上方。 例如,右键单击*主席*,然后再次选择检查或检查元素: ![](img/00234.jpeg) 8. 您选择的元素应突出显示。 实际上,这不是我们感兴趣的元素。继续查看,直到遇到以 *<表*开头的 HTML 标签。 等号左边的所有单词都是属性或`attrs`,右边的是值。 让我们在搜索中使用*对齐*属性及其值 *center* : ```py >>> 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) ``` ![](img/00235.jpeg) 9. 我们仅与一个表匹配,并且行数非常接近起始日期和最后日期之间的总天数。 查看数据,似乎我们确实找到了要查找的表。 六个列的名称似乎在第 4 行。我们可以走得更远,更精确地选择要跳过的行以及要使用`skiprows`和`header`参数的列名称。 我们还可以使用`parse_dates`参数确保将开始日期和结束日期正确地强制为正确的数据类型: ```py >>> 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() ``` ![](img/00236.jpeg) 10. 这几乎是我们想要的,除了缺少值的列。 让我们使用`dropna`方法删除缺少所有值的列: ```py >>> trump = trump.dropna(axis=1, how='all') >>> trump.head() ``` ![](img/00237.jpeg) 11. 让我们用`ffill`方法向前填充`President`列中的缺失值。 首先让我们检查其他列中是否缺少任何值: ```py >>> 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() ``` ![](img/00238.jpeg) 12. 最后,检查数据类型以确保它们正确是很重要的: ```py >>> trump.dtypes President object Start Date datetime64[ns] End Date datetime64[ns] Approving int64 Disapproving int64 unsure/no data int64 dtype: object ``` 13. 让我们构建一个将所有步骤组合在一起的功能,以自动化检索任何总裁的批准数据的过程: ```py >>> 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) ``` 14. 唯一的参数`pres_num`表示每个总裁的订单号。 巴拉克·奥巴马(Barack Obama)是美国第 44 任总统; 将 44 传递给`get_pres_appr`函数以获取其批准号: ```py >>> obama = get_pres_appr(44) >>> obama.head() ``` ![](img/00239.jpeg) 15. 在总统富兰克林·罗斯福(Franklin Roosevelt)第三任期期间,有总统支持率的评级数据可追溯到 1941 年。 通过我们的自定义功能以及`concat`函数,可以从该站点获取所有总统批准评级数据。 现在,让我们获取最后五位总统的支持率数据,并输出每位总统的前三行: ```py >>> 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) ``` ![](img/00240.jpeg) 16. 在继续之前,让我们确定是否有多个批准评级的日期: ```py >>> 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 ``` 17. 只有几天有重复的值。 为了简化分析,让我们仅保留重复日期存在的第一行: ```py >>> pres_41_45 = pres_41_45.drop_duplicates(subset='End Date') ``` 18. 让我们获得一些关于数据的摘要统计信息: ```py >>> 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) ``` ![](img/00241.jpeg) 19. 让我们在同一张图表上绘制每个总裁的支持率。 为此,我们将按每位总裁分组,遍历每组,并分别绘制每个日期的批准等级: ```py >>> 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') ``` ![](img/00242.jpeg) 20. 此图表将所有总统依次排列。 通过将批准等级与在职天数作图,我们可以更简单地比较它们。 让我们创建一个新变量来代表上班天数: ```py >>> 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) ``` ![](img/00243.jpeg) 21. 自总统任期以来,我们已经成功地为每一行分配了相对天数。 有趣的是,新列`Days in Office`具有其值的字符串表示形式。 让我们检查其数据类型: ```py >>> pres_41_45.dtypes ... Days in Office timedelta64[ns] dtype: object ``` 22. `Days in Office`列是具有纳秒精度的`timedelta64`对象。 这比所需的精度要高得多。 让我们通过仅获取日期将数据类型更改为整数: ```py >>> 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 ``` 23. 我们可以按照与步骤 19 中相似的方式来绘制此数据,但是存在一种完全不涉及任何循环的方法。 默认情况下,在数据帧上调用`plot`方法时,pandas 尝试将数据的每一列绘制为线图,并使用索引作为 x 轴。 知道了这一点之后,我们就来讨论一下数据,以便每位总裁都拥有自己的专栏以进行审批: ```py >>> pres_pivot = pres_41_45.pivot(index='Days in Office', columns='President', values='Approving') >>> pres_pivot.head() ``` ![](img/00244.jpeg) 24. 现在,每个总裁都有自己的批准等级列,我们可以直接对每个列进行绘制而无需分组。 为了减少剧情中的混乱情况,我们将仅绘制 Barack Obama 和 Donald J. Trump: ```py >>> 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) ``` ![](img/00245.jpeg) # 工作原理 通常在到达所需的一个或多个表之前多次调用`read_html`。 您可以使用两个主要参数来指定表`match`和`attrs`。 提供给`match`的字符串用于查找表中实际文本的精确匹配。 这是将显示在网页本身上的文本。 另一方面,`attrs`参数搜索在表标记`