8  绘图与可视化


同学们,下午好。我们今天讨论的主题,对于任何现代经济学者而言,都是一项不可或缺的核心技能:如何用数据讲述一个有说服力的故事。你们已经具备了扎实的量化分析基础,但要将冰冷的数字转化为深刻、有力的论点,就必须掌握数据可视化的艺术。今天,我们将从理论走向实践,深入学习Python的核心可视化库——`matplotlib`、`pandas`和`seaborn`——为你们从课程作业到毕业论文的各项实证研究,构建一个坚实的可视化工具箱。

请大家务必专注。我们今天所涵盖的技术,正是你们未来在探索数据集、检验经济学假说、以及在学术界、金融行业或公共政策领域展示研究成果时所要用到的核心方法。让我们开始吧。

制作信息丰富的可视化图形(通常称为“图表”)是经济分析中最重要的任务之一。它是探索性数据分析(Exploratory Data Analysis, EDA)的基石,能帮助我们在运行任何回归分析之前,识别异常值、理解数据分布,或为数据转换提供思路。对于另一些应用场景,例如为政策分析或客户报告构建一个交互式仪表盘,可视化本身就是最终目标。Python的生态系统为静态和动态可视化提供了丰富的库。本章我们将重点关注`matplotlib`以及构建于其上的强大工具。

`matplotlib`是一个功能强大的绘图包,旨在创建出版物质量级别的图形。它由John Hunter于2002年发起,旨在为Python提供一个类似于MATLAB的绘图环境——你们中许多工科或理科背景的同学可能对此并不陌生。`matplotlib`的优势之一在于其通用性;它支持多种后端,并能以所有常见的矢量和栅格格式(如PDF、SVG、PNG等)导出图形。本书中几乎所有的图形都是使用`matplotlib`生成的。

随着时间的推移,`matplotlib`已成为许多专用工具包的基础。对我们经济学研究而言,其中最重要的之一是`seaborn`,这是一个提供了高级接口的库,用于绘制富有吸引力且信息量丰富的统计图形。我们将在本章的后半部分详细探讨它。

要最高效地跟随本章的示例,最佳方式是在Jupyter Notebook环境中进行交互式操作。为确保图表能直接在您的Notebook中显示,您应在会话开始时,在一个代码单元格中执行以下“魔术”命令:

```python
%matplotlib inline

8.1 Matplotlib API入门

尽管新的可视化库层出不穷,matplotlib依然是Python可视化领域的基石。它的应用程序接口(API)为我们提供了对图表每个元素的精细控制。我们坚持从matplotlib的基础知识讲起,是经过深思熟虑的;它与pandas的深度集成为我们处理经济数据提供了无与伦比的便利。你们在这里学到的原则将可以直接应用于其他库。

按照惯例,我们通常将包含核心绘图函数的matplotlib.pyplot导入并简写为plt

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
辨析两种Matplotlib API:“斧头”与“手术刀”

初学者常常对matplotlib的两种编程接口感到困惑:基于plt全局函数的状态机接口(state-based interface)和面向对象的接口(object-oriented interface)。

  1. 状态机接口 (例如 plt.plot()): 你可以把它想象成一把“斧头”。你直接调用plt的函数,matplotlib会在“当前”活动的图形和坐标轴上进行操作。这种方式对于快速、简单的绘图很方便,代码也更简洁。

  2. 面向对象接口 (例如 ax.plot()): 这更像一把“手术刀”。你首先显式地创建一个Figure对象(画布)和一个或多个Axes对象(子图/坐标轴),然后调用这些特定对象的方法来绘图。这种方式提供了无与伦比的控制力,尤其是在处理复杂图形(如多个子图)时,代码会更清晰、更不易出错。

在本书中,我们将主要使用面向对象的接口。这是专业数据分析和经济学研究中的最佳实践,因为它能让你精确控制图表的每一个元素,这对于制作符合出版标准的复杂图表至关重要。

运行%matplotlib inline命令后,我们就可以创建一个简单的图表。下面的代码从一个数字数组生成一个简单的线性图。请注意,np.arange(10)会生成一个数组[0, 1, ..., 9]。当plt.plot只接收一个列表或数组时,它会假定这是y值的序列,并自动生成x值作为该数组的索引。结果如 图 fig-simple-line 所示。

data = np.arange(10)
plt.plot(data)
plt.show()
图 8.1

8.1.1 图形与子图

matplotlib中,所有的绘图元素都存在于一个Figure对象之内。你可以将Figure想象成容纳所有绘图元素的画布或容器。你可以用plt.figure来创建一个新的图形。

在Jupyter Notebook中,单独调用一个空的plt.figure()不会显示任何东西。一个Figure仅仅是一块空白的画布;要制作图表,你需要在上面添加一个或多个子图(subplots)。一个子图由一个Axes对象表示(注意Axes中的”e”;它不同于x轴或y轴的axis)。

你可以使用add_subplot方法来添加子图。例如,fig.add_subplot(2, 2, 1)会在一个2x2的网格上创建一个子图布局,并选中第一个子图(编号从1开始,逐行进行)。让我们创建一个包含三个子图的图形,如 图 fig-empty-subplots 所示。

fig = plt.figure()
ax1 = fig.add_subplot(2, 2, 1)
ax2 = fig.add_subplot(2, 2, 2)
ax3 = fig.add_subplot(2, 2, 3)
plt.show()
图 8.2

Jupyter Notebook的一个特点是,每个单元格执行完毕后,图表的状态会被重置。因此,用于生成单个图形的所有命令都必须包含在同一个单元格内。

add_subplot返回的Axes对象拥有自己的绘图方法。标准做法是使用这些对象的方法(例如ax.plot())而不是顶层函数(plt.plot())。让我们在第三个子图ax3上绘制一个模拟的随机游走过程,这是一个与有效市场假说密切相关的概念,如 图 fig-single-plot 所示。

补充:随机游走与金融市场

随机游走(Random Walk)是金融经济学中的一个核心概念。一个简单的离散时间随机游走可以表示为: \(P_t = P_{t-1} + \epsilon_t\) 其中 \(P_t\) 是资产在时间 \(t\) 的价格,\(\epsilon_t\) 是一个均值为零的随机扰动项(白噪声)。这个公式的直观含义是,明天的价格等于今天的价格加上一个不可预测的随机变化。

这是弱式有效市场假说(Weak-form Efficient-market Hypothesis)的一个关键推论。该假说认为,当前资产价格已经完全反映了所有历史价格信息。因此,基于历史价格的任何交易策略都无法持续获得超额回报。我们在这里生成的np.random.standard_normal(50).cumsum()就是一个典型的随机游走过程的可视化。

fig = plt.figure()
ax1 = fig.add_subplot(2, 2, 1)
ax2 = fig.add_subplot(2, 2, 2)
ax3 = fig.add_subplot(2, 2, 3)

# 在第三个子图上绘制一个随机游走
ax3.plot(np.random.standard_normal(50).cumsum(), color='black', linestyle='dashed')
plt.show()
图 8.3

你可能会注意到,运行绘图命令时会出现类似<matplotlib.lines.Line2D at ...>的输出。这只是创建的matplotlib对象。你可以在该行末尾加上一个分号(;)来抑制这个输出。

现在我们可以通过调用相应Axes对象的实例方法来填充其他空的子图。让我们在第一个子图上添加一个直方图,在第二个子图上添加一个散点图,如 图 fig-additional-plots 所示。

fig = plt.figure()
ax1 = fig.add_subplot(2, 2, 1)
ax2 = fig.add_subplot(2, 2, 2)
ax3 = fig.add_subplot(2, 2, 3)

ax1.hist(np.random.standard_normal(100), bins=20, color='black', alpha=0.3)
ax2.scatter(np.arange(30), np.arange(30) + 3 * np.random.standard_normal(30))
ax3.plot(np.random.standard_normal(50).cumsum(), color='black', linestyle='dashed')

plt.show()
图 8.4

用这种方式创建图形和子图可能有些繁琐。一个更便捷的方法是plt.subplots,它会创建一个新的图形,并返回一个包含所创建的Axes对象的NumPy数组。

fig, axes = plt.subplots(2, 3)
# axes 是一个 2x3 的 Axes 对象数组
print(axes)

axes数组可以像其他NumPy数组一样进行索引,例如axes[0, 1]表示顶部中间的子图。sharexsharey选项非常有用;将它们设置为True可以确保所有子图共享相同的x轴或y轴,这对于在相同尺度上比较分布或时间序列至关重要。我们在 表 tbl-subplots-options 中总结了plt.subplots的关键选项。

表 8.1: matplotlib.pyplot.subplots 选项 {#tbl-subplots-options}
(a) matplotlib.pyplot.subplots 选项
参数 描述
nrows 子图的行数。
ncols 子图的列数。
sharex 所有子图应使用相同的x轴刻度。
sharey 所有子图应使用相同的y轴刻度。
subplot_kw 一个字典,包含传递给每个子图的add_subplot调用的关键字参数。
**fig_kw 传递给plt.figure调用的其他关键字参数,例如figsize=(8, 6)

8.1.1.1 调整子图周围的间距

默认情况下,matplotlib会在子图之间以及图形的外部添加填充。这个间距是相对于绘图尺寸的,并且在调整大小时会动态调整。你可以使用Figure对象上的subplots_adjust方法来精确控制这个间距: subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=None, hspace=None)

wspacehspace参数控制子图之间的宽度和高度间距,指定为平均轴宽度和高度的一小部分。让我们创建一个2x2的直方图网格,并移除所有子图间的间距,如 图 fig-no-spacing 所示。

fig, axes = plt.subplots(2, 2, sharex=True, sharey=True)

for i in range(2):
    for j in range(2):
        axes[i, j].hist(np.random.standard_normal(500), bins=50, color='black', alpha=0.5)

fig.subplots_adjust(wspace=0, hspace=0)
plt.show()
图 8.5

你可能会注意到轴标签重叠了。matplotlib不会自动检查这个问题,所以在这种情况下,你需要手动设置刻度位置和标签,这是我们稍后会讨论的主题。

8.1.2 颜色、标记和线型

ax.plot方法接受x和y坐标以及可选的样式参数。例如,要用绿色虚线绘制x对y的图,你可以写成:ax.plot(x, y, linestyle='--', color='g')

常见的颜色有单字母代码(例如,’g’代表绿色,’k’代表黑色),但你也可以使用十六进制代码(例如'#CECECE')来指定任何颜色。有多种线型可选,例如'-'代表实线,'--'代表虚线,':'代表点线。

线图也可以有标记(markers)来突出实际的数据点。这很有用,因为matplotlib通过在点之间进行插值来创建连续的线条,这有时会掩盖它们的确切位置。让我们绘制一个带标记的随机游走,如 图 fig-line-markers 所示。

fig, ax = plt.subplots()
ax.plot(np.random.standard_normal(30).cumsum(), color='black', linestyle='dashed', marker='o');
plt.show()
图 8.6

默认情况下,点是线性插值的。你可以用drawstyle选项来改变这种行为。在经济学中,一个常见的用例是创建阶梯图,例如在可视化税收等级或离散模拟结果时。drawstyle='steps-post'选项创建的图表中,线条会保持其水平直到下一个数据点。让我们在 图 fig-drawstyle 中比较默认插值和阶梯图。

fig, ax = plt.subplots()
data = np.random.standard_normal(30).cumsum()

ax.plot(data, color='black', linestyle='dashed', label='Default')
ax.plot(data, color='black', linestyle='-', drawstyle='steps-post', label='steps-post')
ax.legend()
plt.show()
图 8.7

8.1.3 刻度、标签和图例

大多数图表的装饰元素都可以通过Axes对象的方法来访问。这包括set_xlimset_xticksset_xticklabels,它们分别控制绘图范围、刻度位置和刻度标签。

这些方法有两种模式: * 不带参数调用时,它们返回当前的参数值(例如ax.get_xlim())。 * 带参数调用时,它们设置参数值(例如ax.set_xlim([0, 10]))。

8.1.3.1 设置标题、轴标签、刻度和刻度标签

让我们通过绘制一个真实的经济时间序列来说明轴的定制:美国实际国内生产总值(GDP)。我们将从联邦储备经济数据库(FRED)获取数据。图 fig-gdp-default 显示了默认的图表。

from fredapi import Fred
fred = Fred(api_key='f2a2c60b6dc82682031f4ce84bf6da18')
gdp = fred.get_series('GDPC1', start_date='2000-01-01')

fig, ax = plt.subplots()
ax.plot(gdp.index, gdp.values)
plt.show()
图 8.8

默认的x轴标签过于拥挤,信息量也不大。我们可以使用set_xticks来指定刻度的位置,并使用set_xticklabels来提供自定义标签,从而改善这一点。在 图 fig-gdp-custom 中,我们将在特定的年份设置刻度并相应地标记它们,同时为图表添加标题和轴标签以提高清晰度。

fig, ax = plt.subplots()
ax.plot(gdp.index, gdp.values)

# 自定义图表
ticks_loc = pd.to_datetime(['2000-01-01', '2005-01-01', '2010-01-01', '2015-01-01', '2020-01-01', '2025-01-01'])
ax.set_xticks(ticks_loc)
ax.set_xticklabels(['2000', '2005', '2010', '2015', '2020', '2025'], rotation=30, fontsize='small')

ax.set_title('2000年以来美国实际GDP')
ax.set_xlabel('年份')
ax.set_ylabel('十亿美元 (2017年不变价)')

# 一种更便捷的设置属性的方法
# ax.set(xticks=ticks_loc, 
#        xticklabels=['2000', '2005', '2010', '2015', '2020', '2025'],
#        title='2000年以来美国实际GDP', 
#        xlabel='年份', 
#        ylabel='十亿美元 (2017年不变价)')
# ax.tick_params(axis='x', labelrotation=30, labelsize='small')

plt.show()
图 8.9

8.1.3.2 添加图例

图例对于识别图表中的不同元素至关重要。创建图例最简单的方法是在绘制每个序列时传递label参数,然后调用ax.legend()来显示图例。

让我们绘制三个不同的模拟资产路径,分别代表低、中、高波动性的股票,并添加一个图例来区分它们。结果见 图 fig-legend-example

fig, ax = plt.subplots()

# 模拟三个不同波动率的随机游走
ax.plot(np.random.standard_normal(1000).cumsum(), color='black', linestyle='-', label='低波动率')
ax.plot(1.5 * np.random.standard_normal(1000).cumsum(), color='black', linestyle='--', label='中波动率')
ax.plot(2.0 * np.random.standard_normal(1000).cumsum(), color='black', linestyle=':', label='高波动率')

ax.legend(loc='best')
plt.show()
图 8.10

ax.legend()中的loc参数告诉matplotlib将图例放在哪里。默认值'best'会尝试找到对数据遮挡最少的位置。

8.1.4 注释与在子图上绘图

除了标准的图表类型,你可能还需要添加自定义的注释——文本、箭头或形状——来突出特定的特征。ax.textax.annotate函数就是用于此目的。

让我们创建一个经典的经济学图表:在一个主要金融指数的时间序列上标注关键事件。我们将绘制标普500指数,并突出显示互联网泡沫峰值、2008年金融危机期间雷曼兄弟的倒闭,以及2020年3月的COVID-19市场崩盘。我们将从FRED获取实时数据。参见 图 fig-sp500-annotated

sp500 = fred.get_series('SP500', start_date='1995-01-01')

fig, ax = plt.subplots(figsize=(12, 6))
sp500.plot(ax=ax, color='black')

crisis_data = [
    (pd.to_datetime('2000-03-24'), '互联网泡沫顶峰'),
    (pd.to_datetime('2008-09-15'), '雷曼兄弟破产'),
    (pd.to_datetime('2020-03-23'), '新冠疫情市场探底')
]

for date, label in crisis_data:
    price_at_date = sp500.asof(date)
    ax.annotate(label,
                xy=(date, price_at_date),
                xytext=(date, price_at_date + 1000),
                arrowprops=dict(facecolor='black', headwidth=4, width=2, headlength=4),
                horizontalalignment='left',
                verticalalignment='top')

ax.set_xlim(['1998-01-01', '2024-01-01'])
ax.set_ylim()
ax.set_title('标普500指数与重大金融事件')
ax.set_ylabel('指数值')
ax.set_xlabel('日期')
plt.show()
图 8.11

绘制形状需要创建patch对象,并使用ax.add_patch将其添加到子图中。让我们绘制几个基本形状,如 图 fig-shapes 所示。这对于阐释理论概念非常有用,比如在供需图中说明消费者剩余。

fig, ax = plt.subplots()

rect = plt.Rectangle((0.2, 0.75), 0.4, 0.15, color='black', alpha=0.3)
circ = plt.Circle((0.7, 0.2), 0.15, color='blue', alpha=0.3)
pgon = plt.Polygon([[0.15, 0.15], [0.35, 0.4], [0.2, 0.6]], color='green', alpha=0.5)

ax.add_patch(rect)
ax.add_patch(circ)
ax.add_patch(pgon)
plt.show()
图 8.12

8.1.5 将图表保存到文件

你可以使用fig.savefig保存当前活动的图形。文件格式会根据文件扩展名自动推断。对于学术论文,通常首选PDF或SVG等矢量格式,因为它们可以无损缩放。对于演示文稿或网页使用,高分辨率的PNG通常是最佳选择。dpi选项控制分辨率,单位是每英寸点数。

# 保存为高分辨率PNG
# fig.savefig('my_figure.png', dpi=400)

# 为出版物保存为矢量PDF
# fig.savefig('my_figure.pdf')

表 tbl-savefig-options 列出了一些关键的savefig选项。

表 8.2: fig.savefig 的一些选项 {#tbl-savefig-options}
(a) fig.savefig 的一些选项
参数 描述
fname 包含文件路径的字符串或Python文件类对象。
dpi 图形分辨率,单位为每英寸点数。
facecolor, edgecolor 子图外图形背景的颜色。
format 显式文件格式(‘png’, ‘pdf’, ’svg’等)。
bbox_inches 要保存的图形部分。’tight’常用于裁剪空白。

8.1.6 matplotlib配置

matplotlib有一个广泛的配置系统,用于自定义默认设置,如图形大小、字体样式、颜色等。你可以使用plt.rc以编程方式修改这些设置。第一个参数是要自定义的组件(例如'figure', 'axes'),后面跟着新参数的关键字参数。

# 将默认图形尺寸设置得更大
plt.rc('figure', figsize=(10, 6))

# 为所有绘图元素设置字体属性
# plt.rc('font', family='serif', weight='normal', size=12)

你可以随时通过调用plt.rcdefaults()恢复默认设置。对于持久性的更改,你可以修改matplotlibrc配置文件。

8.2 使用pandas和seaborn绘图

虽然matplotlib功能强大,但有时代码会显得冗长。对于许多标准的统计图表,尤其是在使用pandas DataFrame时,更高级别的库可以极大地简化你的工作。pandas有自己内置的绘图方法,它们是对matplotlib的封装;而seaborn则是一个专门的统计可视化库,提供了更多复杂的图表类型。

8.2.1 线图

pandas中的SeriesDataFrame对象都有一个.plot属性,为创建各种图表提供了便捷的接口。默认情况下,它会创建一个线图。对象的索引被用作x轴。图 fig-series-plot 显示了一个由Series对象生成的简单图表。

s = pd.Series(np.random.standard_normal(10).cumsum(), index=np.arange(0, 100, 10))
s.plot()
plt.show()
图 8.13

表 tbl-series-plot-args 列出了Series.plot方法的一些参数。

表 8.3: Series.plot 方法参数 {#tbl-series-plot-args}
(a) Series.plot 方法参数
参数 描述
label 图例的标签。
ax 用于绘图的matplotlib子图对象。
style 样式字符串,例如 'k--'
kind 图表类型:'line', 'bar', 'barh', 'hist', 'kde'等。
logy 在y轴上使用对数刻度。
use_index 使用对象的索引作为刻度标签。
rot 刻度标签的旋转角度(0到360)。
xticks, yticks 用于轴刻度的值。
xlim, ylim 轴的限制范围。
grid 显示轴网格(默认为关闭)。

当绘制一个DataFrame时,每一列都会在同一个子图上被绘制为一条独立的线,并且会自动创建一个图例。为了提供一个有意义的例子,让我们比较自2000年以来七国集团(G7)国家的人均GDP。我们将使用wbdata包从世界银行获取数据。结果如 图 fig-dataframe-plot 所示。

import wbdata

# G7国家的ISO代码
countries = ['USA', 'CAN', 'GBR', 'FRA', 'DEU', 'ITA', 'JPN']
# 人均GDP的指标
indicators = {'NY.GDP.PCAP.CD': '人均GDP'}

# 获取数据
g7_gdp = wbdata.get_dataframe(indicators, country=countries, convert_date=True)
# 重塑数据,使国家成为列
g7_gdp_unstacked = g7_gdp.unstack('country')
g7_gdp_unstacked.columns = g7_gdp_unstacked.columns.droplevel(0)

# 绘图
g7_gdp_unstacked.plot(figsize=(10, 6))
plt.title('G7国家人均GDP')
plt.ylabel('现价美元')
plt.xlabel('年份')
plt.legend(title='国家')
plt.grid(True)
plt.show()
图 8.14

DataFrame.plot方法有处理列的额外选项。例如,你可以在不同的子图中绘制每一列。这些选项总结在 表 tbl-dataframe-plot-args 中。

表 8.4: DataFrame特有的plot参数 {#tbl-dataframe-plot-args}
(a) DataFrame特有的plot参数
参数 描述
subplots 在单独的子图中绘制DataFrame的每一列。
sharex 如果subplots=True,则共享相同的x轴。
sharey 如果subplots=True,则共享相同的y轴。
figsize 要创建的图形尺寸,以元组形式表示。
title 图表的标题。
legend 添加子图图例(默认为True)。

8.2.2 条形图

plot.bar()plot.barh()方法分别创建垂直和水平的条形图。Series或DataFrame的索引被用作刻度标签。让我们创建一个代表2023年美国不同能源发电份额的Series,并用垂直和水平条形图在 图 fig-bar-plots 中进行可视化。

energy_data = pd.Series({'天然气': 43.1, '煤炭': 16.2, '核能': 18.6, 
                         '可再生能源': 21.4, '其他': 0.7}, 
                        name='能源占比 (%)')

fig, axes = plt.subplots(2, 1, figsize=(8, 8))

energy_data.plot.bar(ax=axes, color='black', alpha=0.7)
axes.set_ylabel('占比 (%)')
axes.set_title('垂直条形图')
axes.tick_params(axis='x', rotation=45)


energy_data.plot.barh(ax=axes, color='black', alpha=0.7)
axes.set_xlabel('占比 (%)')
axes.set_title('水平条形图')

plt.tight_layout()
plt.show()
图 8.15

对于一个DataFrame,条形图会将每一行中的值并排分组。在 图 fig-df-bar 中,我们将比较最近一年美国和中国的GDP构成(按支出法计算)。

gdp_comp = {
    '居民消费': {'USA': 68.2, 'CHN': 37.1},
    '政府支出': {'USA': 14.1, 'CHN': 14.5},
    '投资': {'USA': 17.5, 'CHN': 43.1},
    '净出口': {'USA': -3.8, 'CHN': 3.5}
}
df_gdp = pd.DataFrame(gdp_comp).T
df_gdp.index.name = '构成部分'

df_gdp.plot.bar()
plt.ylabel('GDP占比 (%)')
plt.title('GDP构成对比: 美国 vs. 中国 (2022)')
plt.xticks(rotation=0)
plt.show()
图 8.16

通过传递stacked=True,我们可以创建一个堆叠条形图,其中每一行的值会堆叠在一起。这对于可视化部分与整体的关系非常有效。图 fig-df-bar-stacked 展示了同样的GDP构成数据的堆叠格式。

df_gdp.plot.barh(stacked=True, alpha=0.7)
plt.xlabel('GDP占比 (%)')
plt.title('GDP构成对比: 美国 vs. 中国 (2022)')
plt.legend(title='国家')
plt.show()
图 8.17

在经济学中,一个常见的任务是可视化频率分布。对于分类数据,一个有用的方法是 data['category'].value_counts().plot.bar()。让我们将此应用于一个更复杂的场景。我们将分析世界银行关于不同世界地区收入水平的数据。首先,我们创建一个地区和收入水平的交叉表,然后将每行归一化以得到比例。

表 8.5: 各区域内按收入水平划分的国家比例
# 从世界银行获取国家元数据
countries_meta = wbdata.get_country(display=False)
df_meta = pd.DataFrame(countries_meta)
# 过滤掉非国家实体/聚合数据
df_meta = df_meta[df_meta['region'] != 'Aggregates']

# 创建一个交叉表
party_counts = pd.crosstab(df_meta['region'], df_meta['incomeLevel'])

# 归一化以获得比例
party_pcts = party_counts.div(party_counts.sum(axis=1), axis=0)
party_pcts

现在,让我们创建一个堆叠条形图来可视化这些比例(图 fig-income-region-plot)。这张图揭示了世界不同地区的经济结构。例如,欧洲和中亚地区拥有高比例的高收入国家,而撒哈拉以南非洲则以低收入和中等偏下收入国家为主。

party_pcts.plot.bar(stacked=True, figsize=(10, 7))
plt.title('世界各区域经济构成')
plt.ylabel('国家比例')
plt.xlabel('区域')
plt.xticks(rotation=45, ha='right')
plt.legend(title='收入水平')
plt.show()
图 8.18

对于涉及此类聚合的分析,seaborn提供了一种更直接、更强大的方法。让我们来探究各世界地区的平均预期寿命。seaborn会自动计算每个地区的均值和95%置信区间。

import seaborn as sns

# 获取预期寿命数据
indicators = {'SP.DYN.LE00.IN': 'Life Expectancy'}
df_life = wbdata.get_dataframe(indicators, country='all', convert_date=False)
df_life = df_life.reset_index()

# 与元数据合并以获取区域信息
df_merged = pd.merge(df_life, df_meta[['iso2Code', 'region']], left_on='country', right_on='iso2Code')
df_merged.dropna(subset=['Life Expectancy', 'region'], inplace=True)

plt.figure(figsize=(10, 6))
sns.barplot(x='Life Expectancy', y='region', data=df_merged, palette='viridis')
plt.title('各世界地区平均预期寿命 (含95%置信区间)')
plt.show()
图 8.19

图 fig-seaborn-barplot 中,条形上的黑线代表均值的95%置信区间,为我们的估计提供了一个不确定性的度量。seabornhue参数允许我们引入另一个分类维度。在 图 fig-seaborn-hue 中,我们除了按地区划分外,还按收入水平对数据进行了拆分。

# 同时合并收入水平信息
df_merged_hue = pd.merge(df_life, df_meta[['iso2Code', 'region', 'incomeLevel']], left_on='country', right_on='iso2Code')
df_merged_hue.dropna(subset=['Life Expectancy', 'region', 'incomeLevel'], inplace=True)

plt.figure(figsize=(12, 8))
sns.barplot(x='Life Expectancy', y='region', hue='incomeLevel', data=df_merged_hue)
plt.title('按地区和收入水平划分的平均预期寿命')
plt.legend(title='收入水平', bbox_to_anchor=(1.05, 1), loc=2)
plt.tight_layout()
plt.show()
图 8.20

8.2.3 直方图和密度图

直方图是一种条形图,它以离散化的方式显示数值的频率。让我们来考察标普500指数的日收益率分布。金融收益率以其非正态分布而闻名,常常表现出“肥尾”现象。直方图可以帮助我们看到这一点,如 图 fig-returns-hist 所示。

sp500_ret = np.log(sp500 / sp500.shift(1)).dropna()

plt.figure(figsize=(10, 6))
sp500_ret.plot.hist(bins=100)
plt.title('标普500日对数收益率分布')
plt.xlabel('对数收益率')
plt.show()
图 8.21

一个相关的图是密度图,或称核密度估计(Kernel Density Estimate, KDE),它从数据中计算出一个连续概率分布的估计。图 fig-returns-kde 显示了同样是标普500收益率的KDE图。

plt.figure(figsize=(10, 6))
sp500_ret.plot.density()
plt.title('标普500日对数收益率的密度估计')
plt.xlabel('对数收益率')
plt.show()
图 8.22

seabornhistplot函数非常方便,因为它可以同时绘制直方图和KDE。让我们用它来可视化一个双峰分布,这可能代表,例如,一个男女混合群体的身高分布。结果如 图 fig-bimodal-hist 所示。

comp1 = np.random.normal(0, 1, size=200)
comp2 = np.random.normal(10, 2, size=200)
values = pd.Series(np.concatenate([comp1, comp2]))

sns.histplot(values, bins=100, color='black', kde=True)
plt.title('一个正态混合分布的直方图')
plt.show()
图 8.23

8.2.4 散点图

散点图对于考察两个连续变量之间的关系非常有价值。让我们使用FRED的数据来研究一些核心的宏观经济关系。我们将考察CPI(通货膨胀)、M1货币供应量、3个月期国库券利率和失业率的对数差分。

表 8.6: 美国宏观经济数据对数差分 (最近5个季度)
macro_vars = {'CPIAUCSL': 'cpi', 'M1SL': 'm1', 'TB3MS': 'tbilrate', 'UNRATE': 'unemp'}
macro_data = fred.get_series_latest_release('CPIAUCSL').to_frame(name='cpi')
for code, name in list(macro_vars.items())[1:]:
    macro_data[name] = fred.get_series_latest_release(code)

macro_data = macro_data.resample('Q').mean()
trans_data = np.log(macro_data).diff().dropna()
trans_data.tail()

seabornregplot函数可以创建一个散点图,并拟合一条线性回归线,同时还带有回归线的置信区间。让我们看一下M1货币供应量变化与失业率变化之间的关系(图 fig-regplot)。

sns.regplot(x='m1', y='unemp', data=trans_data)
plt.title('log(M1)的变化 vs log(unemp)的变化')
plt.show()
图 8.24

在探索性分析中,查看一组变量之间所有的成对散点图通常很有用。这被称为配对图或散点图矩阵。seabornpairplot函数使这变得很容易。在对角线上,它可以显示每个变量的直方图或KDE。图 fig-pairplot 显示了我们宏观经济变量的配对图。

sns.pairplot(trans_data, diag_kind='kde', plot_kws={'alpha': 0.4})
plt.show()
图 8.25

8.2.5 分面网格与分类数据

对于具有多个分类分组变量的数据集,分面网格(facet grid)是一种有效的可视化策略。它创建了一个图表矩阵,其中行和列对应于分类变量的水平。seaborncatplot函数是实现此功能的强大工具。

让我们回到世界银行的预期寿命数据。在 图 fig-catplot-facet 中,我们创建了一个预期寿命与地区的条形图,但现在我们将为每个收入水平创建一个单独的列(col)。为了清晰起见,我们筛选数据只包括几个收入水平。

# 为了清晰起见进行筛选
income_filter = ['High income', 'Upper middle income', 'Lower middle income']
df_filtered = df_merged_hue[df_merged_hue['incomeLevel'].isin(income_filter)]

sns.catplot(x='Life Expectancy', y='region', col='incomeLevel', 
            kind='bar', data=df_filtered, height=5, aspect=1.2)
plt.show()
图 8.26

我们还可以通过使用row参数来进一步扩展网格。在 图 fig-catplot-box 中,我们将图表kind切换为'box'来创建箱形图,它显示了中位数、四分位数和异常值。这比简单的条形图能提供更多关于分布的信息。我们将可视化按地区分面的预期寿命与收入水平的关系。

sns.catplot(x='incomeLevel', y='Life Expectancy', col='region', col_wrap=3,
            kind='box', data=df_filtered, height=4, aspect=1.5)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
图 8.27

8.3 其他Python可视化工具

Python的可视化生态系统是广阔且不断发展的。虽然我们本章的重点是使用matplotlibpandasseaborn进行用于分析和出版的静态图形,但大量的开发工作都集中在为Web创建交互式图形上。

PlotlyBokehAltair这样的库,允许你构建动态的、可交互的可视化,这对于基于Web的仪表盘、论文的在线附录或面向客户的应用来说是理想的选择。学习这些工具可以是有价值的下一步,特别是如果你期望在数据科学、咨询或政策分析等领域发展,因为在这些领域,交互式数据探索是关键。

8.4 总结

本章的目标是为你们提供一个功能强大且稳健的工具箱,用于在经济学中进行基本的数据可视化。我们已经看到了如何从使用matplotlib进行低级别、精细的控制,过渡到使用seabornpandas进行高级别、具有统计意识的绘图。

将你的分析结果进行可视化传达,是一项与分析本身同等重要的技能。一个深刻的发现如果不能被清晰、有说服力地传达,那它就是无用的。用你们其他课程的数据集进行练习,打造一幅清晰、真实、有力的图表的能力,是一位优秀的应用人才的标志之一。