16.1 CSV文件格式

要在文本文件中存储数据,一个简单方式是将数据作为一系列以逗号分隔的值 (comma-separated values)写入文件。这样的文件称为CSV 文件。例如,下面是一行CSV格式的天气数据:

"USW00025333","SITKA AIRPORT, AK US","2018-01-01","0.45",,"48","38"


这是阿拉斯加州锡特卡2018年1月1日的天气数据,其中包含当天的最高温度和最低温度,还有众多其他的数据。CSV文件对人来说阅读起来比较麻烦,但程序可轻松提取并处理其中的值,有助于加快数据分析过程。

我们将首先处理少量CSV格式的锡特卡天气数据,这些数据可在本书的配套资源(ituring.cn/ book/2784)中找到。请将文件sitka_weather_07-2018_simple.csv复制到存储本章程序的文件夹中。(下载本书的配套资源后,就有了这个项目所需的所有文件。)

注意  该项目使用的天气数据来自美国国家海洋与大气管理局(National Oceanic and Atmospheric Administration,NOAA)。

16.1.1 分析CSV文件头

csv 模块包含在Python标准库中,可用于分析CSV文件中的数据行,让我们能够快速提取感兴趣的值。先来查看这个文件的第一行,其中的一系列文件头指出了后续各行包含的是什么样的信息: sitkahighs.py
import csv

filename = 'data/sitka_weather_07-2018_simple.csv'
❶ with open(filename) as f:
❷ reader = csv.reader(f)
❸ header_row = next(reader)
print(header_row)


导入模块csv 后,将要使用的文件的名称赋给filename 。接下来,打开这个文件,并将返回的文件对象赋给f (见❶)。然后,调用csv.reader() 并将前面存储的文件对象作为实参传递给它,从而创建一个与该文件相关联的阅读器对象(见❷)。这个阅读器对象被赋给了reader 。 模块csv 包含函数next() ,调用它并传入阅读器对象时,它将返回文件中的下一行。在上述代码中,只调用了next() 一次,因此得到的是文件的第一行,其中包含文件头(见❸)。将返回的数据存储到header_row 中。如你所见,header_row 包含与天气相关的文件头,指出了每行都包含哪些数据:
['STATION', 'NAME', 'DATE', 'PRCP', 'TAVG', 'TMAX', 'TMIN']


reader 处理文件中以逗号分隔的第一行数据,并将每项数据都作为一个元素存储在列表中。文件头STATION 表示记录数据的气象站的编码。这个文件头的位置表明,每行的第一个值都是气象站编码。文件头NAME 指出每行的第二个值都是记录数据的气象站的名称。其他文件头则指出记录了哪些信息。当前,我们最关心的是日期(DATE )、最高温度(TMAX )和最低温度(TMIN )。这是一个简单的数据集,只包含降水量以及与温度相关的数据。你自己下载天气数据时,可选择涵盖众多测量值,如风速、风向以及详细的降水量数据。 16.1.2 打印文件头及其位置 为了让文件头数据更容易理解,将列表中的每个文件头及其位置打印出来: sitka_highs.py
—snip—
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

❶ for index, column_header in enumerate(header_row):
print(index, column_header)


在循环中,对列表调用了enumerate() (见❶)来获取每个元素的索引及其值。(请注意,我们删除了代码行print(header_row) ,转而显示这个更详细的版本。) 输出如下,指出了每个文件头的索引:
0 STATION
1 NAME
2 DATE
3 PRCP
4 TAVG
5 TMAX
6 TMIN


从中可知,日期和最高温度分别存储在第三列和第六列。为研究这些数据,我们将处理sitka_weather_07-2018_simple.csv中的每行数据,并提取其中索引为2和5的值。 16.1.3 提取并读取数据 知道需要哪些列中的数据后,我们来读取一些数据。首先,读取每天的最高温度: sitka_highs.py
—snip—
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

# 从文件中获取最高温度。
❶ highs = []
❷ for row in reader:
❸ high = int(row[5])
highs.append(high)

print(highs)


创建一个名为highs 的空列表(见❶),再遍历文件中余下的各行(见❷)。阅读器对象从其停留的地方继续往下读取CSV文件,每次都自动返回当前所处位置的下一行。由于已经读取了文件头行,这个循环将从第二行开始——从这行开始包含的是实际数据。每次执行循环时,都将索引5处(TMAX 列)的数据附加到highs 末尾(见❸)。在文件中,这项数据是以字符串格式存储的,因此在附加到highs 末尾前,使用函数int() 将其转换为数值格式,以便使用。 highs 现在存储的数据如下:
[62, 58, 70, 70, 67, 59, 58, 62, 66, 59, 56, 63, 65, 58, 56, 59, 64, 60, 60,
61, 65, 65, 63, 59, 64, 65, 68, 66, 64, 67, 65]


