从历代皇帝的寿命纵观中国上下五千年

宏观的了解中国古代统治者的历史,可能会对历史的学习有很大的帮助。这个项目给大家准备了两个数据集:

✭统治时间及年号数据集:

这个数据集是从 Wikipedia 上面整理下来的 – List of Chinese Monarchs,包括袁世凯以及它之前具有不同头衔的中国统治者。从周朝到秦朝,统治者通常冠以“国王”的称号(中文:王;拼音:wáng),随着中国分裂成不同的战国,这个称号变得非常普遍。直到中国的统一者,即秦始皇为自己创建了一个新称谓“皇帝”(拼音:huángdì)。皇帝这个称号继续用于剩余中国的朝代历史,一直到 1912 年清朝灭亡。(这个数据集并不完整的包括了所有的帝王或其他统治者,仅作为本项目的数据可视化的实践使用)

✭皇帝寿命数据集:

这个数据集是从《中国帝王皇后亲王公主世系录》当中整理出来的。

1. 统治时间及年号数据集

数据集是从维基百科上整理下来的,所以数据不一定完全的准确,数据也不一定完整,我们仅用这些数据做可视化呈现的实践。 数据存储在 list_of_chinese_monarchs.csv 当中,下面是关于数据集的字段描述:

  • index:序号;
  • time_period:历史时期或朝代;
  • name:姓名,有部分姓名为汉语拼音;
  • reign_start:开始统治的年,负数代表公元前,整数代表公元后;
  • reign_period:统治的年数,由于更换朝代,有的帝王在不同朝代有统治的记录,在数据中会有多行。

In [ ]:

# ... 这里需要您编写 - 任务 1 的代码 ...

# 导入 pandas 模块,并简称为 pd
import pandas as pd

# 加载数据,index_col=0 表示最前面一列为索引
df_monarchs = pd.read_csv('/data/course_data/visualization/list_of_chinese_monarchs.csv', index_col=0)

# 查看数据集的总体信息
df_monarchs

2. 计算直方图所需要的数据

在这个任务中我们需要计算直方图所需要的数据,这个步骤的关键是找到合适的 bin 大小,大致的思路是:

  • 使用 df_monarchs['reign_period'].describe() 查看统治时间列的整体情况;
  • 然后根据经验,猜一个 bin 的值,猜的方法可以是 (max - min) / 上四分位值,上四分位值就是 25% 所表示的值,也可以用任何一种经验方法;
  • 使用 np.histogram(df_monarchs['reign_period'], bins=30) 计算直方图的数据集。

In [ ]:

# ... 这里需要您编写 - 任务 2 的代码 ...

# 导入 numpy,并简称为 np
import numpy as np

# 查看 reign_period 列中所有值的总体情况
print(df_monarchs['reign_period'].describe())

# 计算直方图所需要的数据集
hist, edges = np.histogram(df_monarchs['reign_period'], bins=30)
hist, edges

3. 绘制关于帝王统治时长的直方图

本项目中,我们会使用 Python 中的 Bokeh 工具包来绘制图形,绘图的脚本主要的步骤包括:

  • 导入与绘图相关的 Bokeh 工具包;
  • 创建绘图对象,并设置绘图参数;
  • 根据第 2 步计算的 hist 和 edges 结果数据绘制直方图;
  • 显示绘图结果。

In [ ]:

# ... 这里需要您编写 - 任务 3 的代码 ...

# 导入绘图组件,figure 是绘图主工具,show 用来显示绘图结果
from bokeh.plotting import figure, show

# 导入一个指定绘图输出方式的工具,
# output_notebook 用来指定 Bokeh 将绘图输出到 Notebook 中
from bokeh.io import output_notebook
output_notebook()

# 创建绘图对象,并设置绘图的标题,指定绘图的宽度和高度
p = figure(
    # 绘图标题
    title='中国帝王统治时长(年)', 
    # 绘图宽度
    plot_width=600,
    # 绘图高度
    plot_height=400,
)

# 设置 y 轴的名称(可选)
# p.yaxis.axis_label = '帝王数量(人)'

# 设置 x 轴的名称(可选)
# p.xaxis.axis_label = '统治时间(年)'

# 使用 quad 方法,绘制直方图
p.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:], 
       fill_color="navy", line_color="white", alpha=0.6)

# 显示绘图
show(p)

4. 分朝代绘制帝王开始统治时间的分布图

学习一个事物,往往先了解他的整体,会帮助学习更加成功。

  • 中国历史上有很多朝代,哪些朝代长久、哪些短命?
  • 哪些朝代风云变幻、哪些长治久安?
  • 哪些朝代是并行的?

要想回答这些问题,数据可视化可以派上用场。我们可以利用数据集中的 reign_start 字段做分朝代的直方图,并且把这些直方图垂直依次排列。

