ch09.md 49.5 KB
Newer Older
W
wizardforcel 已提交
1
# 组合 Pandas 对象
W
wizardforcel 已提交
2 3 4 5

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

*   将新行追加到 DataFrames
W
wizardforcel 已提交
6
*   将多个数据帧连接在一起
W
wizardforcel 已提交
7 8 9 10 11 12
*   比较特朗普总统和奥巴马总统的支持率
*   了解`concat``join``merge`之间的区别
*   连接到 SQL 数据库

# 介绍

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

# 将新行追加到 DataFrames

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

W
wizardforcel 已提交
19
# 准备
W
wizardforcel 已提交
20

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

W
wizardforcel 已提交
23
# 操作步骤
W
wizardforcel 已提交
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

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)

W
wizardforcel 已提交
53
4.  为了更明确地将变量与值相关联,可以使用字典。 同样,在这一步中,我们可以动态选择新的索引标签作为数据帧的长度:
W
wizardforcel 已提交
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71

```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)

W
wizardforcel 已提交
72
6.  前面的操作全部使用`.loc`索引运算符就地更改`names` DataFrame。 没有返回的数据帧的单独副本。 在接下来的几个步骤中,我们将研究`append`方法,该方法不会修改调用数据帧的方法。 而是返回带有附加行的数据帧的新副本。 让我们从原始的`names` DataFrame 开始,并尝试追加一行。 `append`的第一个参数必须是另一个数据帧,序列,dictionary 或它们的列表,但不能是步骤 2 中的列表。让我们看看当尝试将字典与`append`一起使用时会发生什么:
W
wizardforcel 已提交
73 74 75 76 77 78 79

```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
```

W
wizardforcel 已提交
80
7.  此错误消息似乎有点不正确。 我们正在传递一个数据帧而不是一个序列,但是它为我们提供了如何更正它的说明:
W
wizardforcel 已提交
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147

```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}
```

W
wizardforcel 已提交
148
# 工作原理
W
wizardforcel 已提交
149

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

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

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

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

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

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

W
wizardforcel 已提交
162
# 更多
W
wizardforcel 已提交
163

W
wizardforcel 已提交
164
将单行添加到数据帧是相当昂贵的操作,如果您发现自己编写了将单行数据附加到数据帧的循环,那么您做错了。 让我们首先创建 1,000 行新数据作为序列列表:
W
wizardforcel 已提交
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204

```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)
```

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

W
wizardforcel 已提交
207
# 将多个数据帧连接在一起
W
wizardforcel 已提交
208

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

W
wizardforcel 已提交
211
# 准备
W
wizardforcel 已提交
212

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

W
wizardforcel 已提交
215
# 操作步骤
W
wizardforcel 已提交
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236

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)

W
wizardforcel 已提交
237
3.  默认情况下,`concat`函数垂直连接数据帧,一个接一个。 前面的数据帧的一个问题是无法识别每一行的年份。 `concat`函数允许使用`keys`参数标记每个结果数据帧。 该标签将显示在级联框架的最外层索引级别中,并强制创建 MultiIndex。 同样,为了清楚起见,`names`参数还可以重命名每个索引级别:
W
wizardforcel 已提交
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254

```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)

W
wizardforcel 已提交
255
5.  请注意,当一年中存在股票代号而另一年不存在时,会出现缺失值。 默认情况下,`concat`函数使用外部联接,将列表中每个数据帧的所有行保留在列表中。 但是,它为我们提供了仅在两个数据帧中保留具有相同索引值的行的选项。 这称为内部联接。 我们将`join`参数设置为*内部*,以更改行为:
W
wizardforcel 已提交
256 257 258 259 260 261 262 263

```py
>>> pd.concat(s_list, join='inner', keys=['2016', '2017'],
              axis='columns', names=['Year', None])
```

![](img/00229.jpeg)

W
wizardforcel 已提交
264
# 工作原理
W
wizardforcel 已提交
265

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

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