提取每天的最高温度并将其存储到列表中之后,就可以可视化这些数据了。 16.1.4 绘制温度图表 为可视化这些温度数据,首先使用Matplotlib创建一个显示每日最高温度的简单图形,如下所示: sitka_highs.py
import csv

import matplotlib.pyplot as plt

filename = 'data/sitka_weather_07-2018_simple.csv'
with open(filename) as f:
—_snip


# 根据最高温度绘制图形。
plt.style.use('seaborn')
fig, ax = plt.subplots()
❶ ax.plot(highs, c='red')

# 设置图形的格式。
❷ ax.set_title("2018年7月每日最高温度", fontsize=24)
❸ ax.set_xlabel('', fontsize=16)
ax.set_ylabel("温度 (F)", fontsize=16)
ax.tick_params(axis='both', which='major', labelsize=16)

plt.show()


将最高温度列表传给plot() (见❶),并传递c='red' 以便将数据点绘制为红色。(这里使用红色显示最高温度,用蓝色显示最低温度。)接下来,设置了一些其他的格式,如名称和字号(见❷),这些都在第15章介绍过。鉴于还没有添加日期,因此没有给16.1 CSV文件格式 - 图1 轴添加标签,但ax.set_xlabel() 确实修改了字号,让默认标签更容易看清❸。图16-1显示了绘制的图表:一个简单的折线图,显示了阿拉斯加州锡特卡2018年7月的每日最高温度。 16.1 CSV文件格式 - 图2 图16-1 阿拉斯加州锡特卡2018年7月每日最高温度折线图 16.1.5 模块datetime

下面在图表中添加日期,使其更有用。在天气数据文件中,第一个日期在第二行:

"USW00025333","SITKA AIRPORT, AK US","2018-07-01","0.25",,"62","50"


读取该数据时,获得的是一个字符串,因此需要想办法将字符串"2018-7-1" 转换为一个表示相应日期的对象。为创建一个表示2018年7月1日的对象,可使用模块datetime 中的方法strptime() 。我们在终端会话中看看strptime() 的工作原理:

>>> from datetime import datetime
>>> first_date = datetime.strptime('2018-07-01', '%Y-%m-%d')
>>> print(first_date)
2018-07-01 00:00:00


首先导入模块datetime 中的datetime 类,再调用方法strptime() ,并将包含所需日期的字符串作为第一个实参。第二个实参告诉Python如何设置日期的格式。在这里,'%Y-' 让Python将字符串中第一个连字符前面的部分视为四位的年份,'%m-' 让Python将第二个连字符前面的部分视为表示月份的数,'%d' 让Python将字符串的最后一部分视为月份中的一天(1~31)。

方法strptime() 可接受各种实参,并根据它们来决定如何解读日期。表16-1列出了这样的一些实参。

表16-1 模块datetime 中设置日期和时间格式的实参

实参 含义
%A 星期几,如Monday
%B 月份名,如January
%m 用数表示的月份(01~12)
%d 用数表示的月份中的一天(01~31)
%Y 四位的年份,如2019
%y 两位的年份,如19
%H 24小时制的小时数(00~23)
%I 12小时制的小时数(01~12)
%p am或pm
%M 分钟数(00~59)
%S 秒数(00~61)

16.1.6 在图表中添加日期

现在,可以通过提取日期和最高温度并将其传递给plot() ,对温度图形进行改进,如下所示:

sitka_highs.py

import csv
from datetime import datetime

import matplotlib.pyplot as plt

filename = 'data/sitka_weather_07-2018_simple.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

# 从文件中获取日期和最高温度。
❶ dates, highs = [], []
for row in reader:
❷ current_date = datetime.strptime(row[2], '%Y-%m-%d')
high = int(row[5])
dates.append(current_date)
highs.append(high)

# 根据最高温度绘制图形。
plt.style.use('seaborn')
fig, ax = plt.subplots()
❸ ax.plot(dates, highs, c='red')

# 设置图形的格式。
ax.set_title("2018年7月每日最高温度", fontsize=24)
ax.set_xlabel('', fontsize=16)
❹ fig.autofmt_xdate()
ax.set_ylabel("温度 (F)", fontsize=16)
ax.tick_params(axis='both', which='major', labelsize=16)

plt.show()


我们创建了两个空列表,用于存储从文件中提取的日期和最高温度(见❶)。然后,将包含日期信息的数据(row[0] )转换为datetime 对象(见❷),并将其附加到列表dates 末尾。在❸处,将日期和最高温度值传递给plot() 。在❹处,调用fig.autofmt_xdate() 来绘制倾斜的日期标签,以免其彼此重叠。图16-2显示了改进后的图表。

16.1 CSV文件格式 - 图3

图16-2 现在图表的16.1 CSV文件格式 - 图4 轴上有日期,含义更丰富

