从巨无霸指数看全球货币价值

巨无霸指数(Big Mac index)是一个非正式的经济指数,用以测量货币的汇率理论上是否合理。这种测量方法假定购买力平价理论成立。购买力平价理论是一种研究和比较各国不同的货币之间购买力关系的理论。购买力平价的理论指出,在对外贸易平衡的情况下,两国之间的汇率将会趋向于靠拢购买力平价。一般来讲,这个指标要根据相对于经济的重要性考察许多货物才能得出。

✭ 购买力平价是指两种货币之间的汇率决定于它们单位货币购买力之间的比例。

✭ 购买力平价,也就是平常所说的购买力水平。选择巨无霸的原因是,巨无霸在非常多的国家均有销售,而且巨无霸这个商品的标准化程度非常高,它在全球各地的制作规格都是相同的,由当地麦当劳的经销商负责为各地的制作材料议价。这些因素使该指数能有意义地比较各国货币。这个项目展示了经济学家如何计算巨无霸指数,让我们一起来体验一下经济学家为什么需要数据可视化。在这个项目中,我们将一起探索这些问题:

✭ 中国的货币购买力在过去 20 年当中是如何变化的?

✭ 各个国家的货币购买力是被高估了还是低估了呢?哪些货币升值的空间大?

✭ 像经济学家一样思考,经济学家是如何通过数据认知世界的?

巨无霸指数的原理是,用一个国家的巨无霸当地货币的价格,除以另一个国家的巨无霸当地货币的价格,然后用该商数用来跟实际的两个国家之间的汇率比较;要是商数比汇率为低,就表示第一国货币的汇率被低估了(根据购买力平价理论);相反,要是商数比汇率为高,则第一国货币的汇率被高估了。

实际的计算过程是,假设全世界的麦当劳巨无霸汉堡包的价格都应该是一样的,然后将各地的巨无霸当地价格,通过汇率换算成美元售价,就可以比较出各个国家的购买力水平差异。《经济学人》创立的巨无霸指数,用以计算各国货币相对美元的汇率是否合理,同样我们也可以用相同的原理来计算各国货币相对于人民币的汇率是否合理。指数根据购买力平价理论出发,1 美元在全球各地的购买力都应相同,若某地的巨无霸售价比美国低,就表示其货币相对美元的汇率被低估,相反则是高估。至於选择巨无霸的原因,是由于全球 120 个国家及地区均有售,而且制作规格相同,具一定参考价值。

巨无霸指数的由来,它是由《经济学人》于 1986 年 9 月推出,此后该报每年出版一次新的指数。该指数在英语国家里衍生了 Burgernomics(汉堡包经济)一词。 在 2004 年 1 月,《经济学人》推出了 Tall Latte index(中杯鲜奶咖啡指数),计算原理一样。在 1997 年,该报还出版了一份 “可口可乐地图”,用每个国家的人均可乐饮用量,比较国与国间的财富。

巨无霸指数数据集:数据来源于《经济学人》。

经济学人整理数据集时,源数据来自多个地方:巨无霸的价格直接来自麦当劳世界各地的经销商;汇率来自汤森路透;用于计算欧元区平均值的 GDP 和人口数据来自 Eurostat;人均 GDP 数据来自IMF《世界经济展望》报告。

1. 巨无霸指数数据集

经济学人整理的原始数据集存储在 big-mac-source-data.csv 文件中,数据集中包括了各个国家巨无霸的售价、各国 GDP、各国货币相对于美元的汇率等信息,具体的字段包含如下:

  • name:国家的名称
  • iso_a3:三个字符 ISO 3166-1 国家代码
  • currency_code:三个字符 ISO 4217 货币代码
  • local_price:巨无霸本地货币的售价
  • dollar_ex:本地货币相对于 1 美元的汇率
  • GDP_dollar:人均 GDP,单位为美元
  • date:观测和采集数据的时间

数据集的时间跨度为 20 年(2000 ~ 2020 年),从 2010 年以后数据集每 6 个月会重新采集一次各个国家的巨无霸售价、美金汇率和 GDP,我们用 Python 把它加载到脚本中。

请直接执行以下的脚本。

In [ ]:

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

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

# 加载数据
df_big_mac_data = pd.read_csv('/data/course_data/visualization/big-mac-source-data.csv')

# 查看数据集的总体信息
df_big_mac_data.head(5)

2. 转换为统一的货币

