Skip to content

Data Visualizing

数据可视化指的是通过可视化表示来探索数据。它与数据分析紧密相关,而数据分析指的是使用代码来探索数据集的规律和关联

文章涉及两个常用的 Python package:

  • Matplotlib v3.7.4 是一个数学绘图库,常被用作数据处理和可视化分析
  • Plotly v5.18 包,它生成的图表非常适合在数字设备上显示。
Install matplotlib

通过命令 python -m pip install --user matplotlib 直接安装 matplotlib 即可

也可以通过创建一个 虚拟环境 ,在虚拟环境下直接执行 pip install matplotlib

或者 官方 列举的其他方法

matplotlib

Simple Line Chart

About matpltlib.pyplot

matplotlib.pyplot is a collection of functions that make matplotlib work like MATLAB. Each pyplot function makes some change to a figure: e.g., creates a figure, creates a plotting area in a figure, plots some lines in a plotting area, decorates the plot with labels, etc.

通过如下代码,我们可以通过使用 matpltlib 中的 pyplot 函数集,快速生成一个简单的折线图

python
import matplotlib.pyplot as plt

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

代码 plot.show() 用于新建一个图形实例,并展示图表。如果需要保存图标,可以参考 pyplot.savefigplot.show() 替换为 plt.savefig('suqares_plot.png', bbox_inches='tight')

WARNING

the variable fig in the code fig, ax = plt.subplots() can not be omitted because the function plt.subplots() returns a tuple containing a Figure and Axes object(s). tuple unpacking will make an assignment to fig and ax respectively with the Figure and Axes objects. Just like javascript, the args can not be omitted or the unpacked variables will mismatch.

If you only want the Axes object and don't care about the Figure, you could use _ to ignore the Figure

参考 Tuple Unpacking

python
_, ax = plt.subplots()

Set labels and Line Width

下面对代码进行标签和线条粗细的定制化

python
import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]
fig, ax = plt.subplots()
ax.plot(squares) 
ax.plot(squares, linewidth=3) 
ax.set_title("Square Numbers", fontsize=24) 
# 标签最好使用英文,以防出现文字显示问题
ax.set_xlabel("Value", fontsize=14) 
ax.set_ylabel("Square of Value", fontsize=14) 

# Set size of tick labels.
ax.tick_params(axis='both', labelsize=14) 

plt.show()

Correcting the Figure

仔细观察此时的图形,我们会发现实际上 x轴y轴 的数据是错位的。在 x=0 的位置上 y=1,而在最后一个点 x=4 的位置上 y=25

这是因为根据 ax.plot , plot 方法接收可选的 x轴 参数,当只传递了一组数据的时候,x轴 的数据为默认为 range(len(y))。因此当前图表数据的 x轴 数据是默认的 range(len(squares))

所以我们只需要指定对应的 x轴 数据即可

python
import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5] 
squares = [1, 4, 9, 16, 25]
fig, ax = plt.subplots()
ax.plot(squares, linewidth=3) 
ax.plot(input_values, squares, linewidth=3) 
ax.set_title("Square Numbers", fontsize=24)
ax.set_xlabel("Value", fontsize=14)
ax.set_ylabel("Square of Value", fontsize=14)

# Set size of tick labels.
ax.tick_params(axis="both", labelsize=14)

plt.show()

Set Plot Styles

可以通过 plt.style.available 获悉当前系统可使用那些样式,然后通过 plt.style.use('Solarize_Light2') 来使用对应的主题

python
import matplotlib.pyplot as plt

print(plt.style.available)
"""
output: ['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']
"""
Code Example
python
import matplotlib.pyplot as plt

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

plt.style.use("Solarize_Light2")
fig, ax = plt.subplots()
ax.plot(input_values, squares, linewidth=3)
ax.set_title("Square Numbers", fontsize=24)
ax.set_xlabel("Value", fontsize=14)
ax.set_ylabel("Square of Value", fontsize=14)

# Set size of tick labels.
ax.tick_params(axis="both", labelsize=14)

plt.show()

Scatter

当需要对点进行单独的设置时,散点图 Scatters 往往很有用。比如绘制大型数据集时。

要绘制单个点,可使用方法 scatter()。具体参数可参考matplotlib.pyplot.scatter

python
import matplotlib.pyplot as plt

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

plt.show()

Set scatter style

随后进行图表样式的设置

python
import matplotlib.pyplot as plt

plt.style.use("seaborn")
fig, ax = plt.subplots()
# 设置散点图点的大小及颜色
ax.scatter(2, 4) 
ax.scatter(2, 4, s=200, c="red") 
# ax.scatter(1, 3, c="blue")