16.1.7 涵盖更长的时间

设置好图表后,我们来添加更多的数据,生成一幅更复杂的锡特卡天气图。请将文件sitka_weather_2018_simple.csv复制到本章程序所在的文件夹,该文件包含整年的锡特卡天气数据。

现在可创建覆盖整年的天气图了:

sitka_highs.py

—snip—
❶ filename = 'data/sitka_weather_2018_simple.csv'
with open(filename) as f:
—snip—
# 设置图形的格式。
❷ ax.set_title("2018年每日最高温度", fontsize=24)
ax.set_xlabel('', fontsize=16)
—snip—


这里修改了文件名,以使用数据文件sitka_weather_2018_simple.csv(见❶),还修改了图表的标题,以反映其内容的变化(见❷)。图16-3显示了生成的图形。

16.1 CSV文件格式 - 图5

图16-3 一年的天气数据

16.1.8 再绘制一个数据系列

虽然改进后的图表已经显示了丰富的数据,但是还能再添加最低温度数据,使其更有用。为此,需要从数据文件中提取最低温度,并将它们添加到图表中,如下所示:

sitka_highs_lows.py

—snip—
filename = 'sitka_weather_2018_simple.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

# 从文件中获取日期、最高温度和最低温度。
❶ dates, highs, lows = [], [], []
for row in reader:
current_date = datetime.strptime(row[2], '%Y-%m-%d')
high = int(row[5])
❷ low = int(row[6])
dates.append(current_date)
highs.append(high)
lows.append(low)

# 根据最高温度和最低温度绘制图形。
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.plot(dates, highs, c='red')
❸ ax.plot(dates, lows, c='blue')

# 设置图形的格式。
❹ ax.set_title("2018年每日最高温度", fontsize=24)
—snip—


在❶处,添加空列表lows ,用于存储最低温度。接下来,从每行的第七列(row[6] )提取最低温度并存储(见❷)。在❸处,添加调用plot() 的代码,以使用蓝色绘制最低温度。最后,修改标题(见❹)。图16-4显示了这样绘制出来的图表。

16.1 CSV文件格式 - 图6

图16-4 在一个图表中包含两个数据系列

16.1.9 给图表区域着色

添加两个数据系列后,就可以知道每天的温度范围了。下面来给这个图表做最后的修饰,通过着色来呈现每天的温度范围。为此,将使用方法fill_between() 。它接受一个16.1 CSV文件格式 - 图7 值系列和两个16.1 CSV文件格式 - 图8 值系列,并填充两个16.1 CSV文件格式 - 图9 值系列之间的空间:

sitka_highs_lows.py

—snip—
# 根据最低温度和最高温度绘制图形。
plt.style.use('seaborn')
fig, ax = plt.subplots()
❶ ax.plot(dates, highs, c='red', alpha=0.5)
ax.plot(dates, lows, c='blue', alpha=0.5)
❷ ax.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1)
—snip—


❶处的实参alpha 指定颜色的透明度。alpha 值为0表示完全透明,为1(默认设置)表示完全不透明。通过将alpha 设置为0.5,可让红色和蓝色折线的颜色看起来更浅。

在❷处,向fill_between() 传递一个16.1 CSV文件格式 - 图10 值系列(列表dates ),以及两个16.1 CSV文件格式 - 图11 值系列(highslows )。实参facecolor指定填充区域的颜色,还将alpha 设置成了较小的值0.1,让填充区域将两个数据系列连接起来的同时不分散观察者的注意力。图16-5显示了最高温度和最低温度之间的区域被填充后的图表。

16.1 CSV文件格式 - 图12

图16-5 给两个数据集之间的区域着色

着色让两个数据集之间的区域变得更显眼了。

16.1.10 错误检查

我们应该能够使用任何地方的天气数据来运行sitka_highs_lows.py中的代码,但有些气象站收集的数据种类不同,有些气象站会偶尔出现故障,未能收集部分或全部应收集的数据。缺失数据可能引发异常,如果不妥善处理,可能导致程序崩溃。

例如,来看看生成加利福尼亚州死亡谷的温度图时出现的情况。请将文件death_valley_2018_simple.csv复制到本章程序所在的文件夹。

首先通过编写代码来查看这个数据文件包含的文件头:

death_valley_highs

import csvlows.py_

filename = 'data/death_valley_2018_simple.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

for index, column_header in enumerate(header_row):
print(index, column_header)


输出如下:

0 STATION
1 NAME
2 DATE
3 PRCP
4 TMAX
5 TMIN
6 TOBS


与前面一样,日期也在索引2处,但最高温度和最低温度分别在索引4和索引5处,因此需要修改代码中的索引,以反映这一点。另外,这个气象站没有记录平均温度,而记录了TOBS ,即特定时点的温度。