W
wizardforcel 已提交
270
# 更多
W
wizardforcel 已提交
271

W
wizardforcel 已提交
272
`append`方法是`concat`的精简版本,只能将新行附加到数据帧。 在内部,`append`仅调用`concat`函数。 例如,此秘籍中的第 2 步可以复制以下内容:
W
wizardforcel 已提交
273 274 275 276 277 278 279 280 281

```py
>>> stocks_2016.append(stocks_2017)
```

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

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

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

W
wizardforcel 已提交
284
# 准备
W
wizardforcel 已提交
285

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

W
wizardforcel 已提交
288
# 操作步骤
W
wizardforcel 已提交
289 290 291 292 293

1.  导航至唐纳德·特朗普总统的*美国总统职位*批准页( [http://www.presidency.ucsb.edu/data/popularity.php?pres=45](http://www.presidency.ucsb.edu/data/popularity.php?pres=45) )。 您应该获得一个包含时间序列图的页面,该页面紧随其后的是表格中的数据:

![](img/00230.jpeg)

W
wizardforcel 已提交
294
2.  `read_html` 函数能够从网页上抓取表格并将其数据放入数据帧中。 它最适合简单的 HTML 表,并提供一些有用的参数来选择所需的确切表,以防同一页上有多个表。 让我们继续使用`read_html`作为其默认值,它将以列表形式将所有表作为数据帧返回:
W
wizardforcel 已提交
295 296 297 298 299 300 301 302 303 304 305 306 307

```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)

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

![](img/00232.jpeg)

W
wizardforcel 已提交
312
5.  让我们开始检查`df_list`中的数据帧:
W
wizardforcel 已提交
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433

```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)

W
wizardforcel 已提交
434
15.  在总统富兰克林·罗斯福(Franklin Roosevelt)第三任期期间,有总统支持率的评级数据可追溯到 1941 年。 通过我们的自定义功能以及`concat`函数,可以从该站点获取所有总统批准评级数据。 现在,让我们获取最后五位总统的支持率数据,并输出每位总统的前三行:
W
wizardforcel 已提交
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537

```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
```

W
wizardforcel 已提交
538
23.  我们可以按照与步骤 19 中相似的方式来绘制此数据,但是存在一种完全不涉及任何循环的方法。 默认情况下,在数据帧上调用`plot`方法时,pandas 尝试将数据的每一列绘制为线图,并使用索引作为 x 轴。 知道了这一点之后,我们就来讨论一下数据,以便每位总裁都拥有自己的专栏以进行审批:
W
wizardforcel 已提交
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559

```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)

W
wizardforcel 已提交
560
# 工作原理
W
wizardforcel 已提交
561 562 563

通常在到达所需的一个或多个表之前多次调用`read_html`。 您可以使用两个主要参数来指定表`match``attrs`。 提供给`match`的字符串用于查找表中实际文本的精确匹配。 这是将显示在网页本身上的文本。 另一方面,`attrs`参数搜索在表标记`<table`开始之后直接找到的 HTML 表属性。 要查看更多表格属性,请访问 W3 学校(本页为 [http://bit.ly/2hzUzdD](https://www.w3schools.com/TagS/tag_table.asp) )。

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

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

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

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

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

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

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

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

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

W
wizardforcel 已提交
582
# 更多
W
wizardforcel 已提交
583

W
wizardforcel 已提交
584
步骤 19 中的图显示了大量噪声,如果对其进行了平滑处理,则数据可能更易于解释。 一种常见的平滑方法称为**滚动平均值**。 Pandas 为数据帧和`groupby`对象提供了`rolling`方法。 它通过返回一个对象以等待对其执行附加操作,从而类似于`groupby`方法。 创建它时,必须将窗口的大小作为第一个参数传递,它可以是整数或日期偏移量字符串。
W
wizardforcel 已提交
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617

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

```py
>>> 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)