# 设置图表标题并给坐标轴加上标签
ax.set_title("Square Numbers", fontsize=24) 
ax.set_xlabel("Value", fontsize=14) 
ax.set_ylabel("Square of Value", fontsize=14) 

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

plt.show()

Set multiple scatters

可以通过传入 shape(n, )list 绘制多个点。同时可以利用自动计算与 列表推导 快速声明相关联的数据

python
import matplotlib.pyplot as plt

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

plt.style.use("seaborn")
fig, ax = plt.subplots()
ax.scatter(2, 4, s=200, c="red") 
ax.scatter(x_values, y_values, s=10, c="red") 

# 设置图表标题并给坐标轴加上标签
--snip--
# 设置每个坐标轴的取值范围
ax.axis([0, 1100, 0, 1100000])

Set Color Map

除了手动设置颜色 c='red' 外,pyplot 内置了一组颜色映射,可以通过传入数组并指定颜色集,动态赋予散点图颜色。要使用这些颜色映射,需要告诉 pyplot 如何设置数据集中的颜色。

python
import matplotlib.pyplot as plt

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

ax.scatter(x_values, y_values, s=10, c="red") 
ax.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Reds, s=10) 

# 设置图表标题并给坐标轴加上标签。
--snip--
Code Example
python
import matplotlib.pyplot as plt

x_values = range(1, 1001)
y_values = [v**2 for v in x_values]
plt.style.use("seaborn")
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, s=10, c=y_values, cmap=plt.cm.Reds)


# 设置图表标题并给坐标轴加上标签
ax.set_title("Square Numbers", fontsize=24)
ax.set_xlabel("Value", fontsize=14)
ax.set_ylabel("Square of Value", fontsize=14)

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

# 设置每个坐标轴的取值范围
ax.axis([0, 1100, 0, 1100000])
# plt.show()
plt.savefig("test.png", bbox_inches="tight")

Random Walk

随机漫步是这样行走得到的路径:每次行走都是完全随机的、没有明确的方向,结果是由一系列随机决策决定的。

首先我们新建一个随机漫步类。设置初始点为 (0, 0) ,传入一个目标值 num_points 模拟操作次数

随即声明一个随机步数工具函数 get_random_step 随机生成每一步的方向和距离。再通过在操作次数的限制内,进行 while 循环,生成 5000 次随机漫步

about random module

random.choice(seq) This module implements pseudo-random number generators for various distributions.

And random.choice(seq) return a random element from the non-empty sequence seq

pseudo-random means something that looks random, but is actually produced by a fixed and predictable process.

Update Styles

Scatter 散点图 那一章提到的相关样式也可以应用在随机漫步散点图中,通过如下代码去掉散点轮廓,并对 所有的点进行 num_points 维度的着色

python
ax.scatter(rw.x_values, rw.y_values, s=15) 
ax.scatter(rw.x_values, rw.y_values, s=15, c=range(rw.num_points), cmap=plt.cm.Blues, edgecolors="none") 

于此同时我们可能需要对随机漫步图的起点和重点进行特殊的样式设置,可以通过在 draw_plot 函数内声明一个强调点的函数,对特殊的点进行样式更新

python
def emphasize_point(idx, **scatter_args):
        final_args = dict(s=100, edgecolors="none", c="red")
        final_args.update(scatter_args)
        ax.scatter(
            rw.x_values[idx],
            rw.y_values[idx],
            **final_args,
        )

    emphasize_point(0, c="green")
    emphasize_point(-1, c="red", s=120)
Code Example
python
from random import choice
import matplotlib.pyplot as plt


def get_random_step(distance=[0, 1, 2, 3, 4]):
    """Determine the direction and distance for each step"""
    direction = choice([-1, 1])
    return direction * choice(distance)


class RandomWalk:
    """A class to generate random walks."""

    def __init__(self, num_points=5000):
        """Initialize attributes of a walk"""
        self.num_points = num_points

        # All walks start at (0,0)
        self.x_values = [0]
        self.y_values = [0]

    def fill_walk(self):
        """Calculate all the points in the walk"""

        while len(self.x_values) < self.num_points:
            # Decide which direction to go and how far to go in that direction
            x_step = get_random_step()
            y_step = get_random_step()

            # Reject moves that go nowhere
            if x_step == 0 and y_step == 0:
                continue
            # calculate the next x and y values
            x = self.x_values[-1] + x_step
            y = self.y_values[-1] + y_step

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