In [ ]:

# 导入批量绘图工具,将多个绘图按照网格方式排布
from bokeh.layouts import gridplot

# ------------------------------------------------------------------- #
# 基于 df_monarchs 中的数据悉数中国历史上有哪些朝代
dynasty = df_monarchs['time_period'].unique()

# ------------------------------------------------------------------- #
# 为每一个朝代绘制帝王开始统治时间的直方图
plots = []
for d in dynasty:
    # 创建一个新的绘图
    p = figure(tools='save', x_range=[-3100, 2200], min_border_left=30)
    
    # 开始设置:绘图的样式
    # 设置整体绘图的外部有一个窄窄的、粗细为 0.5 个像素的边框
    p.outline_line_width=0.5
    # 隐藏横向的网格,避免线条过多影响观看
    p.ygrid.visible = False
    # 隐藏 x 坐标轴,让其不显示
    p.xaxis.visible = False
    # 设置绘图不显示二级刻度,使得绘图的一级刻度更加清晰
    p.yaxis.ticker.num_minor_ticks = 0
    # 设置 y 轴期望的刻度数量,使得刻度不至于过密影响显示
    # 设置的值只是一个期望值,系统会根据实际的数据情况进行微调
    p.yaxis.ticker.desired_num_ticks = 2
    # 设置完成:绘图的样式

    # 开始绘图:为每一个朝代绘制直方图
    # 获得朝代名称为变量 d 的数据
    df_monarchs_d = df_monarchs[df_monarchs['time_period'] == d]
    # 计算绘制直方图所需要的数据
    hist, edges = np.histogram(df_monarchs_d['reign_start'], bins=np.linspace(-2500, 2000, 200))
    # 绘制直方图中每个条形柱状图
    p.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:], 
           fill_color="navy", line_color="white", alpha=1)
    # 绘制帝王人数和朝代相关的文本数据
    p.text(x=-3000, y=0, text=[f'帝王人数:{sum(hist)}'], y_offset=-15, text_font_size='8pt')
    p.text(x=-3000, y=0, text=[d], text_font_size='8pt', text_font_style='bold')
    
    # 将当前朝代的直方图添加到 plots 列表当中
    plots.append(p)

# ------------------------------------------------------------------- #
# 绘制一个新的绘图,放到所有绘图的最上面:
# - 显示标题:'中国历朝历代帝王开始统治的时间(年)'
# - 显示共享的 x 坐标轴:显示 [-2150, -1500, -1000, -500, 0, 500, 1000, 1500, 2000] 九个年份
# - 隐藏 y 坐标轴
# - 隐藏绘图的网格
p = figure(outline_line_width=0, frame_height=40, x_range=[-3100, 2200], min_border_left=30, tools='save')
p.text(-2150, 0, ['2,150 BCE '], text_align='left', text_baseline='middle')
p.text(2000, 0, ['2,000 CE'], text_align='right', text_baseline='middle')
p.text(0, 0, ['中国历朝历代帝王开始统治的时间(年)'], text_align='center', text_baseline='middle')
p.xaxis.ticker = [-2150, -1500, -1000, -500, 0, 500, 1000, 1500, 2000]
p.yaxis.visible = False
p.grid.visible = False
plots.insert(0, p)

# ------------------------------------------------------------------- #
# 使用 Bokeh 中的 gridplot 工具将所有的绘图排成一列显示:
# - plots:这个参数就是存储了标题绘图和所有朝代绘图的列表
# - ncols:这个参数的意思是所有 plots 中的绘图排成 1 列
# - plot_width:这个参数的意思是每个 plots 中的绘图的宽度都是 800 个像素
# - plot_height:这个参数的意思是每个 plots 中的绘图的宽度都是 40 个像素
grid = gridplot(plots, ncols=1, plot_width=800, plot_height=40)

# 最终显示绘图 grid,grid 中存储着所有已经绘制好的绘图
# 调用 show 方法将 grid 中的,按照 1 列进行排布的绘图显示出来
show(grid)

5. 回答几个小问题

In [ ]:

# ... 这里需要您编写 - 任务 5 的代码 ...

# 在第 4 个任务的绘图中,可以看出只有 1 位帝王的朝代有几个?

answer_1 = ...

# 在第 4 个任务的绘图中,可以看出存在时间最长的朝代是哪个朝代?

answer_2 = ''

# 在第 4 个任务的绘图中,帝王最多的朝代中共有多少位帝王?

answer_3 = ...

6. 皇帝寿命数据集

在这个数据集中,包含了 302 位称号为皇帝的中国古代帝王。我们来用这个数据集学习绘制箱形图,并对箱形图进行排序,获取洞察。