W
wizardforcel 已提交
618
# 另见
W
wizardforcel 已提交
619 620 621

*   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)
W
wizardforcel 已提交
622
*   请参阅第 11 章,“使用 Matplotlib,Pandas 和 Seaborn 进行可视化”
W
wizardforcel 已提交
623 624 625

# 了解 concat,join 和 merge 之间的区别

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

*   `concat`
W
wizardforcel 已提交
629 630
    *   Pandas 功能
    *   垂直或水平组合两个或多个 Pandas 对象
W
wizardforcel 已提交
631 632 633 634 635
    *   仅在索引上对齐
    *   每当索引中出现重复项时发生错误
    *   默认为外部联接,带有内部联接选项
*   `join`
    *   DataFrame 方法
W
wizardforcel 已提交
636
    *   水平组合两个或多个 Pandas 对象
W
wizardforcel 已提交
637
    *   将调用的数据帧的列或索引与其他对象的索引(而不是列)对齐
W
wizardforcel 已提交
638 639 640 641 642
    *   通过执行笛卡尔乘积来处理联接列/索引上的重复值
    *   默认为左连接,带有内部,外部和右侧选项
*   `merge`
    *   DataFrame 方法
    *   水平准确地合并两个 DataFrame
W
wizardforcel 已提交
643
    *   将调用的数据帧的列/索引与其他数据帧的列/索引对齐
W
wizardforcel 已提交
644 645 646 647 648
    *   通过执行笛卡尔乘积来处理联接列/索引上的重复值
    *   默认为内部联接,左,外和右选项

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.

W
wizardforcel 已提交
649
# 准备
W
wizardforcel 已提交
650

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

W
wizardforcel 已提交
653
# 操作步骤
W
wizardforcel 已提交
654

W
wizardforcel 已提交
655
1.  让我们使用循环而不是对`read_csv`函数的三个不同调用将 2016 年,2017 年和 2018 年的股票数据读入数据帧列表中。 Jupyter 笔记本当前仅允许将一个数据帧显示在一行上。 但是,有一种方法可以在`IPython`库的帮助下自定义 HTML 输出。 用户定义的`display_frames` 函数接受数据帧列表并将它们全部输出到一行:
W
wizardforcel 已提交
656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678

```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 = '<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
```

![](img/00247.jpeg)

W
wizardforcel 已提交
679
2.  `concat`函数是唯一能够垂直组合数据帧的功能。 让我们通过将列表`stock_tables`传递给它:
W
wizardforcel 已提交
680 681 682 683 684 685 686

```py
>>> pd.concat(stock_tables, keys=[2016, 2017, 2018])
```

![](img/00248.jpeg)

W
wizardforcel 已提交
687
3.  通过将`axis`参数更改为`columns`,它也可以水平组合数据帧:
W
wizardforcel 已提交
688 689 690 691 692 693 694

```py
>>> pd.concat(dict(zip(years,stock_tables)), axis='columns')
```

![](img/00249.jpeg)

W
wizardforcel 已提交
695
4.  现在我们已经开始水平组合数据帧了,我们可以使用`join``merge`方法来复制`concat`的功能。 在这里,我们使用`join`方法来组合`stock_2016``stock_2017`数据帧。 默认情况下,数据帧按其索引对齐。 如果任何一列具有相同的名称,则必须为`lsuffix``rsuffix`参数提供一个值,以在结果中区分它们:
W
wizardforcel 已提交
696 697 698 699 700 701 702 703

```py
>>> stocks_2016.join(stocks_2017, lsuffix='_2016',
                     rsuffix='_2017', how='outer')
```

![](img/00250.jpeg)

W
wizardforcel 已提交
704
5.  为了精确复制步骤 3 中`concat`函数的输出,我们可以将数据帧列表传递给`join`方法:
W
wizardforcel 已提交
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727

```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
```