为演示缺失数据时将出现的状况,我故意从这个文件中删除了一项温度数据。下面来修改sitka_highs_lows.py,使用前面所说的索引来生成死亡谷的天气图,看看将出现什么状况:

death_valley_highs_lows.py

—snip—
filename = 'data/death_valley_2018_simple.csv'
with open(filename) as f:
—snip—
# 从文件中获取日期、最高温度和最低温度。
dates, highs, lows = [], [], []
for row in reader:
current_date = datetime.strptime(row[2], '%Y-%m-%d')
❶ high = int(row[4])
low = int(row[5])
dates.append(current_date)
—snip—


在❶处,修改索引,使其对应于这个文件中TMAXTMIN 的位置。

运行这个程序时出现了错误,如下述输出的最后一行所示:

Traceback (most recent call last):
File "death_valley_highs_lows.py", line 15, in <module>
high = int(row[4])
ValueError: invalid literal for int() with base 10: ''


该traceback指出,Python无法处理其中一天的最高温度,因为无法将空字符串('' )转换为整数。我们只要看一下文件death_valley_2018_simple.csv,就知道缺失了哪项数据,但这里不这样做,而是直接对缺失数据的情形进行处理。

为此,在从CSV文件中读取值时执行错误检查代码,对可能出现的异常进行处理,如下所示:

death_valley_highs_lows.py

—snip—
filename = 'data/death_valley_2018_simple.csv'
with open(filename) as f:
—snip—
for row in reader:
current_date = datetime.strptime(row[2], '%Y-%m-%d')
❶ try:
high = int(row[4])
low = int(row[5])
except ValueError:
❷ print(f"Missing data for {current_date}")
❸ else:
dates.append(current_date)
highs.append(high)
lows.append(low)

# 根据最高温度和最低温度绘制图形。
—snip—

# 设置图形的格式。
❹ title = "2018年每日最高温度和最低温度\n美国加利福尼亚州死亡谷"
ax.set_title(title, fontsize=20)
ax.set_xlabel('', fontsize=16)
—snip—


对于每一行,都尝试从中提取日期、最高温度和最低温度(见❶)。只要缺失其中一项数据,Python就会引发ValueError 异常。我们这样进行处理:打印一条错误消息,指出缺失数据的日期(见❷)。打印错误消息后,循环将接着处理下一行。如果获取特定日期的所有数据时没有发生错误,就运行else 代码块,将数据附加到相应列表的末尾(见❸)。这里绘图时使用的是有关另一个地方的信息,因此修改标题以指出这个地方。因为标题更长,所以我们缩小了字号(见❹)。

如果现在运行death_valley_highs_lows.py,将发现缺失数据的日期只有一个:

Missing data for 2018-02-18 00:00:00


妥善地处理错误后,代码能够生成图形并忽略缺失数据的那天。图16-6显示了绘制出的图形。

16.1 CSV文件格式 - 图13

图16-6 死亡谷每天的最高温度和最低温度

将这个图表与锡特卡的图表进行比较可知,总体而言,死亡谷比阿拉斯加东南部暖和,这符合预期。同时,死亡谷沙漠中每天的温差也更大——从着色区域的高度可以看出这一点。

你使用的很多数据集都可能缺失数据、格式不正确或数据本身不正确。对于这样的情形,可使用本书前半部分介绍的工具来处理。在这里,使用了一个try-except-else 代码块来处理数据缺失的问题。在有些情况下,需要使用continue 来跳过一些数据,或者使用remove()del 将已提取的数据删除。只要能进行精确而有意义的可视化,采用任何管用的方法都是可以的。

16.1.11 自己动手下载数据

如果你想自己下载天气数据,可采取如下步骤。

(1) 访问网站NOAA Climate Data Online。在Discover Data By部分,单击Search Tool。在下拉列表Select a Dataset中,选择Daily Summaries。

(2) 选择一个日期范围,在Search For下拉列表中ZIP Codes,输入你感兴趣地区的邮政编码,再单击Search按钮。

(3) 在下一个页面中,你将看到指定地区的地图和相关信息。单击地区名下方的View Full Details或单击地图再单击Full Details。

(4) 向下滚动并单击Station List,以显示该地区的气象站,再选择一个气象站并单击Add to Cart。虽然这个网站使用了购物车图标,但提供的数据是免费的。单击右上角的购物车。

(5) 在Select the Output中选择Custom GHCN-Daily CSV。确认日期范围无误后单击Continue。

(6) 在下一个页面中,可选择要下载的数据类型。可以只下载一种数据(如气温),也可以下载该气象站提供的所有数据。做出选择后单击Continue。

(7) 在最后一个页面,你将看到订单小结。请输入你的电子邮箱地址,再单击Submit Order。你将收到一封确认邮件,指出收到了你的订单。几分钟后,你将收到另一封邮件,其中包含用于下载数据的链接。