计算巨无霸指数,我们第一步就需要把所有的本地货币的价格转换为统一的货币价格,我们这里使用美元作为统一货币。

请直接执行以下的脚本。In [ ]:

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

# 去除 dollar_ex 等于 0 或是空值的数据
df_big_mac_data = df_big_mac_data[df_big_mac_data.dollar_ex > 0].copy()

# 计算直方图所需要的数据集
df_big_mac_data['dollar_price'] = df_big_mac_data['local_price'] / df_big_mac_data['dollar_ex']
df_big_mac_data

3. 中国巨无霸汉堡价格的变化

我们通过比较巨无霸汉堡在中国的售价(转换为 USD)与在美国售价之间的差距,来看一看人民币的相对购买力,如果大家在过去的几年中去美国旅游过,是否会觉得在美国买东西挺便宜的呢?如果还没有去美国旅游过,可以思考一下为什么美国人总希望人民币升值呢?

阅读脚本,并取消部分脚本的注释:

# ** 取消下面 3 行代码注释 ** #
band = Band(base='x', lower='y_cny', upper='y_usd', level='underlay',
            fill_alpha=0.5, line_width=1, source=data)
p.add_layout(band)

In [ ]:

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

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

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

# 绘制巨无霸在中国和美国的销售价格变化曲线
data = {
    'x': pd.to_datetime(
        df_big_mac_data.loc[
            df_big_mac_data.currency_code == 'CNY', 
            'date'
        ]
    ),
    # 提取中国的价格数据
    'y_cny': df_big_mac_data.loc[df_big_mac_data.currency_code == 'CNY', 'dollar_price'],
    # 提取美国的价格数据
    'y_usd': df_big_mac_data.loc[df_big_mac_data.currency_code == 'USD', 'dollar_price']
}
data = ColumnDataSource(data=data)

# 创建绘图对象 p
p = figure(
    x_axis_type = 'datetime',
    frame_height = 350, frame_width=600
)

# 绘制两条曲线,红色的为巨无霸在美国销售的价格(USD),
# 绿色为巨无霸在中国销售的价格(USD)
p.line(x='x', y='y_cny', color='green', line_width=2, legend_label='中国', source=data)
p.line(x='x', y='y_usd', color='tomato', line_width=2, legend_label='美国', source=data)

# 用默认的颜色填充两条价格折线的中间部分,
# 似的加强两条曲线之间的差距对比,突出视觉的重点
# ** 取消下面 3 行代码注释 ** #
band = Band(base='x', lower='y_cny', upper='y_usd', level='underlay',
            fill_alpha=0.5, line_width=1, source=data)
p.add_layout(band)

# 设置绘图的坐标轴样式,以及图例的样式
p.axis.axis_label_text_font_size = '12pt'
p.axis.major_label_text_font_size = '12pt'
p.xaxis.axis_label = '时间(年)'
p.yaxis.axis_label = '巨无霸价格(USD)'
p.legend.orientation = 'horizontal'
p.legend.location = 'top_left'
p.legend.label_width = 40
show(p)

4. 计算巨无霸指数

上面一步我们已经把各国巨无霸的价格转换成了统一的货币单位 – 美元(USD),下面我们选择人民币和美元作为两个基础货币,来计算巨无霸指数,研究各国货币相对于人民币和美元的汇率是高估了还是低估了。从而可以从巨无霸指数的维度,了解一个国家的货币相对其他国家货币的购买力。

修改基础货币的取值:

# 设置基础货币的种类
base_currencies = ['CNY', 'USD']

In [ ]:

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

# 设置基础货币的种类
base_currencies = ['CNY','USD']

# 对整体数据集按照日期进行分组
groups_by_date = df_big_mac_data.groupby(by='date')

# 对选定的基础货币分别计算
for currency_code in base_currencies:
    
    # 对每一个按日期分组的数据进行各个年度的巨无霸指数
    for date, df in groups_by_date:
        
        # 找到基础货币的美金价格 'dollar_price'
        base_price = df.loc[
            df.currency_code == currency_code, 
            'dollar_price'].values[0]

        # 基础货币的巨无霸指数 = (各个国家巨无霸的 dollar_price) / base_price - 1
        df_big_mac_data.loc[df_big_mac_data.date == date, currency_code] = \
            df['dollar_price'] / base_price - 1

# 查看计算结果        
df_big_mac_data

5. 绘制巨无霸指数