W
wizardforcel 已提交
728
7.  现在,让我们转向`merge`,与`concat``join`不同,它可以将两个数据帧恰好结合在一起。 默认情况下,`merge`尝试对齐每个数据帧中具有相同名称的列中的值。 但是,您可以通过将布尔参数`left_index``right_index`设置为`True`来选择使其与索引对齐。 让我们将 2016 年和 2017 年的股票数据合并在一起:
W
wizardforcel 已提交
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790

```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)

W
wizardforcel 已提交
791
13.  `join`方法仅与传递的数据帧的索引对齐,但可以使用调用数据帧的索引或列。 要使用列在调用数据帧上对齐,您需要将它们传递给`on`参数:
W
wizardforcel 已提交
792 793 794 795 796

```py
>>> food_transactions.join(food_prices_join, on=['item', 'store'])
```

W
wizardforcel 已提交
797
14.  输出与步骤 11 的结果完全匹配。 要使用`concat`方法复制此内容,您需要将该项放置并存储列到两个数据帧的索引中。 但是,在此特定情况下,由于在至少一个数据帧(具有项`steak`和存储`B`中)出现重复的索引值,将产生错误:
W
wizardforcel 已提交
798 799 800 801 802 803 804 805

```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!
```

W
wizardforcel 已提交
806
# 工作原理
W
wizardforcel 已提交
807

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

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

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

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

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

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

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

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

W
wizardforcel 已提交
824
# 更多
W
wizardforcel 已提交
825

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

```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)

W
wizardforcel 已提交
842
# 另见
W
wizardforcel 已提交
843 844

*   IPython 的`read_html`函数官方文档( [http://bit.ly/2fzFRzd](http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.display_html)
W
wizardforcel 已提交
845
*   请参阅第 6 章,“索引对齐”的*爆炸索引*秘籍。
W
wizardforcel 已提交
846 847 848 849 850

# 连接到 SQL 数据库

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

W
wizardforcel 已提交
851
# 准备
W
wizardforcel 已提交
852

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

![](img/00258.jpeg)

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

W
wizardforcel 已提交
859
# 操作步骤
W
wizardforcel 已提交
860 861 862 863 864 865 866 867

1.  在开始从`chinook`数据库中读取表之前,我们需要设置我们的 SQLAlchemy 引擎:

```py
>>> from sqlalchemy import create_engine
>>> engine = create_engine('sqlite:///data/chinook.db')
```

W
wizardforcel 已提交
868
2.  现在,我们可以回到 Pandas 世界,并在剩下的秘籍中呆在那里。 让我们完成一个简单的命令,并使用`read_sql_table`函数读取`tracks`表。 表的名称是第一个参数,SQLAlchemy 引擎是第二个参数:
W
wizardforcel 已提交
869 870 871 872 873 874 875 876

```py
>>> tracks = pd.read_sql_table('tracks', engine)
>>> tracks.head()
```

![](img/00259.jpeg)

W
wizardforcel 已提交
877
3.  对于本秘籍的其余部分,我们将在数据库图的帮助下回答几个不同的特定查询。 首先,让我们找到每种流派的平均歌曲长度:
W
wizardforcel 已提交
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942

```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
```

W
wizardforcel 已提交
943
# 工作原理
W
wizardforcel 已提交
944

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

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

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

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

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

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

W
wizardforcel 已提交
959
# 更多
W
wizardforcel 已提交
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015

如果您精通 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)

W
wizardforcel 已提交
1016
# 另见
W
wizardforcel 已提交
1017 1018

*   *SQLAlchemy* 的所有引擎配置( [http://bit.ly/2kb07vV](http://docs.sqlalchemy.org/en/latest/core/engines.html)
W
wizardforcel 已提交
1019
*   关于 *SQL 查询*[http://bit.ly/2fFsOQ8](http://pandas.pydata.org/pandas-docs/stable/io.html#sql-queries) 的 Pandas 官方文档