你下载的数据与本节处理的数据有类似的结构,但包含的文件头可能不同。然而,只要按本节介绍的步骤做,就能对你感兴趣的数据进行可视化。

动手试一试
练习16-1:锡特卡的降雨量  锡特卡属于温带雨林,降水量非常丰富。在数据文件sitka_weather_2018_simple.csv中,文件头PRCP表示的是每日降水量。请对这列数据进行可视化。如果你想知道沙漠的降水量有多低,可针对死亡谷完成同样的练习。
练习16-2:比较锡特卡和死亡谷的温度  在有关锡特卡和死亡谷的图表中,温度刻度反映了数据范围的不同。为准确比较锡特卡和死亡谷的温度范围,需要在16.1 CSV文件格式 - 图14 轴上使用相同的刻度。为此,请修改图16-5和图16-6所示图表的16.1 CSV文件格式 - 图15 轴设置,对锡特卡和死亡谷的温度范围进行直接比较(也可对任何两个地方的温度范围进行比较)。
练习16-3:旧金山  旧金山的温度更接近锡特卡还是死亡谷呢?为进行比较,可下载一些有关旧金山的温度数据,并据此生成包含最高温度和最低温度的图表。
练习16-4:自动索引  本节以硬编码的方式指定了TMINTMAX 列的索引。请根据文件头行确定这些列的索引,让程序同时适用于锡特卡和死亡谷。另外,请根据气象站的名称自动生成图表的标题。
练习16-5:探索  生成一些图表,对你好奇的任何地方的其他天气数据进行研究。

16.2 制作全球地震散点图:JSON格式

在本节中1 ,你将下载一个数据集,其中记录了一个月内全球发生的所有地震,再制作一幅散点图来展示这些地震的位置和震级。这些数据是以JSON格式存储的,因此要使用模块json 来处理。Plotly提供了根据位置数据绘制地图的工具,适合初学者使用。你将使用它来进行可视化并指出全球的地震分布情况。

1 本节为陶俊杰根据原作编写。——编者注

16.2.1 地震数据

请将文件eq_data_1_day_m1.json复制到存储本章程序的文件夹中。地震是以里氏震级度量的,而该文件记录了(截至写作本节时)最近24小时内全球发生的所有不低于1级的地震。

16.2.2 查看JSON数据

如果打开文件eq_data_1_day_m1.json,你将发现其内容密密麻麻,难以阅读:

{"type":"FeatureCollection","metadata":{"generated":1550361461000,…
{"type":"Feature","properties":{"mag":1.2,"place":"11km NNE of Nor…
{"type":"Feature","properties":{"mag":4.3,"place":"69km NNW of Ayn…
{"type":"Feature","properties":{"mag":3.6,"place":"126km SSE of Co…
{"type":"Feature","properties":{"mag":2.1,"place":"21km NNW of Teh…
{"type":"Feature","properties":{"mag":4,"place":"57km SSW of Kakto…
—snip—


这些数据适合机器而不是人来读取。不过可以看到,这个文件包含一些字典,还有一些我们感兴趣的信息,如震级和位置。

模块json 提供了各种探索和处理JSON数据的工具,其中一些有助于重新设置这个文件的格式,让我们能够更清楚地查看原始数据,继而决定如何以编程的方式来处理。

我们先加载这些数据并将其以易于阅读的方式显示出来。这个数据文件很长,因此不打印出来,而是将数据写入另一个文件,再打开该文件并轻松地在数据中导航:

eq_explore_data.py

import json

# 探索数据的结构。
filename = 'data/eq_data_1_day_m1.json'
with open(filename) as f:
❶ all_eq_data = json.load(f)

❷ readable_file = 'data/readable_eq_data.json'
with open(readable_file, 'w') as f:
❸ json.dump(all_eq_data, f, indent=4)


首先导入模块json ,以便恰当地加载文件中的数据,并将其存储到all_eq_data 中(见❶)。函数json.load() 将数据转换为Python能够处理的格式,这里是一个庞大的字典。在❷处,创建一个文件,以便将这些数据以易于阅读的方式写入其中。函数json.dump() 接受一个JSON数据对象和一个文件对象,并将数据写入这个文件中(见❸)。参数indent=4dump() 使用与数据结构匹配的缩进量来设置数据的格式。

如果你现在查看目录data并打开其中的文件readable_eq_data.json,将发现其开头部分像下面这样:

readable_eq_data.json

{
"type": "FeatureCollection",
❶ "metadata": {
"generated": 1550361461000,
"url": "https://earthquake.usgs.gov/earthquakes/.../1.0_day.geojson&#34;,
"title": "USGS Magnitude 1.0+ Earthquakes, Past Day",
"status": 200,
"api": "1.7.0",
"count": 158
},
❷ "features": [
—snip—


这个文件的开头是一个键为"metadata" 的片段(见❶),指出了这个数据文件是什么时候生成的,以及能够在网上的什么地方找到。它还包含适合人类阅读的标题以及文件中记录了多少次地震:在过去的24小时内,发生了158次地震。

这个geoJSON文件的结构适合存储基于位置的数据。数据存储在一个与键"features" 相关联的列表中(见❷)。这个文件包含的是地震数据,因此列表的每个元素都对应一次地震。这种结构可能有点令人迷惑,但很有用,让地质学家能够将有关每次地震的任意数量信息存储在一个字典中,再将这些字典放在一个大型列表中。

我们来看看表示特定地震的字典:

readable_eq_data.json

—snip—
{
"type": "Feature",
❶ "properties": {
"mag": 0.96,
—snip—
❷ "title": "M 1.0 - 8km NE of Aguanga, CA"
},
❸ "geometry": {
"type": "Point",
"coordinates": [
❹ -116.7941667,
❺ 33.4863333,
3.22
]
},
"id": "ci37532978"
},


"properties" 关联到了与特定地震相关的大量信息(见❶)。我们关心的主要是与键"mag" 相关联的地震震级以及地震的标题,因为后者很好地概述了地震的震级和位置(见❷)。

"geometry" 指出了地震发生在什么地方(见❸),我们需要根据这项信息将地震在散点图上标出来。在与键"coordinates" 相关联的列表中,可找到地震发生位置的经度(见❹)和纬度(见❺)。

这个文件的嵌套层级比我们编写的代码多。如果这让你感到迷惑,也不用担心,Python将替你处理大部分复杂的工作。我们每次只会处理一两个嵌套层级。我们将首先提取过去24小时内发生的每次地震对应的字典。

注意  说到位置时,我们通常先说纬度、再说经度,这种习惯形成的原因可能是人类先发现了纬度,很久后才有经度的概念。然而,很多地质学框架都先列出经度、后列出纬度,因为这与数学约定16.1 CSV文件格式 - 图16 一致。geoJSON格式遵循(经度, 纬度)的约定,但在使用其他框架时,获悉其遵循的约定很重要。

16.2.3 创建地震列表

首先,创建一个列表,其中包含所有地震的各种信息:

eq_explore_data.py

import json
# 探索数据的结构。
filename = 'data/eq_data_1_day_m1.json'
with open(filename) as f:
all_eq_data = json.load(f)

all_eq_dicts = all_eq_data['features']
print(len(all_eq_dicts))


我们提取与键'features' 相关联的数据,并将其存储到all_eq_dicts 中。我们知道,这个文件记录了158次地震。下面的输出表明,我们提取了这个文件记录的所有地震:

158


注意,我们编写的代码很短。格式良好的文件readable_eq_data.json包含超过6000行内容,但只需几行代码,就可读取所有的数据并将其存储到一个Python列表中。下面将提取所有地震的震级。

16.2.4 提取震级

有了包含所有地震数据的列表后,就可遍历这个列表,从中提取所需的数据。下面来提取每次地震的震级:

eq_explore_data.py

—snip—
all_eq_dicts = all_eq_data['features']

❶ mags = []
for eq_dict in all_eq_dicts:
❷ mag = eq_dict['properties']['mag']
mags.append(mag)

print(mags[:10])


我们创建了一个空列表,用于存储地震震级,再遍历列表all_eq_dicts(见❶)。每次地震的震级都存储在相应字典的'properties' 部分的'mag' 键下(见❷)。我们依次将地震震级赋给变量mag ,再将这个变量附加到列表mags 末尾。

为确定提取的数据是否正确,打印前10次地震的震级:

[0.96, 1.2, 4.3, 3.6, 2.1, 4, 1.06, 2.3, 4.9, 1.8]


接下来,我们将提取每次地震的位置信息,然后就可以绘制地震散点图了。

16.2.5 提取位置数据

位置数据存储在"geometry" 键下。在"geometry" 键关联的字典中,有一个"coordinates" 键,它关联到一个列表,而列表中的前两个值为经度和纬度。下面演示了如何提取位置数据:

eq_explore_data.py

—snip—
all_eq_dicts = all_eq_data['features']

mags, titles, lons, lats = [], [], [], []
for eq_dict in all_eq_dicts:
mag = eq_dict['properties']['mag']
❶ title = eq_dict['properties']['title']
❷ lon = eq_dict['geometry']['coordinates'][0]
lat = eq_dict['geometry']['coordinates'][1]
mags.append(mag)
titles.append(title)
lons.append(lon)
lats.append(lat)

print(mags[:10])
print(titles[:2])
print(lons[:5])
print(lats[:5])


我们创建了用于存储位置标题的列表titles ,来提取字典'properties''title' 键对应的值(见❶),以及用于存储经度和纬度的列表。代码eq_dict['geometry'] 访问与"geometry" 键相关联的字典(见❷)。第二个键('coordinates' )提取与"coordinates" 相关联的列表,而索引0提取该列表中的第一个值,即地震发生位置的经度。

打印前5个经度和纬度时,输出表明提取的数据是正确的:

[0.96, 1.2, 4.3, 3.6, 2.1, 4, 1.06, 2.3, 4.9, 1.8]
['M 1.0 - 8km NE of Aguanga, CA', 'M 1.2 - 11km NNE of North Nenana, Alaska']
[-116.7941667, -148.9865, -74.2343, -161.6801, -118.5316667]
[33.4863333, 64.6673, -12.1025, 54.2232, 35.3098333]


有了这些数据,就可以绘制地震散点图了。

16.2.6 绘制震级散点图

有了前面提取的数据,就可以绘制可视化图了。首先要实现一个简单的震级散点图,在确保显示的信息正确无误之后,我们再将注意力转向样式和外观方面。绘制初始散点图的代码如下:

eq_world_map.py

❶ import plotly.express as px

fig = px.scatter(
x=lons,
y=lats,
labels={"x": "经度", "y": "纬度"},
range_x=[-200, 200],
range_y=[-90, 90],
width=800,
height=800,
title="全球地震散点图",
❷ )
❸ fig.write_html("global_earthquakes.html")
❹ fig.show()


首先,导入plotly.express ,用别名px 表示。Plotly Express是Plotly的高级接口,简单易用,语法与Matplotlib类似(见❶)。然后,调用px.scatter 函数配置参数创建一个fig 实例,分别设置16.1 CSV文件格式 - 图17 轴为经度[范围是[-200, 200] (扩大空间,以便完整显示东西经180°附近的地震散点)]、16.1 CSV文件格式 - 图18 轴为纬度[范围是[-90, 90] ],设置散点图显示的宽度和高度均为800像素,并设置标题为“全球地震散点图”(见❷)。

只用14行代码,简单的散点图就配置完成了,这返回了一个fig 对象。fig.write_html 方法可以将可视化图保存为html文件。在文件夹中找到global_earthquakes.html文件,用浏览器打开即可(见❸)。另外,如果使用Jupyter Notebook,可以直接使用fig.show 方法直接在notebook单元格显示散点图(见❹)。

局部效果如下图所示:

16.1 CSV文件格式 - 图19

图16-7 显示24小时内所有地震的简单散点图

可对这幅散点图做大量修改,使其更有意义、更好懂。下面就来做些这样的修改。

16.2.7 另一种指定图表数据的方式

配置这个图表前,先来看看另一种稍微不同的指定Plotly 图表数据的方式。当前,经纬度数据是手动配置的:

—snip—
x=lons,
y=lats,
labels={"x": "经度", "y": "纬度"},
—snip—


这是在Plotly Express中给图表定义数据的最简单方式之一,但在数据处理中并不是最佳的。下面是另一种给图表定义数据的等效方式,需要使用pandas数据分析工具。首先创建一个DataFrame ,将需要的数据封装起来:

import pandas as pd

data = pd.DataFrame(
data=zip(lons, lats, titles, mags), columns=["经度", "纬度", "位置", "震级"]
)
data.head()


然后,参数配置方式可以变更为:

—snip—
data,
x="经度",
y="纬度",
—snip—


在这种方式中,所有有关数据的信息都以键值对的形式放在一个字典中。如果在eq_plot.py中使用这些代码,生成的图表是一样的。相比于前一种格式,这种格式让我们能够无缝衔接数据分析,并且更轻松地进行定制。

16.2.8 定制标记的尺寸

确定如何改进散点图的样式时,应着重于让要传达的信息更清晰。当前的散点图显示了每次地震的位置,但没有指出震级。我们要让观察者迅速获悉最严重的地震发生在什么地方。

为此,根据地震的震级设置其标记的尺寸:

eq_world_map.py

fig = px.scatter(
data,
x="经度",
y="纬度",
range_x=[-200, 200],
range_y=[-90, 90],
width=800,
height=800,
title="全球地震散点图",
❶ size="震级",
❷ size_max=10,
)
fig.write_html("global_earthquakes.html")
fig.show()


Plotly Express支持对数据系列进行定制,这些定制都以参数表示。这里使用了size 参数来指定散点图中每个标记的尺寸,我们只需要将前面data 中的"震级" 字段提供给size 参数即可(见❶)。另外,标记尺寸默认为20像素,还可以通过size_max=10 将最大显示尺寸缩放到10(见❷)。

如果运行这些代码,将看到类似于图16-8所示的散点图。这比前面的散点图好多了,但还有很大的改进空间。

16.1 CSV文件格式 - 图20

图16-8 现在散点图显示了地震的震级

16.2.9 定制标记的颜色

我们还可以定制标记的颜色,以呈现地震的严重程度。执行这些修改前,将文件eq_data_30_day_m1.json复制到你的数据目录中,它包含30天内的地震数据。通过使用这个更大的数据集,绘制出来的地震散点图将有趣得多。

下面演示了如何使用渐变来呈现地震震级:

eq_world_map.py

❶ filename = 'data/eq_data_30_day_m1.json'
—snip—
fig = px.scatter(
data,
x="经度",
y="纬度",
range_x=[-200, 200],
range_y=[-90, 90],
width=800,
height=800,
title="全球地震散点图",
size="震级",
size_max=10,
❷ color="震级",
)
—snip—


首先修改文件名,以使用30天的数据集(见❶)。为了让标记的震级按照不同的颜色显示,只需要配置color="震级" 即可。默认的视觉映射图例渐变色范围是从蓝到红再到黄,数值越小则标记越蓝,而数值越大则标记越黄(见❷)。

如果现在运行这个程序,你看到的散点图将漂亮得多。如图16-9所示,渐变的颜色指出了地震的严重程度。通过在散点图上显示大量的地震,可将板块边界大致呈现出来!

16.1 CSV文件格式 - 图21

图16-9 使用颜色和尺寸呈现震级的30天地震散点图

16.2.10 其他渐变

Plotly Express有大量的渐变可供选择。要获悉有哪些渐变可供使用,请使用文件名show_color_scales.py保存下面这个简短的程序:

show_color_scales.py

import plotly.express as px

for key in px.colors.named_colorscales():
print(key)


Plotly Express将渐变存储在模块colors 中。这些渐变是在列表px.colors.named_colorscales() 中定义的。下面的输出列出了可供你使用的所有渐变:

—snip—
greys
hot
inferno
jet
magenta
magma
—snip—


请尝试使用这些渐变其实映射到一个颜色列表。使用px.colors.diverging.RdYlGn[::-1] 可以将对应颜色的配色列表反转。

注意  Plotly除了有px.colors.diverging 表示连续变量的配色方案,还有px.colors.sequentialpx.colors.qualitative 表示离散变量。随便挑一种配色,例如px.colors.qualitative.Alphabet ,你将看到渐变是如何定义的。每个渐变都有起始色和终止色,有些渐变还定义了一个或多个中间色。Plotly会在这些定义好的颜色之间插入颜色。

16.2.11 添加鼠标指向时显示的文本

为完成这幅散点图的绘制,我们将添加一些说明性文本,在你将鼠标指向表示地震的标记时显示出来。除了默认显示的经度和纬度外,还将显示震级以及地震的大致位置:

eq_world_map.py

fig = px.scatter(
data,
x="经度",
y="纬度",
range_x=[-200, 200],
range_y=[-90, 90],
width=800,
height=800,
title="全球地震散点图",
size="震级",
size_max=10,
color="震级",
hover_name="位置",
)
fig.write_html("global_earthquakes.html")
fig.show()
—snip—


Plotly Express的操作非常简单,只需要将hover_name 参数配置为data"位置" 字段即可。

太令人震惊了!通过编写大约40行代码,我们就绘制了一幅漂亮的全球地震活动散点图,并通过30天地震数据大致展示了地球的板块结构。Plotly Express提供了众多定制可视化外观和行为的方式。使用它提供的众多选项,可让图表和散点图准确地显示你所需的信息。

动手试一试
练习16-6:重构  在从all_eq_dicts 中提取数据的循环中,使用了变量来指向震级、经度、纬度和标题,再将这些值分别附加到相应列表的末尾。这旨在清晰地演示如何从JSON文件中提取数据,但并非必须这样做。你也可以不使用这些临时变量,而是直接从eq_dict 中提取这些值,并将其附加到相应的列表末尾。这样做将缩短这个循环的循环体,使其只包含4行代码。
练习16-7:自动生成标题  本节定义my_layout 时以手工方式指定标题,这意味着每次变更源文件时,都需要修改标题。你可以不这样做,而是使用JSON文件中元数据(metadata)部分的数据集标题。为此,可提取这个值,将其赋给一个变量,并在定义my_layout 时使用这个变量来指定散点图的标题。
练习16-8:最近发生的地震  请在本书配套资源中找到关于最近1小时、1天、7天和30天内地震信息的数据文件(截至本书出版时,参见文件夹chapter_16/Excercise16-8)。请使用其中一个数据集,绘制一幅散点图来展示最近发生的地震。
练习16-9:全球火灾  在本章的配套资源中,有一个名为world_fires_1_day.csv的文件。它包含了有关全球各地发生的火灾信息,包括经度、纬度和火灾强度(brightness)。使用16.1节介绍的数据处理技术以及16.2节介绍的散点图绘制技术,绘制一幅散点图来展示全球哪些地方发生了火灾。