运行下面的脚本,绘制基于美元(USD)作为基础货币的巨无霸指数,思考两个问题:

  • 哪些国家的货币相对于美元的汇率被高估了?
  • 哪个地区的货币相对于美元被低估的程度最大?
  • 汇率被高估和被低估分别意味着什么?

根据提示,修改脚本,并在此次执行脚本,绘制基于人民币(CNY)作为基础货币的巨无霸指数:

big_mac_index = 'USD'

修改为:

big_mac_index = 'CNY'

In [ ]:

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

def plot_big_mac_index(df_big_mac_data, big_mac_index = 'USD', last_date = '2020-01-14'):
    # 截取 last_date, current_code 所对应的数据
    df_last_date = df_big_mac_data[df_big_mac_data.date == last_date].copy()
    df_last_date.sort_values(by=big_mac_index, inplace=True)

    # 准备绘图所需要的数据
    data = {
        'x': df_last_date[big_mac_index],
        'y': df_last_date['name'],
        'xs': [[x, 0] for x in df_last_date[big_mac_index]],
        'ys': [[y, y] for y in df_last_date['name']]
    }

    # 创建绘图工具 p
    p = figure(
        y_range=data['y'],
        frame_height=11 * len(df_last_date), frame_width=600
    )

    # 导入颜色处理的工具
    from bokeh.transform import linear_cmap
    from bokeh.palettes import all_palettes
    from bokeh.models import Span

    # 绘制点到 0 点黑色分割线之间的线段
    p.multi_line(xs='xs', ys='ys', 
                 line_width=3, line_alpha=0.4,
                 line_color=linear_cmap('x', all_palettes['Set1'][3], 0, 0), 
                 source=data)
    # 绘制各国巨无霸指数的点,指数大于 0 显示为绿色,指数小于 0 显示为红色
    p.scatter(x='x', y='y', color=linear_cmap('x', all_palettes['Set1'][3], 0, 0), size=6, source=data)
    # 绘制 0 分割线,提高可视化的效果,更容易人们的视觉分类
    p.add_layout(Span(location=0, 
                      dimension='height', 
                      line_color='black', line_width=2, level='underlay'))
    # 对绘图的背景进行适当的修饰,提高反差
    p.grid.grid_line_color = 'white'
    p.background_fill_color = 'grey'
    p.background_fill_alpha = 0.1
    # 设置 x 坐标轴字体,避免过小看不清
    p.axis.axis_label_text_font_size = '12pt'
    p.xaxis.axis_label = '巨无霸指数(基础货币 %s)' % big_mac_index
    p.xaxis.major_label_text_font_size = '12pt'

    handle = show(p, notebook_handle=True)

# 开始绘制 2020 年 1 月份的巨无霸指数    
big_mac_index = 'CNY'
plot_big_mac_index(df_big_mac_data, big_mac_index = big_mac_index, last_date = '2020-01-14')

6. 计算各国人均 GDP 与巨无霸价格的线性回归

尽管巨无霸指数是令人耳目一新的简单方法,让我们能够了解一种货币相对其他货币的价值。但这种衡量货币相对价值的方法经常会被诟病,主要的反对意见是,汉堡不能轻易跨境交易,导致计算得出的货币相对价值没有任何意义。因此改进的方案是,我们可以使用各国人均 GDP 与巨无霸价格的线性回归对巨无霸的价格进行调整。考虑各国当地非贸易投入(租金和工人的工资),人们会期望巨无霸在较贫穷的国家售价应更便宜,而在较富裕的国家更昂贵。做线性回归的步骤如下:

我们首先做几件事情,对数据进行处理:

  • 去掉没有 GDP 数据的年份,只保留拥有 GDP 数据的国家;
  • 有时我们会在 “巨无霸” 指数中添加或删除国家/地区,但我们希望调整后的指数所基于的国家/地区列表保持一致。我们使用选定的国家列表来计算 GDP 与巨无霸价格之间的关系:

直接运行下面的脚本,根据 GDP 调整后的巨无霸价格将存储于 adj_price 列。

In [ ]:

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

# 去掉没有 GDP 数据的年份,只保留拥有 GDP 数据的国家
df_big_mac_data = df_big_mac_data[df_big_mac_data.GDP_dollar > 0]

# Python 里的 numpy 给了我们非常简便的方法来做线性回归
import numpy as np
def get_fit_function(x, y, degree):
    fit_coff = np.polyfit(x, y, degree)
    def func(x): 
        # 对于 degree 是 1 的线性回归,生成的函数是这个样子的:
        # y = fit_coff[0] * x ^ 1 + fit_coff[1] * x ^ 0
        return sum(fit_coff[i]*(x**(degree-i)) for i in range(degree+1))
    return func

