# 组合 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`参数搜索在表标记`>> 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 的输出类似,然后进行绘制: ```py >>> 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) ``` ![](img/00246.jpeg) # 另见 * matplotlib 的颜色图参考( [http://bit.ly/2yJZOvt](https://matplotlib.org/examples/color/colormaps_reference.html) ) * 所有日期偏移量及其别名的列表( [http://bit.ly/2xO5Yg0](http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases) ) * 请参阅第 11 章,“使用 Matplotlib,Pandas 和 Seaborn 进行可视化” # 了解 concat,join 和 merge 之间的区别 `merge`和`join` DataFrame(而不是序列)方法以及`concat`函数都提供了非常相似的功能,可以将多个 Pandas 对象组合在一起。 由于它们是如此相似,并且它们在某些情况下可以相互复制,因此何时以及如何正确使用它们会变得非常混乱。 为了帮助弄清它们之间的差异,请查看以下概述: * `concat`: * Pandas 功能 * 垂直或水平组合两个或多个 Pandas 对象 * 仅在索引上对齐 * 每当索引中出现重复项时发生错误 * 默认为外部联接,带有内部联接选项 * `join`: * DataFrame 方法 * 水平组合两个或多个 Pandas 对象 * 将调用的数据帧的列或索引与其他对象的索引(而不是列)对齐 * 通过执行笛卡尔乘积来处理联接列/索引上的重复值 * 默认为左连接,带有内部,外部和右侧选项 * `merge`: * DataFrame 方法 * 水平准确地合并两个 DataFrame * 将调用的数据帧的列/索引与其他数据帧的列/索引对齐 * 通过执行笛卡尔乘积来处理联接列/索引上的重复值 * 默认为内部联接,左,外和右选项 The first parameter to the join method is `other` which can either be a single DataFrame/Series or a list of any number of DataFrames/Series. # 准备 在此秘籍中,我们将执行组合数据帧所需的。 第一种情况使用`concat`更简单,而第二种情况使用`merge`更简单。 # 操作步骤 1. 让我们使用循环而不是对`read_csv`函数的三个不同调用将 2016 年,2017 年和 2018 年的股票数据读入数据帧列表中。 Jupyter 笔记本当前仅允许将一个数据帧显示在一行上。 但是,有一种方法可以在`IPython`库的帮助下自定义 HTML 输出。 用户定义的`display_frames` 函数接受数据帧列表并将它们全部输出到一行: ```py >>> 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 = '>> display_frames(stock_tables, 30) >>> stocks_2016, stocks_2017, stocks_2018 = stock_tables ``` ![](img/00247.jpeg) 2. `concat`函数是唯一能够垂直组合数据帧的功能。 让我们通过将列表`stock_tables`传递给它: ```py >>> pd.concat(stock_tables, keys=[2016, 2017, 2018]) ``` ![](img/00248.jpeg) 3. 通过将`axis`参数更改为`columns`,它也可以水平组合数据帧: ```py >>> pd.concat(dict(zip(years,stock_tables)), axis='columns') ``` ![](img/00249.jpeg) 4. 现在我们已经开始水平组合数据帧了,我们可以使用`join`和`merge`方法来复制`concat`的功能。 在这里,我们使用`join`方法来组合`stock_2016`和`stock_2017`数据帧。 默认情况下,数据帧按其索引对齐。 如果任何一列具有相同的名称,则必须为`lsuffix`或`rsuffix`参数提供一个值,以在结果中区分它们: ```py >>> stocks_2016.join(stocks_2017, lsuffix='_2016', rsuffix='_2017', how='outer') ``` ![](img/00250.jpeg) 5. 为了精确复制步骤 3 中`concat`函数的输出,我们可以将数据帧列表传递给`join`方法: ```py >>> other = [stocks_2017.add_suffix('_2017'), stocks_2018.add_suffix('_2018')] >>> stocks_2016.add_suffix('_2016').join(other, how='outer') ``` ![](img/00251.jpeg) 6. 让我们检查一下它们是否实际上完全相等: ```py >>> 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 ``` 7. 现在,让我们转向`merge`,与`concat`和`join`不同,它可以将两个数据帧恰好结合在一起。 默认情况下,`merge`尝试对齐每个数据帧中具有相同名称的列中的值。 但是,您可以通过将布尔参数`left_index`和`right_index`设置为`True`来选择使其与索引对齐。 让我们将 2016 年和 2017 年的股票数据合并在一起: ```py >>> stocks_2016.merge(stocks_2017, left_index=True, right_index=True) ``` ![](img/00252.jpeg) 8. 默认情况下,合并使用内部联接,并自动为名称相同的列提供后缀。 让我们更改为外部联接,然后执行 2018 数据的另一个外部联接以完全复制`concat`: ```py >>> 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 ``` 9. 现在,让我们将比较转到我们希望将列的值对齐而不是索引或列标签本身对齐的数据集。 `merge`方法正是针对这种情况而构建的。 让我们看一下两个新的小型数据集`food_prices`和`food_transactions`: ```py >>> 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) ``` ![](img/00253.jpeg) 10. 如果我们想查找每笔交易的总金额,则需要在`item`和`store`列上加入以下表格: ```py >>> food_transactions.merge(food_prices, on=['item', 'store']) ``` ![](img/00254.jpeg) 11. 现在,价格已正确与其对应的物料和商店对齐,但是存在问题。 客户 2 共有四个`steak`项目。 由于`steak`项目在每个表中针对`B`的存储表都出现两次,因此在它们之间会产生笛卡尔积,导致四行。 此外,请注意缺少`coconut`项目,因为没有相应的价格。 让我们解决这两个问题: ```py >>> food_transactions.merge(food_prices.query('Date == 2017'), how='left') ``` ![](img/00255.jpeg) 12. 我们可以使用`join`方法复制它,但是我们必须首先将`food_prices` DataFrame 的联接列放入索引中: ```py >>> food_prices_join = food_prices.query('Date == 2017') \ .set_index(['item', 'store']) >>> food_prices_join ``` ![](img/00256.jpeg) 13. `join`方法仅与传递的数据帧的索引对齐,但可以使用调用数据帧的索引或列。 要使用列在调用数据帧上对齐,您需要将它们传递给`on`参数: ```py >>> food_transactions.join(food_prices_join, on=['item', 'store']) ``` 14. 输出与步骤 11 的结果完全匹配。 要使用`concat`方法复制此内容,您需要将该项放置并存储列到两个数据帧的索引中。 但是,在此特定情况下,由于在至少一个数据帧(具有项`steak`和存储`B`中)出现重复的索引值,将产生错误: ```py >>> 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`属性更改为*内联*,可以更改每个表的 CSS(级联样式表),以便元素在水平方向上彼此相邻而不是垂直显示。 要在笔记本中正确呈现表格,您必须使用 IPython 库提供的辅助函数`read_html`。 在第 1 步结束时,我们将数据帧列表解压缩为它们自己的适当命名的变量,以便可以轻松,清晰地引用每个表。 关于数据帧列表的好处是,它是`concat`函数的确切要求,如步骤 2 所示。请注意,步骤 2 如何使用`keys`参数命名每个数据块。 也可以通过将字典传递给`concat`来完成,如步骤 3 所示。 在步骤 4 中,我们必须将`join`的类型更改为`outer`,以包括所传递的数据帧中所有在调用数据帧中不存在索引的行。 在步骤 5 中,传递的数据帧列表不能有任何共同的列。 尽管有`rsuffix`参数,但仅在传递单个数据帧而不是它们的列表时才起作用。 为了解决此限制,我们预先使用`add_suffix`方法更改列的名称,然后调用`join`方法。 在第 7 步中,我们使用`merge`,默认情况下,将对齐两个数据帧中相同的所有列名称。 要更改此默认行为,并对齐一个或两个的索引,请将`left_index`或`right_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`函数: ```py >>> 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() ``` ![](img/00257.jpeg) # 另见 * IPython 的`read_html`函数官方文档( [http://bit.ly/2fzFRzd](http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.display_html) ) * 请参阅第 6 章,“索引对齐”的*爆炸索引*秘籍。 # 连接到 SQL 数据库 要成为一名认真的数据分析师,几乎可以肯定,您必须学习一些 SQL。 世界上许多数据都存储在接受 SQL 语句的数据库中。 关系数据库管理系统有许多种,其中 SQLite 是最受欢迎和易于使用的系统之一。 # 准备 我们将探索 SQLite 提供的 Chinook 示例数据库,其中包含音乐商店的 11 个数据表。 首先进入适当的关系数据库时,最好的事情之一就是研究数据库图(有时称为实体关系图) ,以更好地了解表之间的关系。 下图在导航此秘籍时将非常有帮助: ![](img/00258.jpeg) 为了使此秘籍生效,您将需要安装`sqlalchemy` Python 软件包。 如果您安装了 Anaconda 发行版,则应该已经可以使用它。 与数据库建立连接时,SQLAlchemy 是首选的 Pandas 工具。 在本秘籍中,您将学习如何连接到 SQLite 数据库。 然后,您将问两个不同的查询,并通过使用`merge`方法将表连接在一起来回答它们。 # 操作步骤 1. 在开始从`chinook`数据库中读取表之前,我们需要设置我们的 SQLAlchemy 引擎: ```py >>> from sqlalchemy import create_engine >>> engine = create_engine('sqlite:///data/chinook.db') ``` 2. 现在,我们可以回到 Pandas 世界,并在剩下的秘籍中呆在那里。 让我们完成一个简单的命令,并使用`read_sql_table`函数读取`tracks`表。 表的名称是第一个参数,SQLAlchemy 引擎是第二个参数: ```py >>> tracks = pd.read_sql_table('tracks', engine) >>> tracks.head() ``` ![](img/00259.jpeg) 3. 对于本秘籍的其余部分,我们将在数据库图的帮助下回答几个不同的特定查询。 首先,让我们找到每种流派的平均歌曲长度: ```py >>> genre_track = genres.merge(tracks[['GenreId', 'Milliseconds']], on='GenreId', how='left') \ .drop('GenreId', axis='columns') >>> genre_track.head() ``` ![](img/00260.jpeg) 4. 现在我们可以轻松找到每种流派的每首歌曲的平均长度。 为了帮助简化解释,我们将`Milliseconds`列转换为`timedelta`数据类型: ```py >>> 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] ``` 5. 现在,让我们找出每个客户花费的总金额。 我们需要将`customers`,`invoices`和`invoice_items`表都相互连接: ```py >>> 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() ``` ![](img/00261.jpeg) 6. 现在,我们可以将数量乘以单价,然后找到每个客户花费的总金额: ```py >>> 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`操作来回答查询。 我们进一步走了一步,将整数毫秒转换为更容易阅读的 Timedelta 对象。 密钥以字符串形式传入正确的度量单位。 现在我们有了 Timedelta 系列,我们可以使用`dt`属性访问`floor`方法,该方法将时间向下舍入到最接近的秒。 回答步骤 5 所需的查询涉及三个表。 通过将表传递给`columns`参数,可以将表显着减少到仅需要的列。 使用`merge`时,具有相同名称的连接列将不保留。 在第 6 步中,我们可以为价格乘以数量分配一列,内容如下: ```py cust_inv['Total'] = cust_inv['Quantity'] * cust_inv['UnitPrice'] ``` 以这种方式分配列没有错。 我们选择使用 assign 方法动态创建新列,以允许连续的方法链。 # 更多 如果您精通 SQL,则可以将 SQL 查询作为字符串编写,并将其传递给`read_sql_query`函数。 例如,以下将重现步骤 4 的输出: ```py >>> 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) ``` ![](img/00262.jpeg) 要重现步骤 6 的答案,请使用以下 SQL 查询: ```py >>> 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) ``` ![](img/00263.jpeg) # 另见 * *SQLAlchemy* 的所有引擎配置( [http://bit.ly/2kb07vV](http://docs.sqlalchemy.org/en/latest/core/engines.html) ) * 关于 *SQL 查询*( [http://bit.ly/2fFsOQ8](http://pandas.pydata.org/pandas-docs/stable/io.html#sql-queries) 的 Pandas 官方文档