Python编程 从入门到实践(项目二:数据可视化)

本篇为实践项目二:数据可视化。

配合文章python编程入门学习,代码附文末。

项目二:数据可视化

  • 1.生成数据
    • [1.1 安装Matplotlib](#1.1 安装Matplotlib)
    • [1.2 绘制简单的折线图](#1.2 绘制简单的折线图)
      • [1.2.1 修改标签文字和线条粗细](#1.2.1 修改标签文字和线条粗细)
      • [1.2.2 校正图形](#1.2.2 校正图形)
      • [1.2.3 使用内置样式](#1.2.3 使用内置样式)
      • [1.2.4 使用scatter()绘制散点图并设置样式](#1.2.4 使用scatter()绘制散点图并设置样式)
      • [1.2.5 使用scatter()绘制一系列点](#1.2.5 使用scatter()绘制一系列点)
      • [1.2.6 自动计算数据](#1.2.6 自动计算数据)
      • [3.2.7 自定义颜色](#3.2.7 自定义颜色)
      • [1.2.8 使用颜色映射](#1.2.8 使用颜色映射)
      • [1.2.9 自动保存图表](#1.2.9 自动保存图表)
    • [1.3 随机漫步](#1.3 随机漫步)
      • [1.3.1 创建RandomWalk类](#1.3.1 创建RandomWalk类)
      • [1.3.2 选择方向](#1.3.2 选择方向)
      • [1.3.3 绘制随机漫步图](#1.3.3 绘制随机漫步图)
      • [1.3.4 模拟多次随机漫步](#1.3.4 模拟多次随机漫步)
      • [1.3.5 设置随机漫步图的样式](#1.3.5 设置随机漫步图的样式)
        • [01. 给点着色](#01. 给点着色)
        • [02. 重新绘制起点和终点](#02. 重新绘制起点和终点)
        • [03. 隐藏坐标轴](#03. 隐藏坐标轴)
        • [04. 增加点数](#04. 增加点数)
        • [05. 调整尺寸以适合屏幕](#05. 调整尺寸以适合屏幕)
    • [1.4 使用Plotly模拟掷骰子](#1.4 使用Plotly模拟掷骰子)
      • [1.4.1 安装Plotly](#1.4.1 安装Plotly)
      • [1.4.2 创建Die类](#1.4.2 创建Die类)
      • [1.4.3 掷骰子](#1.4.3 掷骰子)
      • [1.4.4 分析结果](#1.4.4 分析结果)
      • [1.4.5 绘制直方图](#1.4.5 绘制直方图)
      • [1.4.6 同时掷两个骰子](#1.4.6 同时掷两个骰子)
      • [1.4.7 同时掷两个面数不同的骰子](#1.4.7 同时掷两个面数不同的骰子)
  • [2. 下载数据](#2. 下载数据)
    • [2.1 CSV文件格式](#2.1 CSV文件格式)
      • [2.1.1 分析CSV文件头](#2.1.1 分析CSV文件头)
      • [2.1.2 打印文件头及其位置](#2.1.2 打印文件头及其位置)
      • [2.1.3 提取并读取数据](#2.1.3 提取并读取数据)
      • [2.1.4 绘制温度图表](#2.1.4 绘制温度图表)
      • [2.1.5 模块datetime](#2.1.5 模块datetime)
      • [2.1.6 在图表中添加日期](#2.1.6 在图表中添加日期)
      • [2.1.7 涵盖更长的时间](#2.1.7 涵盖更长的时间)
      • [2.1.8 再绘制一个数据系列](#2.1.8 再绘制一个数据系列)
      • [2.1.9 给图表区域着色](#2.1.9 给图表区域着色)
      • [2.1.10 错误检查](#2.1.10 错误检查)
      • [2.1.11 自己动手下载数据](#2.1.11 自己动手下载数据)
    • [2.2 制作全球地震散点图:JSON格式](#2.2 制作全球地震散点图:JSON格式)
      • [2.2.1 地震数据](#2.2.1 地震数据)
      • [2.2.2 查看JSON数据](#2.2.2 查看JSON数据)
      • [2.2.3 创建地震列表](#2.2.3 创建地震列表)
      • [2.2.4 提取震级](#2.2.4 提取震级)
      • [2.2.5 提取位置数据](#2.2.5 提取位置数据)
      • [2.2.6 绘制震级散点图](#2.2.6 绘制震级散点图)
      • [2.2.7 另一种指定图表数据的方式](#2.2.7 另一种指定图表数据的方式)
      • [2.2.8 定制标记的尺寸](#2.2.8 定制标记的尺寸)
      • [2.2.9 定制标记的颜色](#2.2.9 定制标记的颜色)
      • [2.2.10 其他渐变](#2.2.10 其他渐变)
      • [2.2.11 添加鼠标指向时显示的文本](#2.2.11 添加鼠标指向时显示的文本)
  • [3. 使用API](#3. 使用API)
    • [3.1 使用Web API](#3.1 使用Web API)
      • [3.1.1 Git和GitHub](#3.1.1 Git和GitHub)
      • [3.1.2 使用API调用请求数据](#3.1.2 使用API调用请求数据)
      • [3.1.3 安装Requests](#3.1.3 安装Requests)
      • [3.1.4 处理API响应](#3.1.4 处理API响应)
      • [3.1.5 处理响应字典](#3.1.5 处理响应字典)
      • [3.1.6 概述最受欢迎的仓库](#3.1.6 概述最受欢迎的仓库)
      • [3.1.7 监视API的速率限制](#3.1.7 监视API的速率限制)
    • [3.2 使用Plotly可视化仓库](#3.2 使用Plotly可视化仓库)
      • [3.2.1 设计图形样式](#3.2.1 设计图形样式)
      • [3.2.2 添加自定义工具提示](#3.2.2 添加自定义工具提示)
      • [2.2.3 在图表中添加可单击的链接](#2.2.3 在图表中添加可单击的链接)
      • [3.2.4 定制标记颜色](#3.2.4 定制标记颜色)
      • [3.2.5 深入了解Plotly和GitHub API](#3.2.5 深入了解Plotly和GitHub API)
    • [3.3 Hacker News API](#3.3 Hacker News API)
  • [4. 小结](#4. 小结)

1.生成数据

数据可视化指的是通过可视化表示来探索数据。它与数据分析紧密相关,而数据分析指的是使用代码来探索数据集的规律和关联。数据集可以是用一行代码就能表示的小型数字列表,也可以是数千兆字节的数据。

漂亮地呈现数据并非仅仅关乎漂亮的图片。通过以引人注目的简单方式呈现数据,能让观看者明白其含义:发现数据集中原本未知的规律和意义。

数据点并非必须是数,也可对非数值数据进行分析。在基因研究、天气研究、政治经济分析等众多领域,人们常常使用Python来完成数据密集型工作。最流行的工具之一是Matplotlib,它是一个数学绘图库,我们将使用它来制作简单的图表,如折线图和散点图。然后,我们将基于随机漫步概念生成一个更有趣的数据集------根据一系列随机决策生成的图表。

Plotly生成的图表可根据显示设备的尺寸自动调整大小,还具备众多交互特性,如在用户将鼠标指向图表的不同部分时突出数据集的特定方面。本章将使用Plotly来分析掷骰子的结果。

1.1 安装Matplotlib

这里将首先使用Matplotlib来生成几个图表,为此需要使用pip来安装它。pip是一个可用于下载并安装Python包的模块。请在终端提示符下执行如下命令:
python -m pip install --user matplotlib

这个命令让Python运行模块pip,并将matplotlib包添加到当前用户的Python安装中。在你的系统中,如果运行程序或启动终端会话时使用的命令不是python,而是python3,应使用类似于下面的命令:
python3 -m pip install --user matplotlib

注意 在macOS系统中,如果这样不管用,请尝试在不指定标志--user的情况下再次执行该命令。

要查看使用Matplotlib可制作的各种图表,请访问其官方网站,浏览示例画廊。通过单击画廊中的图表,可查看生成它们的代码。

1.2 绘制简单的折线图

下面使用Matplotlib绘制一个简单的折线图,再对其进行定制,以实现信息更丰富的数据可视化效果。我们将使用平方数序列1、4、9、16和25来绘制这个图表。

只需提供如下的数,Matplotlib将完成其他工作:

py 复制代码
import matplotlib.pyplot as plt

squares = [1, 2, 9, 16, 25]
fig, ax = plt.subplots()
ax.plot(squares)

plt.show()

首先导入模块pyplot,并为其指定别名plt,以免反复输入pyplot。(在线示例大多这样做,这里也不例外。)模块pyplot包含很多用于生成图表的函数。

我们创建了一个名为squares的列表,在其中存储要用来制作图表的数据。然后,采取了另一种常见的Matplotlib做法------调用函数subplots()。这个函数可在一张图片中绘制一个或多个图表。变量fig表示整张图片。变量ax表示图片中的各个图表,大多数情况下要使用它。

接下来调用方法plot(),它尝试根据给定的数据以有意义的方式绘制图表。函数plt.show()打开Matplotlib查看器并显示绘制的图表,在查看器中,你可缩放和导航图形,还可单击磁盘图标将图表保存起来。

1.2.1 修改标签文字和线条粗细

如图所示的图形表明数是越来越大的,但标签文字太小、线条太细,难以看清楚。所幸Matplotlib让你能够调整可视化的各个方面。

下面通过一些定制来改善这个图表的可读性,如下所示:

py 复制代码
import matplotlib.pyplot as plt

from pylab import mpl
mpl.rcParams['font.sans-serif'] = ['SimHei']    # 设置默认字体

squares = [1, 2, 9, 16, 25]

fig, ax = plt.subplots()
ax.plot(squares, linewidth=3)   # 决定plot()绘制的线条粗细

# 设置图表标题并给坐标轴加上标签
ax.set_title("平方数", fontsize=24)    # 指定标题
ax.set_xlabel("值", fontsize=14)     # 方法set_xlabel()和set_ylabel()让你能够为每条轴设置标题
ax.set_ylabel("值的平方", fontsize=14)

# 设置刻度标记的大小
ax.tick_params(axis='both', labelsize=14)

plt.show()

1.2.2 校正图形

图形更容易看清后,我们发现没有正确地绘制数据:折线图的终点指出4.0的平方为25!下面来修复这个问题。

向plot()提供一系列数时,它假设第一个数据点对应的 坐标值为0,但这里第一个点对应的 值为1。为改变这种默认行为,可向plot()同时提供输入值和输出值:

py 复制代码
import matplotlib.pyplot as plt

from pylab import mpl
mpl.rcParams['font.sans-serif'] = ['SimHei']    # 设置默认字体

input_values = [1, 2, 3, 4, 5]
squares = [1, 2, 9, 16, 25]

fig, ax = plt.subplots()
ax.plot(input_values, squares, linewidth=3)   # 决定plot()绘制的线条粗细

# 设置图表标题并给坐标轴加上标签
ax.set_title("平方数", fontsize=24)    # 指定标题
ax.set_xlabel("值", fontsize=14)     # 方法set_xlabel()和set_ylabel()让你能够为每条轴设置标题
ax.set_ylabel("值的平方", fontsize=14)

# 设置刻度标记的大小
ax.tick_params(axis='both', labelsize=14)

plt.show()

使用plot()时可指定各种实参,还可使用众多函数对图形进行定制。本章后面处理更有趣的数据集时,将继续探索这些定制函数。

1.2.3 使用内置样式

Matplotlib提供了很多已经定义好的样式,它们使用的背景色、网格线、线条粗细、字体、字号等设置很不错,让你无须做太多定制就可生成引人瞩目的可视化效果。要获悉在系统中可使用哪些样式,可在终端会话中执行如下命令:

py 复制代码
import matplotlib.pyplot as plt
print(plt.style.available)

结果如下:

py 复制代码
['Solarize_Light2', '_classic_test_patch', '_mpl-gallery', '_mpl-gallery-nogrid', 'bmh', 'classic', 'dark_background', 'fast', 'fivethirtyeight', 'ggplot', 'grayscale', 'seaborn-v0_8', 'seaborn-v0_8-bright', 'seaborn-v0_8-colorblind', 'seaborn-v0_8-dark', 'seaborn-v0_8-dark-palette', 'seaborn-v0_8-darkgrid', 'seaborn-v0_8-deep', 'seaborn-v0_8-muted', 'seaborn-v0_8-notebook', 'seaborn-v0_8-paper', 'seaborn-v0_8-pastel', 'seaborn-v0_8-poster', 'seaborn-v0_8-talk', 'seaborn-v0_8-ticks', 'seaborn-v0_8-white', 'seaborn-v0_8-whitegrid', 'tableau-colorblind10']

要使用这些样式,可在生成图表的代码前添加如下代码行:

py 复制代码
import matplotlib.pyplot as plt


input_values = [1, 2, 3, 4, 5]
squares = [1, 2, 9, 16, 25]

plt.style.use('seaborn')    # 样式
fig, ax = plt.subplots()
ax.plot(input_values, squares, linewidth=3)   # 决定plot()绘制的线条粗细

# 设置图表标题并给坐标轴加上标签
ax.set_title("平方数", fontsize=24)    # 指定标题
ax.set_xlabel("值", fontsize=14)     # 方法set_xlabel()和set_ylabel()让你能够为每条轴设置标题
ax.set_ylabel("值的平方", fontsize=14)

# 设置刻度标记的大小
ax.tick_params(axis='both', labelsize=14)

plt.rcParams['font.sans-serif']=['SimHei']   # 设置默认字体
plt.show()

1.2.4 使用scatter()绘制散点图并设置样式

有时候,绘制散点图并设置各个数据点的样式很有用。例如,你可能想以一种颜色显示较小的值,用另一种颜色显示较大的值。绘制大型数据集时,还可对每个点都设置同样的样式,再使用不同的样式选项重新绘制某些点以示突出。

要绘制单个点,可使用方法scatter()。向它传递一对x坐标和y坐标,它将在指定位置绘制一个点:

py 复制代码
import matplotlib.pyplot as plt

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(2, 4)

plt.show()

下面来设置图表的样式,使其更有趣。我们将添加标题,给坐标轴加上标签,并且确保所有文本都大到能够看清:

py 复制代码
import matplotlib.pyplot as plt

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(2, 4, s=200)

# 设置图表标题并给坐标轴加上标签
ax.set_title("平方数", fontsize=24)
ax.set_xlabel("值", fontsize=14)
ax.set_ylabel("值的平方", fontsize=14)

# 设置刻度标记的大小
ax.tick_params(axis='both', which='major', labelsize=14)

plt.rcParams['font.sans-serif']=['SimHei']   # 设置默认字体
plt.show()

1.2.5 使用scatter()绘制一系列点

要绘制一系列的点,可向scatter()传递两个分别包含x值和y值的列表。

py 复制代码
import matplotlib.pyplot as plt

x_values = [1, 2, 3, 4, 5]
y_values = [1, 4, 9, 16, 25]

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, s=100)

# 设置图表标题并给坐标轴加上标签
ax.set_title("平方数", fontsize=24)
ax.set_xlabel("值", fontsize=14)
ax.set_ylabel("值的平方", fontsize=14)

# 设置刻度标记的大小
ax.tick_params(axis='both', which='major', labelsize=14)

plt.rcParams['font.sans-serif']=['SimHei']   # 设置默认字体
plt.show()

列表x_values包含要计算平方值的数,列表y_values包含前述数的平方值。将这些列表传递给scatter()时,Matplotlib依次从每个列表中读取一个值来绘制一个点。要绘制的点的坐标分别为 (1, 1)、(2, 4)、(3, 9)、(4, 16)和(5, 25),最终的结果如图。

1.2.6 自动计算数据

手工计算列表要包含的值可能效率低下,需要绘制的点很多时尤其如此。我们不必手工计算包含点坐标的列表,可以用Python循环来完成。下面是绘制1000个点的代码。

py 复制代码
import matplotlib.pyplot as plt

x_values = range(1, 1001)
y_values = [x**2 for x in x_values]

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, s=10)

# 设置图表标题并给坐标轴加上标签
ax.set_title("平方数", fontsize=24)
ax.set_xlabel("值", fontsize=14)
ax.set_ylabel("值的平方", fontsize=14)

# 设置刻度标记的大小
ax.tick_params(axis='both', which='major', labelsize=14)

# 设置每个坐标轴的取值范围
ax.axis([0, 1100, 0, 1100000])

plt.rcParams['font.sans-serif'] = ['SimHei']   # 设置默认字体
plt.show()

首先创建了一个包含 值的列表,其中包含数1~1000。接下来,是一个生成 值的列表解析,它遍历x值(for x in x_values),计算其平方值(x**2),并将结果存储到列表y_values中。然后,将输入列表和输出列表传递给scatter()。这个数据集较大,因此将点设置得较小。

使用方法axis()指定了每个坐标轴的取值范围。方法axis()要求提供4个值:x和y坐标轴的最小值和最大值。这里将 坐标轴的取值范围设置为0~1100,并将 坐标轴的取值范围设置为0~1 100 000。

3.2.7 自定义颜色

要修改数据点的颜色,可向scatter()传递参数c,并将其设置为要使用的颜色的名称(放在引号内)

py 复制代码
import matplotlib.pyplot as plt

x_values = range(1, 1001)
y_values = [x**2 for x in x_values]

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, c='red', s=10)

# 设置图表标题并给坐标轴加上标签
ax.set_title("平方数", fontsize=24)
ax.set_xlabel("值", fontsize=14)
ax.set_ylabel("值的平方", fontsize=14)

# 设置刻度标记的大小
ax.tick_params(axis='both', which='major', labelsize=14)

# 设置每个坐标轴的取值范围
ax.axis([0, 1100, 0, 1100000])

plt.rcParams['font.sans-serif'] = ['SimHei']   # 设置默认字体
plt.show()

还可使用RGB颜色模式自定义颜色。要指定自定义颜色,可传递参数c,并将其设置为一个元组,其中包含三个0~1的小数值,分别表示红色、绿色和蓝色的分量。

py 复制代码
ax.scatter(x_values, y_values, c=(0, 0.8, 0), s=10)

值越接近0,指定的颜色越深;值越接近1,指定的颜色越浅。

1.2.8 使用颜色映射

颜色映射(colormap)是一系列颜色,从起始颜色渐变到结束颜色。在可视化中,颜色映射用于突出数据的规律。例如,你可能用较浅的颜色来显示较小的值,并使用较深的颜色来显示较大的值。

模块pyplot内置了一组颜色映射。要使用这些颜色映射,需要告诉pyplot该如何设置数据集中每个点的颜色。下面演示了如何根据每个点的 值来设置其颜色:

py 复制代码
import matplotlib.pyplot as plt

x_values = range(1, 1001)
y_values = [x**2 for x in x_values]

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Blues, s=10)


# 设置图表标题并给坐标轴加上标签
ax.set_title("平方数", fontsize=24)
ax.set_xlabel("值", fontsize=14)
ax.set_ylabel("值的平方", fontsize=14)

# 设置刻度标记的大小
ax.tick_params(axis='both', which='major', labelsize=14)

# 设置每个坐标轴的取值范围
ax.axis([0, 1100, 0, 1100000])

plt.rcParams['font.sans-serif'] = ['SimHei']   # 设置默认字体
plt.show()

我们将参数c设置成了一个 值列表,并使用参数cmap告诉pyplot使用哪个颜色映射。这些代码将y值较小的点显示为浅蓝色,并将y值较大的点显示为深蓝色。

注意 要了解pyplot中所有的颜色映射,请访问Matplotlib网站主页,单击Examples,向下滚动到Color,再单击Colormaps reference。

1.2.9 自动保存图表

要让程序自动将图表保存到文件中,可将调用plt.show()替换为调用plt.savefig():

py 复制代码
plt.savefig('squares_plot.png', bbox_inches='tight')
py 复制代码
import matplotlib.pyplot as plt

x_values = range(1, 1001)
y_values = [x**2 for x in x_values]

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Blues, s=10)


# 设置图表标题并给坐标轴加上标签
ax.set_title("平方数", fontsize=24)
ax.set_xlabel("值", fontsize=14)
ax.set_ylabel("值的平方", fontsize=14)

# 设置刻度标记的大小
ax.tick_params(axis='both', which='major', labelsize=14)

# 设置每个坐标轴的取值范围
ax.axis([0, 1100, 0, 1100000])

plt.rcParams['font.sans-serif'] = ['SimHei']   # 设置默认字体
plt.savefig('squares_plot.png', bbox_inches='tight')    # 保存图片文件
# plt.show()

第一个实参指定要以什么文件名保存图表,这个文件将存储到scatter_squares.py所在的目录。第二个实参指定将图表多余的空白区域裁剪掉。如果要保留图表周围多余的空白区域,只需省略这个实参即可。

1.3 随机漫步

这里使用Python来生成随机漫步数据,再使用Matplotlib以引人瞩目的方式将这些数据呈现出来。随机漫步是这样行走得到的路径:每次行走都是完全随机的、没有明确的方向,结果是由一系列随机决策决定的。你可以将随机漫步看作蚂蚁在晕头转向的情况下,每次都沿随机的方向前行所经过的路径。

在自然界、物理学、生物学、化学和经济领域,随机漫步都有其实际用途。例如,漂浮在水滴上的花粉因不断受到水分子的挤压而在水面上移动。水滴中的分子运动是随机的,因此花粉在水面上的运动路径犹如随机漫步。我们稍后编写的代码将模拟现实世界的很多情形。

1.3.1 创建RandomWalk类

为模拟随机漫步,将创建一个名为RandomWalk的类,它随机地选择前进方向。这个类需要三个属性:一个是存储随机漫步次数的变量,其他两个是列表,分别存储随机漫步经过的每个点的x坐标和y坐标。

RandomWalk类只包含两个方法:方法__init___()和fill_walk(),后者计算随机漫步经过的所有点。先来看看__init__(),如下所示:

py 复制代码
from random import choice

class RandomWalk:
    """一个生成随机漫步数据的类"""
    def __init__(self, num_points=5000):
        """初始化随机漫步的属性"""
        self.num_points = num_points

        # 所有随机漫步都始于(0, 0)
        self.x_values = [0]
        self.y_values = [0]

为做出随机决策,将所有可能的选择都存储在一个列表中,并在每次决策时都使用模块random中的choice()来决定使用哪种选择。接下来,将随机漫步包含的默认点数设置为5000。这个数大到足以生成有趣的模式,又小到可确保能够快速地模拟随机漫步。然后创建两个用于存储 值和 值的列表,并让每次漫步都从点(0,0)出发。

1.3.2 选择方向

我们将使用方法fill_walk()来生成漫步包含的点并决定每次漫步的方向,如下所示。

py 复制代码
from random import choice


class RandomWalk:
    """一个生成随机漫步数据的类"""
    def __init__(self, num_points=5000):
        """初始化随机漫步的属性"""
        self.num_points = num_points

        # 所有随机漫步都始于(0, 0)
        self.x_values = [0]
        self.y_values = [0]

    def fill_walk(self):
        """计算随机漫步包含的所有点"""
        # 不断漫步,直到列表达到指定的长度
        while len(self.x_values) < self.num_points:
            # 决定前进方向以及沿这个方向前进的距离
            x_direction = choice([1, -1])

            x_distance = choice([0, 1, 2, 3, 4])
            x_step = x_direction * x_distance

            y_direction = choice([1, -1])
            y_distance = choice([0, 1, 2, 3, 4])
            y_step = y_direction * y_distance

            # 拒绝原地踏步
            if x_step == 0 and y_step == 0:
                continue

            # 计算下一个点的x值和y值
            x = self.x_values[-1] + x_step
            y = self.y_values[-1] + y_step

            self.x_values.append(x)
            self.y_values.append(y)

建立一个循环,它不断运行,直到漫步包含所需的点数。方法fill_walk()的主要部分告诉Python如何模拟四种漫步决定:向右走还是向左走?沿指定的方向走多远?向上走还是向下走?沿选定的方向走多远?

使用choice([1, -1])给x_direction选择一个值,结果要么是表示向右走的1,要么是表示向左走的-1。接下来,choice([0, 1, 2, 3, 4])随机地选择一个0~4的整数,告诉Python 沿指定的方向走多远(x_distance)。通过包含0,不仅能够同时沿两个轴移动,还能够只沿一个轴移动。

将移动方向乘以移动距离,确定沿x轴和y轴移动的距离。如果x_step为正将向右移动,为负将向左移动,为零将垂直移动;如果y_step为正将向上移动,为负将向下移动,为零将水平移动。如果x_step和y_step都为零,则意味着原地踏步。我们拒绝这样的情况,接着执行下一次循环。

为获取漫步中下一个点的 值,将x_step与x_values中的最后一个值相加,对值也做相同的处理。获得下一个点的x值和y值后,将它们分别附加到列表x_values和y_values的末尾。

1.3.3 绘制随机漫步图

下面的代码将随机漫步的所有点都绘制出来:

py 复制代码
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 创建一个RandomWalk实例
rw = RandomWalk()
rw.fill_walk()

# 将所有点绘制出来
plt.style.use('classic')
fig, ax = plt.subplots()
ax.scatter(rw.x_values, rw.y_values, s=15)

plt.show()

首先导入模块pyplot和RandomWalk类,再创建一个RandomWalk实例并将其存储到rw中

,并且调用fill_walk()。将随机漫步包含的 值和 值传递给scatter(),并选择合适的点尺寸。如下图显示了包含5000个点的随机漫步图。

1.3.4 模拟多次随机漫步

每次随机漫步都不同,因此探索可能生成的各种模式很有趣。要在不多次运行程序的情况下使用前面的代码模拟多次随机漫步,一种办法是将这些代码放在一个while循环中,如下所示:

py 复制代码
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 只要程序处于活动状态,就不断地模拟随机漫步。
while True:
    # 创建一个RandomWalk实例。
    rw = RandomWalk()
    rw.fill_walk()

    # 将所有的点都绘制出来。
    plt.style.use('classic')
    fig, ax = plt.subplots()
    ax.scatter(rw.x_values, rw.y_values, s=15)
    plt.show()

    keep_running = input("Make another walk? (y/n): ")
    if keep_running == 'n':
        break

这些代码模拟一次随机漫步,在Matplotlib查看器中显示结果,再在不关闭查看器的情况下暂停。如果关闭查看器,程序将询问是否要再模拟一次随机漫步。如果输入y,可模拟在起点附近进行的随机漫步、大多沿特定方向偏离起点的随机漫步、漫步点分布不均匀的随机漫步,等等。要结束程序,请输入n。

1.3.5 设置随机漫步图的样式

定制图表,以突出每次漫步的重要特征,并让分散注意力的元素不那么显眼。为此,我们确定要突出的元素,如漫步的起点、终点和经过的路径。接下来确定要使其不那么显眼的元素,如刻度标记和标签。最终的结果是简单的可视化表示,清楚地指出了每次漫步经过的路径。

01. 给点着色

我们将使用颜色映射来指出漫步中各点的先后顺序,并删除每个点的黑色轮廓,让其颜色更为明显。为根据漫步中各点的先后顺序来着色,传递参数c,并将其设置为一个列表,其中包含各点的先后顺序。这些点是按顺序绘制的,因此给参数c指定的列表只需包含数0~4999,如下所示:

py 复制代码
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 只要程序处于活动状态,就不断地模拟随机漫步
while True:
    # 创建一个RandomWalk实例
    rw = RandomWalk()
    rw.fill_walk()

    # 将所有的点都绘制出来
    plt.style.use('classic')
    fig, ax = plt.subplots()
    point_numbers = range(rw.num_points)
    ax.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, edgecolors='none', s=15)
    plt.show()

    keep_running = input("Make another walk? (y/n): ")
    if keep_running == 'n':
        break

使用range()生成了一个数字列表,其中包含的数与漫步包含的点数量相同。接下来,将这个列表存储在point_numbers中,以便后面使用它来设置每个漫步点的颜色。将参数c设置为point_numbers,指定使用颜色映射Blues,并传递实参edgecolors='none'以删除每个点周围的轮廓。最终的随机漫步图从浅蓝色渐变为深蓝色。

02. 重新绘制起点和终点

除了给随机漫步的各个点着色,以指出其先后顺序外,如果还能呈现随机漫步的起点和终点就好了。为此,可在绘制随机漫步图后重新绘制起点和终点。这里让起点和终点更大并显示为不同的颜色。

py 复制代码
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 只要程序处于活动状态,就不断地模拟随机漫步
while True:
    # 创建一个RandomWalk实例
    rw = RandomWalk()
    rw.fill_walk()

    # 将所有的点都绘制出来
    plt.style.use('classic')
    fig, ax = plt.subplots()
    point_numbers = range(rw.num_points)
    ax.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, edgecolors='none', s=15)

    # 突出起点和终点
    ax.scatter(0, 0, c='green', edgecolors='none', s=100)
    ax.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none', s=100)

    plt.show()

    keep_running = input("Make another walk? (y/n): ")
    if keep_running == 'n':
        break

为突出起点,使用绿色绘制点(0, 0),并使其比其他点大(s=100)。为突出终点,在漫步包含的最后一个 值和 值处绘制一个点,将其颜色设置为红色,并将尺寸设置为100。务必将这些代码放在调用plt.show()的代码前面,确保在其他点之上绘制起点和终点。

03. 隐藏坐标轴

下面来隐藏这个图表的坐标轴,以免分散观察者对随机漫步路径的注意力。要隐藏坐标轴,可使用如下代码:

py 复制代码
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 只要程序处于活动状态,就不断地模拟随机漫步
while True:
    # 创建一个RandomWalk实例
    rw = RandomWalk()
    rw.fill_walk()

    # 将所有的点都绘制出来
    plt.style.use('classic')
    fig, ax = plt.subplots()
    point_numbers = range(rw.num_points)
    ax.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, edgecolors='none', s=15)

    # 突出起点和终点
    ax.scatter(0, 0, c='green', edgecolors='none', s=100)
    ax.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none', s=100)

    # 隐藏坐标轴
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    plt.show()

    keep_running = input("Make another walk? (y/n): ")
    if keep_running == 'n':
        break

为修改坐标轴,使用方法ax.get_xaxis()和ax.get_yaxis()将每条坐标轴的可见性都设置为False。随着对数据可视化的不断学习和实践,你会经常看到这种串接方法的方式。

04. 增加点数

下面来增加点数,以提供更多数据。为此,在创建RandomWalk实例时增大num_points的值,并在绘图时调整每个点的大小。

py 复制代码
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 只要程序处于活动状态,就不断地模拟随机漫步
while True:
    # 创建一个RandomWalk实例
    rw = RandomWalk(50_000)
    rw.fill_walk()

    # 将所有的点都绘制出来
    plt.style.use('classic')
    fig, ax = plt.subplots()
    point_numbers = range(rw.num_points)
    ax.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, edgecolors='none', s=1)

    # 突出起点和终点
    ax.scatter(0, 0, c='green', edgecolors='none', s=100)
    ax.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none', s=100)

    # 隐藏坐标轴
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    plt.show()

    keep_running = input("Make another walk? (y/n): ")
    if keep_running == 'n':
        break

这个示例模拟了一次包含50 000个点的随机漫步(以模拟现实情况),并将每个点的大小都设置为1。最终的随机漫步图更稀疏,犹如云朵。

05. 调整尺寸以适合屏幕

图表适合屏幕大小时,更能有效地将数据中的规律呈现出来。为让绘图窗口更适合屏幕大小,可以像下面这样调整Matplotlib输出的尺寸:

py 复制代码
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 只要程序处于活动状态,就不断地模拟随机漫步
while True:
    # 创建一个RandomWalk实例
    rw = RandomWalk(50_000)
    rw.fill_walk()

    # 将所有的点都绘制出来
    plt.style.use('classic')
    fig, ax = plt.subplots(figsize=(15, 9))		# 屏幕尺寸
    point_numbers = range(rw.num_points)
    ax.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, edgecolors='none', s=1)

    # 突出起点和终点
    ax.scatter(0, 0, c='green', edgecolors='none', s=100)
    ax.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none', s=100)

    # 隐藏坐标轴
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    plt.show()

    keep_running = input("Make another walk? (y/n): ")
    if keep_running == 'n':
        break

创建图表时,可传递参数figsize以指定生成的图形的尺寸。需要给参数figsize指定一个元组,向Matplotlib指出绘图窗口的尺寸,单位为英寸 。

Matplotlib假定屏幕分辨率为100像素/英寸。如果上述代码指定的图表尺寸不合适,可根据需要调整数字。如果知道当前系统的分辨率,可通过参数dpi向plt.subplots()传递该分辨率,以有效利用可用的屏幕空间。

py 复制代码
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 只要程序处于活动状态,就不断地模拟随机漫步
while True:
    # 创建一个RandomWalk实例
    rw = RandomWalk(50_000)
    rw.fill_walk()

    # 将所有的点都绘制出来
    plt.style.use('classic')
    fig, ax = plt.subplots(figsize=(10, 6), dpi=128)     # 屏幕尺寸
    point_numbers = range(rw.num_points)
    ax.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, edgecolors='none', s=1)

    # 突出起点和终点
    ax.scatter(0, 0, c='green', edgecolors='none', s=100)
    ax.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none', s=100)

    # 隐藏坐标轴
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    plt.show()

    keep_running = input("Make another walk? (y/n): ")
    if keep_running == 'n':
        break

1.4 使用Plotly模拟掷骰子

这里将使用Python包Plotly来生成交互式图表。需要创建在浏览器中显示的图表时,Plotly很有用,因为它生成的图表将自动缩放以适合观看者的屏幕。Plotly生成的图表还是交互式的:用户将鼠标指向特定元素时,将突出显示有关该元素的信息。

在这个项目中,我们将对掷骰子的结果进行分析。抛掷一个6面的常规骰子时,可能出现的结果为1~6点,且出现每种结果的可能性相同。然而,如果同时掷两个骰子,某些点数出现的可能性将比其他点数大。为确定哪些点数出现的可能性最大,将生成一个表示掷骰子结果的数据集,并根据结果绘制一个图形。

在数学领域,掷骰子常被用来解释各种数据分析类型,而它在赌场和其他博弈场景中也有实际应用,在游戏《大富翁》以及众多角色扮演游戏中亦如此。

1.4.1 安装Plotly

要安装Plotly,可像本章前面安装Matplotlib那样使用pip:
python -m pip install --user plotly

在前面安装Matplotlib时,如果使用了python3之类的命令,这里也要使用同样的命令。要了解使用Plotly可创建什么样的图表,请在其官方网站查看图表类型画廊。每个示例包含源代码,让你知道这些图表是如何生成的。

1.4.2 创建Die类

为模拟掷一个骰子的情况,我们创建下面的类:

py 复制代码
from random import randint


class Die:
    """表示一个骰子的类"""
    def __init__(self, num_sides=6):
        """骰子默认为6面"""
        self.num_sides = num_sides

    def roll(self):
        """"返回一个位于1和骰子面数之间的随机值"""
        return randint(1, self.num_sides)
    

方法__init__()接受一个可选参数。创建这个类的实例时,如果没有指定任何实参,面数默认为6;如果指定了实参,这个值将用于设置骰子的面数。骰子是根据面数命名的,6面的骰子名为D6,8面的骰子名为D8,依此类推。

方法roll()使用函数randint()来返回一个1和面数之间的随机数。这个函数可能返回起始值1、终止值num_sides或这两个值之间的任何整数。

1.4.3 掷骰子

使用这个类来创建图表前,先来掷D6,将结果打印出来,并确认结果是合理的:

py 复制代码
from die import Die

# 创建一个D6
die = Die()

# 掷几次骰子并将结果存储在一个列表之中
results = []
for roll_num in range(100):
    result = die.roll()
    results.append(result)

print(results)

创建一个Die实例,其面数为默认值6。掷骰子100次,并将每次的结果都存储在列表results中。

1.4.4 分析结果

为分析掷一个D6的结果,计算每个点数出现的次数:

py 复制代码
from die import Die

# 创建一个D6
die = Die()

# 掷几次骰子并将结果存储在一个列表之中
results = []
for roll_num in range(100):
    result = die.roll()
    results.append(result)

# 分析结果
frequencies = []
for value in range(1, die.num_sides+1):
    frequency = results.count(value)
    frequencies.append(frequency)

print(frequencies)

由于将使用Plotly来分析,而不是将结果打印出来,因此可将模拟掷骰子的次数增加到1000。为分析结果,我们创建空列表frequencies,用于存储每种点数出现的次数。遍历可能的点数(这里为1~6),计算每种点数在results中出现了多少次,并将这个值附加到列表frequencies的末尾。接下来,在可视化之前将这个列表打印出来:

结果看起来是合理的:有6个值,对应掷D6时可能出现的每个点数;另外,没有任何点数

出现的频率比其他点数高很多。下面来可视化这些结果。

1.4.5 绘制直方图

有了频率列表,就可以绘制一个表示结果的直方图了。直方图是一种条形图,指出了各种结果出现的频率。创建这种直方图的代码如下:

py 复制代码
from die import Die

from plotly import offline
from plotly.graph_objs import Bar, Layout

# 创建一个D6
die = Die()

# 掷几次骰子并将结果存储在一个列表之中
results = []
for roll_num in range(1000):
    result = die.roll()
    results.append(result)

# 分析结果
frequencies = []
for value in range(1, die.num_sides+1):
    frequency = results.count(value)
    frequencies.append(frequency)

# 对结果进行可视化
x_values = list(range(1, die.num_sides+1))
data = [Bar(x=x_values, y=frequencies)]
x_axis_config = {'title': '结果'}
y_axis_config = {'title': '结果的频率'}
my_layout = Layout(title='掷一个D6 1000次的结果', xaxis=x_axis_config, yaxis=y_axis_config)
offline.plot({'data': data, 'layout': my_layout}, filename='d6.html')

print(frequencies)

为创建直方图,需要为每个可能出现的点数生成一个条形。我们将可能出现的点数(1到骰子的面数)存储在一个名为x_values的列表中。Plotly不能直接接受函数range()的结果,因此需要使用函数list()将其转换为列表。Plotly类Bar()表示用于绘制条形图的数据集,需要一个存储x值的列表和一个存储y值的列表。这个类必须放在方括号内,因为数据集可能包含多个元素。

每个坐标轴都能以不同的方式进行配置,而每个配置选项都是一个字典元素。这里只设置了坐标轴标签。类Layout()返回一个指定图表布局和配置的对象。这里设置了图表名称,并传入了 轴和 轴的配置字典。

为生成图表,我们调用了函数offline.plot()。这个函数需要一个包含数据和布局对象的字典,还接受一个文件名,指定要将图表保存到哪里。这里将输出存储到文件d6.html。

运行程序die_visual.py时,可能打开浏览器并显示文件d6.html。如果没有自动显示d6.html,可在任意Web浏览器中新建一个标签页,再在其中打开文件d6.html(它位于die_visual.py所在的文件夹中)。

注意,Plotly让这个图表具有交互性:如果将鼠标指向其中的任意条形,就能看到与之相关联的数据。在同一个图表中绘制多个数据集时,这项功能特别有用。另外,注意到右上角有一些图标,让你能够平移和缩放图表以及将其保存为图像。

1.4.6 同时掷两个骰子

同时掷两个骰子时,得到的点数更多,结果分布情况也不同。下面来修改前面的代码,创建两个D6以模拟同时掷两个骰子的情况。每次掷两个骰子时,都将两个骰子的点数相加,并将结果存储在results中。请复制die_visual.py并将其保存为dice_visual.py,再做如下修改:

py 复制代码
from die import Die

from plotly import offline
from plotly.graph_objs import Bar, Layout

# 创建两个D6
die_1 = Die()
die_2 = Die()

# 掷几次骰子并将结果存储在一个列表之中
results = []
for roll_num in range(1000):
    result = die_1.roll() + die_2.roll()
    results.append(result)

# 分析结果
frequencies = []
max_result = die_1.num_sides + die_2.num_sides
for value in range(1, max_result+1):
    frequency = results.count(value)
    frequencies.append(frequency)

# 对结果进行可视化
x_values = list(range(1, max_result+1))
data = [Bar(x=x_values, y=frequencies)]

x_axis_config = {'title': '结果', 'dtick': 1}
y_axis_config = {'title': '结果的频率'}
my_layout = Layout(title='掷两个D6 1000次的结果', xaxis=x_axis_config, yaxis=y_axis_config)
offline.plot({'data': data, 'layout': my_layout}, filename='d6_d6.html')

print(frequencies)

创建两个Die实例后,掷骰子多次,并计算每次的总点数。可能出现的最大点数

为两个骰子的最大可能点数之和(12),这个值存储在max_result中。可能出

现的最小总点数为两个骰子的最小可能点数之和(2)。

分析结果时,计算2到max_result的各种点数出现的次数。(我们原本可以使用range(2, 13),但这只适用于两个D6。模拟现实世界的情形时,最好编写可轻松模拟各种情形的代码。前面的代码让我们能够模拟掷任意两个骰子的情形,不管这些骰子有多少面。)

创建图表时,在字典x_axis_config中使用了dtick键。这项设置指定了 轴显示的刻度间距。这里绘制的直方图包含的条形更多,Plotly默认只显示某些刻度值,而设置'dtick': 1让Plotly显示每个刻度值。另外,我们还修改了图表名称及输出文件名。

这个图表显示了掷两个D6时得到的大致结果。如你所见,总点数为2或12的可能性最小,而总点数为7的可能性最大。这是因为在下面6种情况下得到的总点数都为7:1和6、2和5、3和4、4和3、5和2以及6和1。

1.4.7 同时掷两个面数不同的骰子

下面来创建一个6面骰子和一个10面骰子,看看同时掷这两个骰子50 000次的结果如何:

py 复制代码
from die import Die

from plotly import offline
from plotly.graph_objs import Bar, Layout

# 创建两个D6
die_1 = Die()
die_2 = Die()

# 掷几次骰子并将结果存储在一个列表之中
results = []
for roll_num in range(50_000):
    result = die_1.roll() + die_2.roll()
    results.append(result)

# 分析结果
frequencies = []
max_result = die_1.num_sides + die_2.num_sides
for value in range(1, max_result+1):
    frequency = results.count(value)
    frequencies.append(frequency)

# 对结果进行可视化
x_values = list(range(2, max_result+1))
data = [Bar(x=x_values, y=frequencies)]

x_axis_config = {'title': '结果', 'dtick': 1}
y_axis_config = {'title': '结果的频率'}
my_layout = Layout(title='掷一个D6和一个D10 50000次的结果', xaxis=x_axis_config, yaxis=y_axis_config)
offline.plot({'data': data, 'layout': my_layout}, filename='d6_d10.html')

print(frequencies)

为创建D10,我们在创建第二个Die实例时传递了实参10;修改了第一个循环,以模拟掷骰子50 000而不是1000次;还修改了图表名称和输出文件名。

图显示了最终的图表。可能性最大的点数不止一个,而是有5个。这是因为导致出现最小点数和最大点数的组合都只有一种(1和1以及6和10),但面数较小的骰子限制了得到中间点数的组合数:得到总点数7、8、9、10和11的组合数都是6种。因此,这些总点数是最常见的结果,它们出现的可能性相同。

2. 下载数据

这里将从网上下载数据,并对其进行可视化。网上的数据多得令人难以置信,大多未经仔细检查。如果能够对这些数据进行分析,就能发现别人没有发现的规律和关联。

将访问并可视化的数据以两种常见格式存储:CSV和JSON。我们将使用Python模块csv来处理以CSV格式存储的天气数据,找出两个地区在一段时间内的最高温度和最低温度。然后,使用Matplotlib根据下载的数据创建一个图表,展示两个不同地区的温度变化:阿拉斯加州锡特卡和加利福尼亚州死亡谷。然后,使用模块json访问以JSON格式存储的地震数据,并使用Plotly绘制一幅散点图,展示这些地震的位置和震级。

处理各种类型和格式的数据集,对如何创建复杂的图表有深入的认识。要处理各种真实的数据集,必须能够访问并可视化各种类型和格式的在线数据。

2.1 CSV文件格式

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

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

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

我们将首先处理少量CSV格式的锡特卡天气数据。请将文件sitka_weather_07_2021_simple.csv复制到存储程序的文件夹中。

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

2.1.1 分析CSV文件头

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

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模块和path类后,创建path对象,指向天气数据文件。读取文件后通过splitlines()方法来获取包含文件中各行的列表,再将列表赋给变量lines。调用csv.reader()并将前面存储的文件对象作为实参传递给它,从而创建一个与该文件相关联的阅读器对象。这个阅读器对象被赋给了reader。

模块csv包含函数next(),调用它并传入阅读器对象时,它将返回文件中的下一行。在上述代码中,只调用了next()一次,因此得到的是文件的第一行,其中包含文件头。将返回的数据存储到header_row中。header_row包含与天气相关的文件头,指出了每行都包含哪些数据。

reader处理文件中以逗号分隔的第一行数据,并将每项数据都作为一个元素存储在列表中。文件头STATION表示记录数据的气象站的编码。这个文件头的位置表明,每行的第一个值都是气象站编码。文件头NAME指出每行的第二个值都是记录数据的气象站的名称。

其他文件头则指出记录了哪些信息。当前,我们最关心的是日期(DATE)、最高温度(TMAX)和最低温度(TMIN)。这是一个简单的数据集,只包含降水量以及与温度相关的数据。你自己下载天气数据时,可选择涵盖众多测量值,如风速、风向以及详细的降水量数据。

2.1.2 打印文件头及其位置

为了让文件头数据更容易理解,将列表中的每个文件头及其位置打印出来:

py 复制代码
from pathlib import Path

import csv

path = Path('weather_data/sitka_weather_07-2021_simple.csv')
lines = path.read_text().splitlines()

reader = csv.reader(lines)
header_row = next(reader)
print(header_row)

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

在循环中,对列表调用了enumerate()来获取每个元素的索引及其值。

从中可知,日期和最高温度分别存储在第三列和第六列。为研究这些数据,我们将处理sitka_weather_07-2021_simple.csv中的每行数据,并提取其中索引为2和4的值。

2.1.3 提取并读取数据

知道需要哪些列中的数据后,我们来读取一些数据。首先,读取每天的最高温度

py 复制代码
from pathlib import Path

import csv

path = Path('weather_data/sitka_weather_07-2021_simple.csv')
lines = path.read_text().splitlines()

reader = csv.reader(lines)
header_row = next(reader)

# 提取最高温度
highs = []
for row in reader:
    high = int(row[4])
    highs.append(high)
print(highs)

创建一个名为highs的空列表,再遍历文件中余下的各行。阅读器对象从其停留的地方继续往下读取CSV文件,每次都自动返回当前所处位置的下一行。由于已经读取了文件头行,这个循环将从第二行开始------从这行开始包含的是实际数据。每次执行循环时,都将索引4处(TMAX列)的数据附加到highs末尾。

2.1.4 绘制温度图表

为可视化这些温度数据,首先使用Matplotlib创建一个显示每日最高温度的简单图形。

py 复制代码
from pathlib import Path
import csv

import matplotlib.pyplot as plt

path = Path('weather_data/sitka_weather_07-2021_simple.csv')
lines = path.read_text().splitlines()

reader = csv.reader(lines)
header_row = next(reader)

# 提取最高温度
highs = []
for row in reader:
    high = int(row[4])
    highs.append(high)
print(highs)

# 根具最高温度绘图
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.plot(highs, color='red')

# 设置绘图的格式
ax.set_title("Daily High Temperatures, July 2021", fontsize=24)
ax.set_xlabel('', fontsize=16)
ax.set_ylabel("Temperature(F)", fontsize=16)
ax.tick_params(labelsize=16)

plt.show()

将最高温度列表传给plot(),并传递c='red'以便将数据点绘制为红色。(这里使用红色显示最高温度,用蓝色显示最低温度。)接下来,设置了一些其他的格式,如名称和字号。鉴于还没有添加日期,因此没有给x轴添加标签,但ax.set_xlabel()确实修改了字号,让默认标签更容易看清。

2.1.5 模块datetime

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

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

py 复制代码
from datetime import datetime
first_date = datetime.strptime('2021-07-01', '%Y-%m-%d')
print(first_date)

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

方法strptime()可接受各种实参,并根据它们来决定如何解读日期。

2.1.6 在图表中添加日期

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

py 复制代码
from pathlib import Path
from datetime import datetime
import csv

import matplotlib.pyplot as plt

path = Path('weather_data/sitka_weather_07-2021_simple.csv')
lines = path.read_text().splitlines()

reader = csv.reader(lines)
header_row = next(reader)

# 提取日期和最高温度
dates, highs = [], []
for row in reader:
    current_date = datetime.strptime(row[2], '%Y-%m-%d')
    high = int(row[4])
    dates.append(current_date)
    highs.append(high)


# 根具最高温度绘图
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.plot(dates, highs, color='red')

# 设置绘图的格式
ax.set_title("Daily High Temperatures, July 2021", fontsize=24)
ax.set_xlabel('', fontsize=16)
fig.autofmt_xdate()
ax.set_ylabel("Temperature(F)", fontsize=16)
ax.tick_params(labelsize=16)

plt.show()

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

2.1.7 涵盖更长的时间

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

气数据。

py 复制代码
from pathlib import Path
from datetime import datetime
import csv

import matplotlib.pyplot as plt

path = Path('weather_data/sitka_weather_2021_simple.csv')
lines = path.read_text().splitlines()

reader = csv.reader(lines)
header_row = next(reader)

# 提取日期和最高温度
dates, highs = [], []
for row in reader:
    current_date = datetime.strptime(row[2], '%Y-%m-%d')
    high = int(row[4])
    dates.append(current_date)
    highs.append(high)


# 根具最高温度绘图
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.plot(dates, highs, color='red')

# 设置绘图的格式
ax.set_title("Daily High Temperatures, 2021", fontsize=24)
ax.set_xlabel('', fontsize=16)
fig.autofmt_xdate()
ax.set_ylabel("Temperature(F)", fontsize=16)
ax.tick_params(labelsize=16)

plt.show()

2.1.8 再绘制一个数据系列

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

py 复制代码
from pathlib import Path
from datetime import datetime
import csv

import matplotlib.pyplot as plt

path = Path('weather_data/sitka_weather_2021_simple.csv')
lines = path.read_text().splitlines()

reader = csv.reader(lines)
header_row = next(reader)

# 提取日期,最低温度和最高温度
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)
    highs.append(high)
    lows.append(low)

# 根具最高温度绘图
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.plot(dates, highs, color='red')
ax.plot(dates, lows, color='blue')

# 设置绘图的格式
ax.set_title("Daily High And Low Temperatures, 2021", fontsize=24)
ax.set_xlabel('', fontsize=16)
fig.autofmt_xdate()
ax.set_ylabel("Temperature(F)", fontsize=16)
ax.tick_params(labelsize=16)

plt.show()

添加空列表lows,用于存储最低温度。接下来,从每行的第七列(row[5])提取最低温度并存储。添加调用plot()的代码,以使用蓝色绘制最低温度。最后,修改标题。这样绘制出来的图表如下。

2.1.9 给图表区域着色

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

py 复制代码
from pathlib import Path
from datetime import datetime
import csv

import matplotlib.pyplot as plt

path = Path('weather_data/sitka_weather_2021_simple.csv')
lines = path.read_text().splitlines()

reader = csv.reader(lines)
header_row = next(reader)

# 提取日期,最低温度和最高温度
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)
    highs.append(high)
    lows.append(low)

# 根具最高和最低温度绘图
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.plot(dates, highs, color='red', alpha=0.5)   # alpha设置透明度
ax.plot(dates, lows, color='blue', alpha=0.5)
ax.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1)

# 设置绘图的格式
ax.set_title("Daily High And Low Temperatures, 2021", fontsize=24)
ax.set_xlabel('', fontsize=16)
fig.autofmt_xdate()
ax.set_ylabel("Temperature(F)", fontsize=16)
ax.tick_params(labelsize=16)

plt.show()

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

向fill_between()传递一个 值系列(列表dates),以及两个y值系列(highs和lows)。实facecolor指定填充区域的颜色,还将alpha设置成了较小的值0.1,让填充区域将两个数据系列连接起来的同时不分散观察者的注意力。

2.1.10 错误检查

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

py 复制代码
from pathlib import Path
from datetime import datetime
import csv

import matplotlib.pyplot as plt

path = Path('weather_data/death_valley_2021_simple.csv')
lines = path.read_text().splitlines()

reader = csv.reader(lines)
header_row = next(reader)

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

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

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

py 复制代码
# 从文件中获取日期、最高温度和最低温度。
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)

修改索引,使其对应于这个文件中TMAX和TMIN的位置。

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

py 复制代码
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文件中读取值时执行错误检查代码,对可能出现的异常进行处理,如下所

示:

py 复制代码
# Extract dates, and high and low temperatures.
dates, highs, lows = [], [], []
for row in reader:
    current_date = datetime.strptime(row[2], '%Y-%m-%d')
    try:
        high = int(row[3])
        low = int(row[4])
    except ValueError:
        print(f"Missing data for {current_date}")
    else:
        dates.append(current_date)
        highs.append(high)
        lows.append(low)

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

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

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

2.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。你将收到一封确认邮件,指出收到了你的订单。几分钟后,你将收到另一封邮件,其中包含用于下载数据的链接。

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

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

2.2.1 地震数据

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

2.2.2 查看JSON数据

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

py 复制代码
{"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...

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

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

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

py 复制代码
from pathlib import Path
import json

# 将数据作为字符串读取并转换为python对象
path = Path("eq_data/eq_data_1_day_m1.geojson")
contents = path.read_text()
all_eq_data = json.dumps(contents)

# 将数据文件转换为更易于阅读的版本
path = Path("eq_data/readable_eq_data.geojson")
readable_contents = json.dumps(all_eq_data, indent=4)
path.write_text(readable_contents)

首先将这个数据文件作为字符串进行读取,并使用json.loads()将这个文件的字符串表示转换为对象python对象。我们将整个数据集转换为一个字典,并将其赋给变量all_eq_data。indent指定数据结构中嵌套元素的缩进量。

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

py 复制代码
{
"type": "FeatureCollection",
"metadata": {
"generated": 1550361461000,
"url": "https://earthquake.usgs.gov/earthquakes/.../1.0_day.geojson",
"title": "USGS Magnitude 1.0+ Earthquakes, Past Day",
"status": 200,
"api": "1.7.0",
"count": 158
},
"features": [

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

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

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

py 复制代码
nip--
{
"type": "Feature",
"properties": {
"mag": 0.96,
"title": "M 1.0 - 8km NE of Aguanga, CA"
},
"geometry": {
"type": "Point",
"coordinates": [
 -116.7941667,
 33.4863333,
3.22
]
},

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

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

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

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

2.2.3 创建地震列表

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

py 复制代码
from pathlib import Path
import json

# 将数据作为字符串读取并转换为python对象
path = Path("eq_data/eq_data_1_day_m1.geojson")
contents = path.read_text()
all_eq_data = json.loads(contents)


# 查看数据集中所有的地震
all_eq_dicts = all_eq_data['features']
print(len(all_eq_dicts))

我们提取与键'features'相关联的数据,并将其存储到all_eq_dicts中。我们知道,这个文件记录了160次地震。

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

2.2.4 提取震级

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

py 复制代码
from pathlib import Path
import json

# 将数据作为字符串读取并转换为python对象
path = Path("eq_data/eq_data_1_day_m1.geojson")
contents = path.read_text()
all_eq_data = json.loads(contents)


# 查看数据集中所有的地震
all_eq_dicts = all_eq_data['features']
print(len(all_eq_dicts))

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

print(mags[:10])

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

2.2.5 提取位置数据

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

py 复制代码
from pathlib import Path
import json

# 将数据作为字符串读取并转换为python对象
path = Path("eq_data/eq_data_1_day_m1.geojson")
contents = path.read_text()
all_eq_data = json.loads(contents)


# 查看数据集中所有的地震
all_eq_dicts = all_eq_data['features']
print(len(all_eq_dicts))

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提取该列表中的第一个值,即地震发生位置的经度。

2.2.6 绘制震级散点图

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

py 复制代码
import plotly.express as px

from pathlib import Path
import json

# 将数据作为字符串读取并转换为python对象
path = Path("eq_data/eq_data_1_day_m1.geojson")
contents = path.read_text()
all_eq_data = json.loads(contents)

# 查看数据集中所有的地震
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)

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实例,分别设置 轴为经度[范围是[-200, 200](扩大空间,以便完整显示东西经180° 附近的地震散点)]、 轴为纬度[范围是[-90, 90]],设置散点图显示的宽度和高度均为800像素,并设置标题为"全球地震散点图"。

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

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

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

py 复制代码
fig = px.scatter(
    x=lons,
    y=lats,
    labels={"x": "经度", "y": "纬度"},

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

py 复制代码
import plotly.express as px
import pandas as pd

from pathlib import Path
import json

# 将数据作为字符串读取并转换为python对象
path = Path("eq_data/eq_data_1_day_m1.geojson")
contents = path.read_text()
all_eq_data = json.loads(contents)

# 查看数据集中所有的地震
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)

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

fig = px.scatter(
    data,
    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()

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

2.2.8 定制标记的尺寸

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

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

py 复制代码
fig = px.scatter(
    data,
    x=lons,
    y=lats,
    labels={"x": "经度", "y": "纬度"},
    range_x=[-200, 200],
    range_y=[-90, 90],
    width=800,
    height=800,
    title="全球地震散点图",
    size="震级",
    size_max=10,
)

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

2.2.9 定制标记的颜色

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

py 复制代码
fig = px.scatter(
    data,
    x=lons,
    y=lats,
    labels={"x": "经度", "y": "纬度"},
    range_x=[-200, 200],
    range_y=[-90, 90],
    width=800,
    height=800,
    title="全球地震散点图",
    size="震级",
    size_max=10,
    color="震级",
)

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

2.2.10 其他渐变

Plotly Express有大量的渐变可供选择。要获悉有哪些渐变可供使用,运行下面这个简短的程序

py 复制代码
import plotly.express as px
for key in px.colors.named_colorscales():
    print(key)

Plotly Express将渐变存储在模块colors中。这些渐变是在列表px.colors.named_colorscales()中定义的。

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

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

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

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

py 复制代码
import plotly.express as px
import pandas as pd

from pathlib import Path
import json

# 将数据作为字符串读取并转换为python对象
path = Path("eq_data/eq_data_1_day_m1.geojson")
contents = path.read_text()
all_eq_data = json.loads(contents)

# 查看数据集中所有的地震
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)

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

fig = px.scatter(
    data,
    x=lons,
    y=lats,
    labels={"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()

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

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

3. 使用API

如何编写独立的程序,对获取的数据进行可视化。这个程序将使用Web应用程序编程接口(API)自动请求网站的特定信息而不是整个网页,再对这些信息进行可视化。由于这样编写的程序始终使用最新的数据进行可视化,即便数据瞬息万变,它呈现的信息也是最新的。

3.1 使用Web API

Web API是网站的一部分,用于与使用具体URL请求特定信息的程序交互。这种请求称为

API调用。请求的数据将以易于处理的格式(如JSON或CSV)返回。依赖于外部数据源

的大多数应用程序依赖于API调用,如集成社交媒体网站的应用程序。

3.1.1 Git和GitHub

本章的可视化基于来自GitHub的信息,这是一个让程序员能够协作开发项目的网站。我们

将使用GitHub的API来请求有关该网站中Python项目的信息,再使用Plotly生成交互式可视

化图表,呈现这些项目的受欢迎程度。

GitHub的名字源自Git,后者是一个分布式版本控制系统,帮助人们管理为项目所做的工

作,避免一个人所做的修改影响其他人所做的修改。在项目中实现新功能时,Git跟踪你

对每个文件所做的修改。确定代码可行后,你提交所做的修改,而Git将记录项目最新的

状态。如果犯了错,想撤销所做的修改,你可以轻松地返回到以前的任何可行状态。(要

更深入地了解如何使用Git进行版本控制,请参阅附录D。)GitHub上的项目都存储在仓库

中,后者包含与项目相关联的一切:代码、项目参与者的信息、问题或bug报告,等等。

GitHub用户可以给喜欢的项目加星(star)以表示支持,还可以跟踪自己可能想使用的项

目。在本章中,我们将编写一个程序,自动下载GitHub上星级最高的Python项目的信息,

并对这些信息进行可视化。

3.1.2 使用API调用请求数据

GitHub的API让你能够通过API调用来请求各种信息。要知道API调用是什么样的,请在浏览器的地址栏中输入如下地址并按回车键:

https://api.github.com/search/repositories?q=language:python&sort=stars

这个调用返回GitHub当前托管了多少个Python项目,以及有关最受欢迎的Python仓库的信息。下面来仔细研究这个调用。开头的https://api.github.com/将请求发送到GitHub网站中响应API调用的部分,接下来的search/repositories让API搜索GitHub上的所有仓库。

repositories后面的问号指出需要传递一个实参。q表示查询,而等号(=)让我们能够开始指定查询。我们使用language:python指出只想获取主要语言为Python的仓库的信息。最后的&sort=stars指定将项目按星级排序。

从响应可知,该URL并不适合人工输入,因为它采用了适合程序处理的格式。GitHub目前总共有九百万个Python项目。"incomplete_results"的值为false,由此知道请求是成功的(并非不完的)。倘若GitHub无法处理该API,此处返回的值将为true。接下来的列表中显示了返的"items",其中包含GitHub上最受欢迎的Python项目的详细信息。

3.1.3 安装Requests

Requests包让Python程序能够轻松地向网站请求信息并检查返回的响应。要安装Requests,可使用pip:

py 复制代码
python -m pip install --user requests

这个命令让Python运行模块pip,并在当前用户的Python安装中添加Requests包。如果你运行程序或安装包时使用的是命令python3或其他命令,请务必在这里使用同样的命令。

注意 如果该命令在macOS系统上不管用,可以尝试删除标志--user再次运行。

3.1.4 处理API响应

下面来编写一个程序,它自动执行API调用并处理结果,以找出GitHub上星级最高的Python项目:

py 复制代码

导入模块requests。存储API调用的URL。最新的GitHub API版本为第3版,因此通过指定headers显式地要求使用这个版本的API,再使用requests调用API。

我们调用get()并将URL传递给它,再将响应对象赋给变量r。响应对象包含一个名为status_code的属性,指出了请求是否成功(状态码200表示请求成功)。打印status_code,核实调用是否成功。

这个API返回JSON格式的信息,因此使用方法json()将这些信息转换为一个Python字典,并将结果存储在response_dict中。

注意 像这样简单的调用应该会返回完整的结果集,因此完全可以忽略与'incomplete_results'关联的值。但在执行更复杂的API调用时,应检查这个值。

3.1.5 处理响应字典

将API调用返回的信息存储到字典后,就可处理其中的数据了。我们来生成一些概述这些信息的输出。这是一种不错的方式,可确认收到了期望的信息,进而开始研究感兴趣的信息。

py 复制代码
import requests

# 执行API调用并存储响应
url = 'https://api.github.com/search/repositories'
url += "?q=language:python+sort:stars+stars:>10000"

headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(url, headers=headers)
print(f"Status code: {r.status_code}")

# 将API响应转换为字典
response_dict = r.json()
print(f"Total repositories: {response_dict['total_count']}")
print(f"Compete results: {not response_dict['incomplete_results']}")

# 探索有关仓库的信息
repo_dicts = response_dict['items']
print(f"Repositories returned: {len(repo_dicts)}")

# 研究第一个仓库
repo_dict = repo_dicts[0]
print(f"\nKeys: {len(repo_dict)}")
for key in sorted(repo_dict.keys()):
    print(key)

打印与'total_count'相关联的值,它指出了GitHub总共包含多少个Python仓库。

与'items'关联的值是个列表,其中包含很多字典,而每个字典都包含有关一个Python仓库的信息。将这个字典列表存储在repo_dicts中。接下来,打印repo_dicts的长度,以获悉获得了多少个仓库的信息。

为更深入地了解每个仓库的信息,提取repo_dicts中的第一个字典,并将其存储在repo_dict中。接下来,打印这个字典包含的键数,看看其中有多少信息。打印这个字典的所有键,看看其中包含哪些信息。

py 复制代码
Status code: 200
Total repositories: 436
Compete results: True
Repositories returned: 30

Keys: 80
allow_forking
archive_url
archived
assignees_url
blobs_url
branches_url
clone_url
collaborators_url
comments_url
commits_url
compare_url
contents_url
contributors_url
created_at
default_branch
deployments_url
description
disabled
downloads_url
events_url
fork
forks
forks_count
forks_url
full_name
git_commits_url
git_refs_url
git_tags_url
git_url
has_discussions
has_downloads
has_issues
has_pages
has_projects
has_wiki
homepage
hooks_url
html_url
id
is_template
issue_comment_url
issue_events_url
issues_url
keys_url
labels_url
language
languages_url
license
merges_url
milestones_url
mirror_url
name
node_id
notifications_url
open_issues
open_issues_count
owner
private
pulls_url
pushed_at
releases_url
score
size
ssh_url
stargazers_count
stargazers_url
statuses_url
subscribers_url
subscription_url
svn_url
tags_url
teams_url
topics
trees_url
updated_at
url
visibility
watchers
watchers_count
web_commit_signoff_required

GitHub的API返回有关仓库的大量信息:repo_dict包含78个键。通过仔细查看这些键,可大致知道可提取有关项目的哪些信息。(要准确地获悉API将返回哪些信息,要么阅读文档,要么像这里一样使用代码来查看。)

py 复制代码
import requests

# 执行API调用并存储响应
url = 'https://api.github.com/search/repositories'
url += "?q=language:python+sort:stars+stars:>10000"

headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(url, headers=headers)
print(f"Status code: {r.status_code}")

# 将API响应转换为字典
response_dict = r.json()
print(f"Total repositories: {response_dict['total_count']}")
print(f"Compete results: {not response_dict['incomplete_results']}")

# 探索有关仓库的信息
repo_dicts = response_dict['items']
print(f"Repositories returned: {len(repo_dicts)}")

# 研究第一个仓库
repo_dict = repo_dicts[0]
print(f"\nKeys: {len(repo_dict)}")
for key in sorted(repo_dict.keys()):
    print(key)

# 研究有关仓库的信息
repo_dicts = response_dict['items']
print(f"Repositories returned: {len(repo_dicts)}")

# 研究第一个仓库
repo_dict = repo_dicts[0]

print("\nSelected information about first repository:")
print(f"Name: {repo_dict['name']}")
print(f"Owner: {repo_dict['owner']['login']}")
print(f"Stars: {repo_dict['stargazers_count']}")
print(f"Repository: {repo_dict['html_url']}")
print(f"Created: {repo_dict['created_at']}")
print(f"Updated: {repo_dict['updated_at']}")
print(f"Description: {repo_dict['description']}")

这里打印的值对应于表示第一个仓库的字典中的很多键。打印了项目的名称。项目所有者是由一个字典表示的,因此使用键owner来访问表示所有者的字典,再使用键key来获取所有者的登录名。打印项目获得了多少个星的评级,以及该项目GitHub仓库的URL。接下来,显示项目的创建时间和最后一次更新的时间。最后,打印仓库的描述。

py 复制代码
Selected information about first repository:
Name: public-apis
Owner: public-apis
Stars: 275926
Repository: https://github.com/public-apis/public-apis
Created: 2016-03-20T23:49:42Z
Updated: 2024-01-23T08:48:13Z
Description: A collective list of free APIs

GitHub上星级最高的Python项目为awesome-python,其所有者为用户vinta,有60 000多位GitHub用户给这项目加星了。我们可以看到这个项目仓库的URL,其创建时间为2014年6月,且最近更新了。最后,描述指出项目awesomepython包含一系列深受欢迎的Python资源。

3.1.6 概述最受欢迎的仓库

对这些数据进行可视化时,我们想涵盖多个仓库。下面就来编写一个循环,打印API调用返回的每个仓库的特定信息,以便能够在可视化中包含所有这些信息:

py 复制代码
import requests

# 执行API调用并存储响应
url = 'https://api.github.com/search/repositories'
url += "?q=language:python+sort:stars+stars:>10000"

headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(url, headers=headers)
print(f"Status code: {r.status_code}")

# 将API响应转换为字典
response_dict = r.json()
print(f"Total repositories: {response_dict['total_count']}")
print(f"Compete results: {not response_dict['incomplete_results']}")

# 探索有关仓库的信息
repo_dicts = response_dict['items']
print(f"Repositories returned: {len(repo_dicts)}")

# 研究第一个仓库
repo_dict = repo_dicts[0]
print(f"\nKeys: {len(repo_dict)}")
for key in sorted(repo_dict.keys()):
    print(key)

# 研究有关仓库的信息
repo_dicts = response_dict['items']
print(f"Repositories returned: {len(repo_dicts)}")

print("\nSelected information about each repository:")

for repo_dict in repo_dicts:
    print(f"\nName: {repo_dict['name']}")
    print(f"Owner: {repo_dict['owner']['login']}")
    print(f"Stars: {repo_dict['stargazers_count']}")
    print(f"Repository: {repo_dict['html_url']}")
    print(f"Description: {repo_dict['description']}")

遍历repo_dicts中的所有字典。在这个循环中,打印每个项目的名称、所有者、星级、在GitHub上的URL以及描述:

py 复制代码
Status code: 200
Total repositories: 436
Compete results: True
Repositories returned: 30

Keys: 80
allow_forking
archive_url
archived
assignees_url
blobs_url
branches_url



Name: gpt4free
Owner: xtekky
Stars: 51898
Repository: https://github.com/xtekky/gpt4free
Description: The official gpt4free repository | various collection of powerful language models

Name: gpt_academic
Owner: binary-husky
Stars: 51000
Repository: https://github.com/binary-husky/gpt_academic
Description: 为GPT/GLM等LLM大语言模型提供实用化交互接口,特别优化论文阅读/润色/写作体验,模块化设计,支持自定义快捷按钮&函数插件,支持Python和C++等项目剖析&自译解功能,PDF/LaTex论文翻译&总结功能,支持并行问询多种LLM模型,支持chatglm3等本地模型。接入通义千问, deepseekcoder, 讯飞星火, 文心一言, llama2, rwkv, claude2, moss等。

在上述输出中,有些有趣的项目可能值得一看。但不要在这上面花费太多时间,因为即将创建的可视化图表能让你更容易地看清结果。

3.1.7 监视API的速率限制

大多数API存在速率限制,也就是说,在特定时间内可执行的请求数存在限制。要获悉是否接近了GitHub的限制,请在浏览器中输入https://api.github.com/rate_limit,你将看到类似于下面的响应:

py 复制代码
{
"resources": {
"core": {
"limit": 60,
"remaining": 58,
"reset": 1550385312
},
"search": {
"limit": 10,
"remaining": 8,
 "reset": 1550381772
},

我们关心的信息是搜索API的速率限制。极限为每分钟10个请求,而在当前分钟内,还可执行8个请求。reset值指的是配额将重置的Unix时间或新纪元时间(1970年1月1日午夜后多少秒。用完配额后,你将收到一条简单的响应,由此知道已到达API极限。到达极限后,必须等待配额重置。

注意 很多API要求注册获得API密钥后才能执行API调用。本书编写期间,GitHub没有这样的要求,但获得API密钥后,配额将高得多。

3.2 使用Plotly可视化仓库

有了一些有趣的数据后,我们来进行可视化,呈现GitHub上Python项目的受欢迎程度。我们将创建一个交互式条形图:条形的高度表示项目获得了多少颗星。单击条形将带你进入项目在GitHub上的主页。请复制前面编写的python_repos_visual.py,并将副本修改成下面这样:

py 复制代码
import requests
import plotly.express as px

# 执行API调用并存储响应
url = "https://api.github.com/search/repositories"
url += "?q=language:python+sort:stars+stars:>10000"

headers = {"Accept": "application/vnd.github.v3+json"}
r = requests.get(url, headers=headers)
print(f"Status code: {r.status_code}")

# 处理结果
response_dict = r.json()
print(f"Complete results: {not response_dict['incomplete_results']}")

# 处理有关仓库的信息
repo_dicts = response_dict['items']
repo_links, stars, hover_texts = [], [], []
for repo_dict in repo_dicts:
    # Turn repo names into active links.
    repo_name = repo_dict['name']
    repo_url = repo_dict['html_url']
    repo_link = f"<a href='{repo_url}'>{repo_name}</a>"
    repo_links.append(repo_link)

    stars.append(repo_dict['stargazers_count'])

    # Build hover texts.
    owner = repo_dict['owner']['login']
    description = repo_dict['description']
    hover_text = f"{owner}<br />{description}"
    hover_texts.append(hover_text)

# 可视化
title = "Most-Starred Python Projects on GitHub"
labels = {'x': 'Repository', 'y': 'Stars'}
fig = px.bar(x=repo_links, y=stars, title=title, labels=labels,
             hover_name=hover_texts)

fig.update_layout(title_font_size=28, xaxis_title_font_size=20,
                  yaxis_title_font_size=20)

fig.update_traces(marker_color='SteelBlue', marker_opacity=0.6)

导入Plotly中的Bar类和模块offline。这里也打印API响应的状态,以便知道是否出现了问题。现在不是探索阶段,早已确定了所需的数据是存在的,因此删除部分处理API响应的代码。

接下来,创建两个空列表,用于存储要在图表中呈现的数据。我们需要每个项目的名称,用于给条形添加标签,还需要知道项目获得了多少个星,用于指定条形的高度。在循环中,将每个项目的名称和星级分别附加到这两个列表末尾。

然后,定义列表data。它像之前的列表data一样包含一个字典,指定了图表的类型,并提供了 值和 值: 值为项目名称, 值为项目获得了多少个星。使用字典定义图表的布局。这里没有创建Layout实例,而是创建了一个包含布局规范的字典,并在其中指定了图表的名称以及每个坐标轴的标签。

3.2.1 设计图形样式

Plotly提供了众多定制图形以及设置其样式的方式,可在确定信息被正确的可视化后使用。

对px.bar()做出修改,调用创建的fig对象做进一步调整。

py 复制代码
# 可视化
title = "Most-Starred Python Projects on GitHub"
labels = {'x': 'Repository', 'y': 'Stars'}
fig = px.bar(x=repo_links, y=stars, title=title, labels=labels,
             hover_name=hover_texts)

fig.update_layout(title_font_size=28, xaxis_title_font_size=20,
                  yaxis_title_font_size=20)

fig.update_traces(marker_color='SteelBlue', marker_opacity=0.6)

fig.show()

添加图形标题,并且给坐标轴添加标题。使用fig.update_layout()方法修改一些图形元素。不同的元素命名和修改方式是一致的。

3.2.2 添加自定义工具提示

在Plotly中,将鼠标指向条形将显示其表示的信息。这通常称为工具提示。在本例中,当前显示的是项目获得了多少个星。下面来创建自定义工具提示,以显示项目的描述和所有者。

为生成这样的工具提示,需要再提取一些信息:

py 复制代码
# 处理有关仓库的信息
repo_dicts = response_dict['items']
repo_links, stars, hover_texts = [], [], []
for repo_dict in repo_dicts:
    # Turn repo names into active links.
    repo_name = repo_dict['name']
    repo_url = repo_dict['html_url']
    repo_link = f"<a href='{repo_url}'>{repo_name}</a>"
    repo_links.append(repo_link)

    stars.append(repo_dict['stargazers_count'])

    # 创建悬停文本
    owner = repo_dict['owner']['login']
    description = repo_dict['description']
    hover_text = f"{owner}<br />{description}"
    hover_texts.append(hover_text)

# 可视化
title = "Most-Starred Python Projects on GitHub"
labels = {'x': 'Repository', 'y': 'Stars'}
fig = px.bar(x=repo_links, y=stars, title=title, labels=labels,
             hover_name=hover_texts)

fig.update_layout(title_font_size=28, xaxis_title_font_size=20,
                  yaxis_title_font_size=20)

fig.update_traces(marker_color='SteelBlue', marker_opacity=0.6)

fig.show()

2.2.3 在图表中添加可单击的链接

Plotly允许在文本元素中使用HTML,让你能够轻松地在图表中添加链接。下面将x轴标签作为链接,让观察者能够访问项目在GitHub上的主页。为此,需要提取URL并用其生成x轴标签:

py 复制代码
# 处理有关仓库的信息
repo_dicts = response_dict['items']
repo_links, stars, hover_texts = [], [], []
for repo_dict in repo_dicts:
    # Turn repo names into active links.
    repo_name = repo_dict['name']
    repo_url = repo_dict['html_url']
    repo_link = f"<a href='{repo_url}'>{repo_name}</a>"
    repo_links.append(repo_link)

    stars.append(repo_dict['stargazers_count'])

    # 创建悬停文本
    owner = repo_dict['owner']['login']
    description = repo_dict['description']
    hover_text = f"{owner}<br />{description}"
    hover_texts.append(hover_text)

# 可视化
title = "Most-Starred Python Projects on GitHub"
labels = {'x': 'Repository', 'y': 'Stars'}
fig = px.bar(x=repo_links, y=stars, title=title, labels=labels,
             hover_name=hover_texts)

fig.update_layout(title_font_size=28, xaxis_title_font_size=20,
                  yaxis_title_font_size=20)

fig.update_traces(marker_color='SteelBlue', marker_opacity=0.6)

fig.show()

将这个列表用作图表的x值。生成的图表与前面相同,但观察者可单击图表底端的项目名,以访问项目在GitHub上的主页。至此,我们对API获取的数据生成了可视化图表------它是交互性的,包含丰富的信息!

3.2.4 定制标记颜色

创建图形后,可使用以update_打头的方法来定制其各个方面。使用update_traces()定制图形呈现的数据。

py 复制代码
fig.update_traces(marker_color='SteelBlue', marker_opacity=0.6)

3.2.5 深入了解Plotly和GitHub API

要深入地了解如何生成Plotly图表,有两个不错的地方可以查看。第一个是Plotly User Guide in Python。通过研究该资源,可更深入地了解Plotly是如何使用数据来生成可视化图表的,以及它采取这种做法的原因。

第二个不错的资源是Plotly网站中的Python Figure Reference,其中列出了可用来配置Plotly可视化的所有设置。这里还列出了所有的图表类型,以及在各个配置选项中可设置的属性。

要更深入地了解GitHub API,可参阅其文档。通过阅读文档,你可以知道如何从GitHub提取各种信息。如果有GitHub账户,除了向公众提供的有关仓库的信息外,你还可以提取有关自己的信息。

3.3 Hacker News API

为探索如何使用其他网站的API调用,我们来看看Hacker News。在Hacker News网站,用户分享编程和技术方面的文章,并就这些文章展开积极的讨论。Hacker News的API让你能够访问有关该网站所有文章和评论的信息,且不要求通过注册获得密钥。

下面的调用返回本书编写期间最热门的文章的信息:

https://hacker-news.firebaseio.com/v0/item/19155826.json

如果在浏览器中输入这个URL,将发现响应位于一对花括号内,表明这是一个字典。如果不改进格式,这样的响应难以阅读。通过方法json.dump()来运行这个URL,以便对返回的信息进行探索:

py 复制代码
import requests
import json

# 执行API调用并存储响应
url = 'https://hacker-news.firebaseio.com/v0/item/31353677.json'
r = requests.get(url)
print(f"Status code: {r.status_code}")

# 探索数据的结构
response_dict = r.json()
response_string = json.dumps(response_dict, indent=4)
print(response_string)

输出是一个字典,其中包含有关ID为31353677的文章的信息。

这个字典包含很多键。与键'descendants'相关联的值是文章被评论的次数。与键'kids'相关联的值包含文章所有评论的ID。每个评论本身也可能有评论,因此文章的后代(descendant)数量可能比其'kids'的数量多。这个字典中还包含当前文章的标题和URL。

下面的URL返回一个列表,其中包含Hacker News上当前排名靠前的文章的ID。

https://hacker-news.firebaseio.com/v0/topstories.json

通过使用这个调用,可获悉当前有哪些文章位于主页,再生成一系列类似于前面的API调用。通过使用这种方法,可概述当前位于Hacker News主页的每篇文章

py 复制代码
from operator import itemgetter
import requests

# 执行API调用并存储响应
url = 'https://hacker-news.firebaseio.com/v0/topstories.json'
r = requests.get(url)
print(f"Status code: {r.status_code}")

# 处理有关每篇文章的信息
submission_ids = r.json()
submission_dicts = []
for submission_id in submission_ids[:30]:
    # 对于每篇文章,都执行一个API调用
    url = f"https://hacker-news.firebaseio.com/v0/item/{submission_id}.json"
    r = requests.get(url)
    print(f"id: {submission_id}\tstatus: {r.status_code}")
    response_dict = r.json()

    # 对于每篇文章,都创建一个字典
    submission_dict = {
        'title': response_dict['title'],
        'hn_link': f"http://news.ycombinator.com/item?id={submission_id}",
        'comments': response_dict['descendants'],
    }
    submission_dicts.append(submission_dict)

submission_dicts = sorted(submission_dicts, key=itemgetter('comments'),
                          reverse=True)

for submission_dict in submission_dicts:
    print(f"\nTitle: {submission_dict['title']}")
    print(f"Discussion link: {submission_dict['hn_link']}")
    print(f"Comments: {submission_dict['comments']}")

首先,执行一个API调用,并打印响应的状态。这个API调用返回一个列表,其中包含Hacker News上当前最热门的500篇文章的ID。接下来,将响应对象转换为一个Python列表,并将其存储在submission_ids中。后面将使用这些ID来创建一系列字典,其中每个字典都存储了一篇文章的信息。

创建一个名为submission_dicts的空列表,用于存储前面所说的字典。接下来,遍历前30篇文章的ID。对于每篇文章,都执行一个API调用,其中的URL包含submission_id的当前值。我们打印请求的状态和文章ID,以便知道请求是否成功。

为当前处理的文章创建一个字典,并在其中存储文章的标题、讨论页面的链接和评论数。然后,将submission_dict附加到submission_dicts末尾。

Hacker News上的文章是根据总体得分排名的,而总体得分取决于很多因素,包含被推荐的次数、评论数和发表时间。我们要根据评论数对字典列表submission_dicts进行排序,为此使用了模块operator中的函数itemgetter()。我们向这个函数传递了键'comments',因此它从该列表的每个字典中提取与键'comments'关联的值。这样,函数sorted()将根据这个值对列表进行排序。我们将列表按降序排列,即评论最多的文章位于最前面。

对列表排序后遍历它,并打印每篇热门文章的三项信息:标题、讨论页面的链接和评论数:

py 复制代码
Status code: 200
id: 39098603	status: 200
id: 39081876	status: 200

无论使用哪个API来访问和分析信息,流程都与此类似。有了这些数据后,就可进行可视化,指出最近哪些文章引发了最激烈的讨论。基于这种方式,应用程序可以为用户提供网站(如Hacker News)的定制化阅读体验。要深入了解通过Hacker News API可访问哪些信息,请参阅其文档页面。

4. 小结

至此,我们的数据可视化学习就算成了,其中有很多细节需要注意和学习,稍有疏漏便会溃不成塔。这只是Python编程从入门到实践的第二个项目,之后还有多个项目可供学习,基础知识可参考python编程入门,项目源代码下载请在资源下载处自行选择下载。

相关推荐
小技与小术3 分钟前
go环境搭建
开发语言·后端·golang
席万里4 分钟前
二叉树Golang
开发语言·golang·深度优先
杜杜的man5 分钟前
【go从零单排】SHA256 Hashes加密
开发语言·golang·哈希算法
hummhumm6 分钟前
第 14 章 -Go语言 错误处理
java·开发语言·前端·后端·python·sql·golang
Linux运维技术栈12 分钟前
Linux运维工程师推荐学习的开发语言
运维·开发语言·学习
拓端研究室TRL14 分钟前
R语言贝叶斯分析:INLA 、MCMC混合模型、生存分析肿瘤临床试验、间歇泉喷发时间数据应用|附数据代码...
开发语言·r语言
linzhisong15 分钟前
LayUI组件国际化多国语言版本脚本-上篇提取中文字符
前端·javascript·python·layui
codists17 分钟前
《Django 5 By Example》阅读笔记:p17-p53
python·django
小馒头学python22 分钟前
机器学习中的概率超能力:如何用朴素贝叶斯算法结合标注数据做出精准预测
人工智能·python·算法·机器学习
Once2gain22 分钟前
dlopen: cannot load any more object with static TLS & sklearn, HPOBench, smac3
人工智能·python·sklearn