def draw_plot():
    # Create a random walk instance and fill it
    rw = RandomWalk()
    rw.fill_walk()
    # plot all the points in the walk
    plt.style.use("classic")
    fig, ax = plt.subplots()

    ax.scatter(
        rw.x_values,
        rw.y_values,
        s=15,
        c=range(rw.num_points),
        cmap=plt.cm.Blues,
        edgecolors="none",
    )

    def emphasize_point(idx, **scatter_args):
        """Emphasize a point in the plot"""
        final_args = dict(s=100, edgecolors="none", c="red")
        final_args.update(scatter_args)
        ax.scatter(
            rw.x_values[idx],
            rw.y_values[idx],
            **final_args,
        )

    emphasize_point(0, c="green")
    emphasize_point(-1, c="red", s=120)
    plt.show()


if __name__ == "__main__":
    draw_plot()

Plotly

Plotly 可以生成交互式的图表。并且其生成的图表还将自动缩放以适合观看者的屏幕

根据 官网 的描述可以得知, plotly 的可交互性是基于 Plotly JavaScript library 的。 因此我们可以根据使用 Plotly 模块的相关功能,导出可交互的 html

python
from plotly import offline
offline.plot({"data": data, "layout": my_layout}, filename="d6.html")

这也是 version 4 推荐的图表展示方(Figure Display)。但在 version 5 可以使用 renderers framework 展示图表。renderers framework plotly.offline.iplot plotly.offline.plot 的一般化方案

Code Example
python
from plotly.graph_objects import Bar, Layout
# from plotly.graph_objects import Figure

import plotly.offline as offline
from random import randint

class Die:
    """A class representing a single die"""

    def __init__(self, num_sides=6):
        """default to a six-sided die"""
        self.num_sides = num_sides

    def roll(self):
        """return a random value between 1 and number of sides"""
        return randint(1, self.num_sides)


# def draw_plot():
# Create a D6
die = Die()

# make some rolls and save the results in a list
results = []
for roll_num in range(1000):
    result = die.roll()
    results.append(result)

# analyze the results
frequencies = []
for value in range(1, die.num_sides + 1):
    frequencies.append(results.count(value))

x_values = list(range(1, die.num_sides + 1))
data = [Bar(x=x_values, y=frequencies)]

x_axis_config = {"title": "Result"}
y_axis_config = {"title": "Frequency of the result"}
my_layout = Layout(
    title="Roll a D6 1000 times", xaxis=x_axis_config, yaxis=y_axis_config
)
# Figure(data=data, layout=my_layout).show()
offline.plot({"data": data, "layout": my_layout}, filename="d6.html")

Mass Data Process

一般我们用于可视化的数据都不是自己生成的。是在网上通过爬取等方式总结汇总后,再进行二次处理得到的。

通过对可视化的数据进行存储与二次处理,就能够通过分析发现别人没有发现的规律和关联

CSV

CSV 文件通过将数据作为一系列以逗号分隔的值(comma-separated values)写入文件进行存储

例如 assets/data/sitka_weather_07-2018_simple.csv 这个文件中,通过如下的文本内容

"STATION","NAME","DATE","PRCP","TAVG","TMAX","TMIN"
"USW00025333","SITKA AIRPORT, AK US","2018-07-01","0.25",,"62","50"

实际上表示了下表的数据

STATIONNAMEDATEPRCPTAVGTMAXTMIN
USW00025333SITKA AIRPORT, AK US2018-07-010.256250

Analyze CSV Data

Python 中内置了 CSV 模块,方便快速处理数据。结合 文件操作,可以

python
import csv
filename = 'assets/data/sitka_weather_07-2018_simple.csv'
with open(filename) as f:
  reader = csv.reader(f)
  # next 为内置函数读取文件的下一行
  header_row = next(reader)
  print(header_row) # ['STATION', 'NAME', 'DATE', 'PRCP', 'TAVG', 'TMAX', 'TMIN']

也可以通过 enumerate 方法,将文件头及其索引更明确的表示出来

python
--snip--
  # next 为内置函数读取文件的下一行
  header_row = next(reader)
  print(header_row) 
  for idx, col_header in enumerate(header_row): 
    print(idx, col_header) 

类似的。通过遍历每一行的数据,可以获取到文件中最高温度的列表

python
--snip--
  # next 为内置函数读取文件的下一行
  header_row = next(reader)
  for idx, col_header in enumerate(header_row): 
    print(idx, col_header) 
  highs = [] 
  for row in reader: 
    high = int(row[5]) 
    highs.append(high) 

或使用列表推导式 highs = [int(row[5]) for row in reader] 快速生成数据 list

Draw temperature plot

参考 绘制折线图 等内容,可将上节中的 highs 设置为 y轴,得到一个基础的折线图

python
import matplotlib.pyplot as plt 
with open(filename) as f:
  --snip--

# 绘制图表
plt.style.use("seaborn")
fig, ax = plt.subplots()
ax.plot(highs, c="red")

# set chart title and label axes
ax.set_title("2018 Daily high temperatures for Sitka, AK", fontsize=24)
ax.set_xlabel("", fontsize=16)
ax.set_ylabel("Temperature(F)", fontsize=16)
ax.tick_params(axis="both", which="major", labelsize=16)