# 选定国家列表来计算 GDP
regression_countries = ['ARG', 'AUS', 'AUT', 'BEL', 'BRA', 'CAN', 'CHE', 'CHL', 'CHN', 
                        'COL', 'CZE', 'DEU', 'DNK', 'EGY', 'ESP', 'EST', 'EUZ', 'FIN',
                        'FRA', 'GBR', 'GRC', 'HKG', 'HUN', 'IDN', 'IND', 'IRL', 'ISR', 
                        'ITA', 'JPN', 'KOR', 'MEX', 'MYS', 'NLD', 'NOR', 'NZL', 'PAK',
                        'PHL', 'POL', 'PRT', 'RUS', 'SAU', 'SGP', 'SWE', 'THA', 'TUR', 
                        'TWN', 'USA', 'ZAF']
df_big_mac_gdp_data = df_big_mac_data[df_big_mac_data.iso_a3.isin(regression_countries)].copy()

# 现在我们有了一篮子 “回归国家”,我们可以运行回归了,
# 基于 GDP 和巨无霸价格进行线性回归,并对巨无霸的价格进行调整,
# 每个测量的日期都需要进行一次线性回归。
groups_by_date = df_big_mac_gdp_data.groupby(by='date')

# 线性回归的函数结果存储在字典 fit_func_of_date 中
fit_func_of_date = dict()
for date, df in groups_by_date:
    fit_func = get_fit_function(df['GDP_dollar'], df['dollar_price'], 1)
    fit_func_of_date[date] = fit_func
    adj_price = fit_func(df['GDP_dollar'])
    df_big_mac_gdp_data.loc[df_big_mac_gdp_data.date == date, 'adj_price'] = adj_price

# 基于 GDP 调整价格后的数据集如下所示:
pd.options.display.max_columns = 6
pd.options.display.max_rows = 6
df_big_mac_gdp_data

7. 绘图来验证通过线性回归调整的价格是否正确

我们在第 6 步已经计算了每个日期(date)的回归,并对价格进行了调整,如果我们做对了所有事情,那么我们绘制图形时生成的所有点 (GDP_dollar, adj_price) 都应该在回归直线上。在这一个任务中,我们需要完成三个小步骤:

  • 直接执行脚本,观察执行结果;
  • 在「原始的价格点」和「调整后的价格点」之间绘制线段,提升可视化的直观效果;
  • 修改绘图矩阵的列宽。

In [ ]:

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

# 为了让可视化的图表坐标系的范围都是一致的,更加容易直观的比较,
# 我们引入 bokeh 当中的一个工具 DataRange1D,来限制 x 和 y 坐标轴的刻度值范围
from bokeh.models import DataRange1d
groups_by_date = df_big_mac_gdp_data.groupby(by='date')
x_range = DataRange1d(
    start=min(df_big_mac_gdp_data.GDP_dollar) - 5000, 
    end=max(df_big_mac_gdp_data.GDP_dollar) + 5000)
y_range = DataRange1d(
    start=min(
        min(df_big_mac_gdp_data.dollar_price),
        min(df_big_mac_gdp_data.adj_price)
    ) - 0.5, 
    end=max(
        max(df_big_mac_gdp_data.dollar_price), 
        max(df_big_mac_gdp_data.adj_price)
    ) + 0.5
)

# 每个日期(date)绘制一个散点图和趋势线
# 我们引入 bokeh 当中的另一个工具 NumeralTickFormatter,
# 让坐标轴的刻度更加美观,将数据呈现为 $xxxk,简短、明了
from bokeh.models import NumeralTickFormatter
# 引入 bokeh 中的一个布局工具,让所有的图放在一个网格中
from bokeh.layouts import gridplot