这个数据集的学习,我们将有三个小步骤:

  • 加载数据;
  • 生成箱形图的数据;
  • 绘制箱形图,并对箱形图排序;
  • 从极端值中找故事。

我们先来加载皇帝寿命的数据集。

In [ ]:

# ... 这里需要您编写 - 任务 6 的代码 ...

# 导入 pandas 模块,并简称为 pd
import pandas as pd

# 加载数据,index_col=0 表示最前面一列为索引
df_emperor_ages = pd.read_csv(
    '/data/course_data/visualization/emperor_ages.csv', index_col=0)

# 查看数据集的总体信息
df_emperor_ages.info()

7. 绘制各朝代皇帝寿命的箱形图

开课吧的工程师们为同学们编写了两个脚本:

  • generate_box_plot_data:按照视频中讲解的箱形图的定义,计算中值、四分位值、箱须长度等;
  • bokeh_box_plot:基于 Bokeh 的基础功能,按照 generate_box_plot_data 计算好的数据进行绘制。

本门课程的重点是数据可视化,我们不把重点放在解读代码上,后续会推出专门 Bokeh 数据可视化脚本编写的课程。

让我们先一起「跑」完为大家写好的代码吧!

In [ ]:

# ... 这里需要您编写 - 任务 7 的代码 ...

# 导入提前编写好的绘制箱形图的脚本

import sys
sys.path.append('/data/course_data/visualization')

from dvfe_01_07 import bokeh_box_plot, generate_box_plot_data

# 下面这个脚本用来计算箱形图所需要的绘图数据:
# - df_emperor_ages:数据集
# - '朝代':df_emperor_ages 中的列名,按照朝代对数据进行分组
# - '寿命':df_emperor_ages 中的列名,需要绘制箱形图的数值变量
data_box_plot, data_box_plot_out = \
    generate_box_plot_data(df_emperor_ages, '朝代', '寿命')

# data_box_plot, data_box_plot_out 是两个字典类型的数据
# 我们查看一下其中存储了哪些数据
print(data_box_plot.keys())
print(data_box_plot_out.keys())

# 绘制箱形图
p = figure(title='中国历朝历代皇帝寿命',
           y_range=data_box_plot['category'], 
           plot_width=600, plot_height=400, 
           tools='save', toolbar_location='right')
bokeh_box_plot(p, data_box_plot, data_box_plot_out, all_dot=False)

# 设置绘图的标题、坐标轴等呈现的样式
p.title.text_font_size = '12pt'
p.xaxis.axis_label = '皇帝寿命(年)'
p.axis.axis_label_text_font_size = "10pt"
p.axis.major_label_text_font_size = "8pt"
p.xaxis.ticker.num_minor_ticks = 10

# 显示绘图
show(p)

8. 对箱形图进行排序 => 发现洞察

在观察箱形图的时候,我们通常需要按照「类别」、「中值」、「IQR」排序,在本项目中:

  • 按照「类别」排序就是按照 data_box_plot 中的 'category' 排序;
  • 按照「中值」排序就是按照 data_box_plot 中的 'q2' 排序;
  • 按照「IQR」排序就是按照 data_box_plot 中的 'iqr' 排序。

请将 sort_by 变量从现在的 'category' 改变为 'q2',并重新绘图。

In [ ]:

# ... 这里需要您编写 - 任务 8 的代码 ...

# 定义排序的规则
sort_by = 'category'

# 按照 sort_by 指定的信息,对 data_box_plot 进行排序
df_for_sort = pd.DataFrame(data_box_plot)
df_for_sort.sort_values(by=sort_by, inplace=True)
data_box_plot = df_for_sort.to_dict('list')

# 对箱形图排序,实际上就是改变箱形图绘图时 y 轴的刻度数据
# 将排序后的分类(category)列表,赋值给绘图的 y 轴坐标刻度就好啦
p.y_range.factors = data_box_plot['category']
show(p)

9. 查看极端值

离群值或极端值(Outliers)表示数值异常的数据,是指在数据中有一个或几个数值与其他数值相比差异较大。

  • 当出现离群值的时候,要慎重处理,要将专业知识和统计学方法结合起来,首先应认真检查原始数据,看能否从专业上加以合理的解释。
  • 如数据存在逻辑错误而原始记录又确实如此,又无法在找到该观察对象进行核实,则只能将该观测值删除。
  • 本项目中的数据全部来自于维基百科或者官方史料,我们在这里不追究原始数据的正确还是错误,我们的目的是学习如何试着来解释这些极端值。

In [ ]:

# ... 这里需要您编写 - 任务 9 的代码 ...
outliers = df_emperor_ages.loc[data_box_plot_out['index']]
outliers

该文章采用「CC 协议」,转载必须注明作者和本文链接.