再使用模块 datetime 将日期数据处理为对应的值。 strftimestrptime 是一对相对应的 API。前者可以实现日期的格式化 format,而后者可以实现日期的解析 parse

为什么不直接使用第二列作为 x轴 数据?

之所以不直接使用 row[2] 的日期数据作为 x轴 的数据,是因为要调用 Figure 对象的 autofmt_xdate 方法进行格式化,并自动倾斜

对代码进行如下修改

python
import csv
import matplotlib.pyplot as plt
from datetime import datetime 

with open(filename) as f:
   --snip--

    # get temperature from the file
    highs = [] 
    dates, highs = [], [] 
    for row in reader:
        high = int(row[5])
        current_date = datetime.strptime(row[2], '%Y-%m-%d') 
        dates.append(current_date) 
        highs.append(high)

--snip--
ax.set_xlabel("", fontsize=16)
fig.autofmt_xdate() 
--snip--

Draw another series

除了绘制最高温度,如果数据充足,我们还可以继续绘制最低温度

python
--snip--
    header_row = next(reader)
    dates, highs = [], [] 
    dates, highs, lows = [], [], [] 
    for row in reader:
        current_date = datetime.strptime(row[2], '%Y-%m-%d')
        dates.append(current_date)
        high = int(row[5])
        highs.append(high)
        lows.append(int(row[6])) 

plt.style.use("seaborn")
fig, ax = plt.subplots()
ax.plot(dates, highs, c="red")
ax.plot(dates, lows, c="blue") 
ax.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1) 

通过 plot.fill_between 方法,可以快速填充 data series 之间的空隙

Error process

理论上我们可以使用上一节中的代码,生成任何包含 date low high 三种数据的图表,但有时候 数据源 可能会出现问题,如

  • 数据故障、缺失
  • 数据格式错误

如果不妥善处理,可能导致程序崩溃。例如如果我们将文件替换为 assets/data/death_valley_2018_simple.csv,再将对应最高温度与最低温度的 idx 修改后,运行当前程序将会报一个 ValueError 的错误,因为在 int(row[4]) 的时候,某一行的 row[4] 数据为空,因此程序出错

解决方案很简单,使用 except 捕捉错误即可

python
try :
    high = int(row[5])
    low = int(row[6])
except ValueError:
    print(f"Missing data for {current_date}")
else:
    highs.append(high)
    lows.append(low)
    dates.append(current_date)
Code Example
python
# sitka_highs.py
import csv
import matplotlib.pyplot as plt
from datetime import datetime

filename = "assets/data/sitka_weather_2018_simple.csv"
with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)

    # get high temperature from the file
    dates, highs, lows = [], [], []
    for row in reader:
        current_date = datetime.strptime(row[2], '%Y-%m-%d')
        try :

            high = int(row[5])
            low = int(row[6])
        except ValueError:
            print(f"Missing data for {current_date}")
        else:
            highs.append(high)
            lows.append(low)
            dates.append(current_date)

plt.style.use("seaborn")
fig, ax = plt.subplots()
ax.plot(dates, highs, c="red")
ax.plot(dates, lows, c="blue")

# set chart title and label axes
ax.set_title("2018 Daily temperatures for Sitka, AK", fontsize=24)
ax.set_xlabel("", fontsize=16)
ax.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1)
# format x axis according to date
fig.autofmt_xdate()
ax.set_ylabel("Temperature(F)", fontsize=16)
ax.tick_params(axis="both", which="major", labelsize=16)

plt.show()

JSON

类似 CSVPython 中同样内置了 JSON 模块,方便快速处理数据

Code Example
python
import json

# Explore the structure of the data
filename = "assets/data/json/eq_data_1_day_m1.json"

with open(filename) as f:
    all_eq_data = json.load(f)

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

Process Json Response

Web API 是网站的一部分,用于使用具体 URL 请求特定信息。我们可以通过使用 Web API,请求外部数据源,对最新的数据进行可视化。通常返回值都是以易于处理的格式如 JSON 或者 CSV 返回

比如 https://api.github.com/search/repositories?q=language:python&sort=stars 。当前接口返回 Github 托管的 Python 项目信息,并且根据 stars 进行排序

为了能够请求对应接口,我们使用了外部模块 requests

python
import requests

# Make an API call and store the response
url = "https://api.github.com/search/repositories?q=language:python&sort=stars"

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

# Store API response in a variable
response_dict = r.json()

# Process results
print(response_dict.keys()) # dict_keys(['total_count', 'incomplete_results', 'items'])

使用 Plotly 图表化及数据处理过程,可以查看 Source Code