plots = []
# 对每一个数据采集日期进行绘图
for date, df in groups_by_date:
    
    # 创建绘图对象 p
    p = figure(
        title=date,
        x_range=x_range, y_range=y_range,
        frame_width=200, frame_height=200
    )
    
    # 获取在第 6 步计算得到的、采集日期为 date 的回归函数
    fit_func = fit_func_of_date[date]
    # 准备绘图数据
    data = {
        # GDP 数值作为 x 轴
        'x': df['GDP_dollar'].tolist(),
        # 原始价格点:美元价格作为 y 轴
        'y': df['dollar_price'].tolist(),
        # 调整后价格点:调整后的美元价格,由回归函数计算得出
        'y1': fit_func(df['GDP_dollar']).tolist()
    }
    # 绘制回归线
    p.line(x='x', y='y1', line_color='tomato', line_width=4, source=data)
    
    # 绘制各个原始价格点 -> 回归线之间的线段
    # p.multi_line(xs=[[data['x'][i], data['x'][i]] for i in range(len(df))],
    #              ys=[[data['y'][i], data['y1'][i]] for i in range(len(df))],
    #              line_width=2, line_color='black', alpha=0.2)

    # 绘制原始价格点
    p.scatter(x='x', y='y', color='black', alpha=0.2, size=6, source=data)
    # 绘制调整后价格点
    p.scatter(x='x', y='y1', color='royalblue', alpha=0.6, size=6, source=data)
    
    # 对绘图的坐标轴进行格式化
    p.xaxis.formatter = NumeralTickFormatter(format='$0,0a')
    p.x_range.range_padding = 0.1
    p.yaxis.axis_label = 'Actual Price ($)'
    p.xaxis.axis_label = 'GDP ($)'
    plots.append(p)

# 将所有的绘图以矩阵的方式来进行呈现
grid = gridplot(plots, ncols=3, toolbar_location='right')
show(grid)

8. 基于调整后的价格重新计算巨无霸指数

计算调整后的指数,需要考虑到基础货币的价格也发生了变化,新的巨无霸指数的计算公式如下:调整后指数=𝑑𝑜𝑙𝑙𝑎𝑟_𝑝𝑟𝑖𝑐𝑒𝑑𝑜𝑙𝑙𝑎𝑟_𝑝𝑟𝑖𝑐𝑒[𝑏𝑎𝑠𝑒_𝑐𝑢𝑟𝑟𝑒𝑛𝑐𝑦]𝑎𝑑𝑗_𝑝𝑟𝑖𝑐𝑒𝑎𝑑𝑗_𝑝𝑟𝑖𝑐𝑒[𝑏𝑎𝑠𝑒_𝑐𝑢𝑟𝑟𝑒𝑛𝑐𝑦]−1=𝑑𝑜𝑙𝑙𝑎𝑟_𝑝𝑟𝑖𝑐𝑒𝑎𝑑𝑗_𝑝𝑟𝑖𝑐𝑒÷𝑑𝑜𝑙𝑙𝑎𝑟_𝑝𝑟𝑖𝑐𝑒[𝑏𝑎𝑠𝑒_𝑐𝑢𝑟𝑟𝑒𝑛𝑐𝑦]𝑎𝑑𝑗_𝑝𝑟𝑖𝑐𝑒[𝑏𝑎𝑠𝑒_𝑐𝑢𝑟𝑟𝑒𝑛𝑐𝑦]−1调整后指数=dollar_pricedollar_price[base_currency]adj_priceadj_price[base_currency]−1=dollar_priceadj_price÷dollar_price[base_currency]adj_price[base_currency]−1

In [ ]:

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

# 对整体数据集按照日期进行分组
groups_by_date = df_big_mac_gdp_data.groupby(by='date')

# 对选定的基础货币分别计算
for currency_code in base_currencies:
    for date, df in groups_by_date:
        df_big_mac_gdp_data.loc[df_big_mac_gdp_data.date == date, 
                                'adj_' + currency_code] = \
        (df['dollar_price'] / df['adj_price']) / \
        (df[df.currency_code == currency_code]['dollar_price'].values[0] / \
         df[df.currency_code == currency_code]['adj_price'].values[0]) \
        - 1

df_big_mac_gdp_data

9. 绘制调整后的巨无霸指数

是的,这一步正是我们最终想要的绘图,现在我们有了第 8 步计算出来的数据,我们可以做与第 5 步几乎相同的事情。使用第 5 步已经写好的脚本 plot_big_mac_index,代入不同的参数,绘制基于 GDP 调整后的巨无霸指数。

  • 绘制以 CNY 为基础货币,基于 GDP 调整价格后的巨无霸指数;
  • 绘制以 USD 为基础货币,基于 GDP 调整价格后的巨无霸指数。

In [ ]:

# 开始绘制 2020 年 1 月份的巨无霸指数
big_mac_index = 'adj_CNY'
plot_big_mac_index(df_big_mac_gdp_data, big_mac_index = big_mac_index, last_date = '2020-01-14')

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