最近要在一门微观经济学课上给学生演示一般均衡理论,最直接的工具就是埃奇沃思盒(Edgeworth Box)。以前我在白板上手画,画一条无差异曲线都要小心翼翼,画出一整族更是费劲,更别说还要表达帕累托改进、契约曲线这些动态概念。后来换成了 Python 脚本,改一个参数整张图就能重新生成,一节课下来能把交换经济里的禀赋、无差异曲线、交易收益、均衡价格全套讲透。这篇文章把这套代码和背后的原理完整拆开,讲清楚怎么用 Python 画一张带无差异曲线、帕累托改进区域、契约曲线和竞争均衡点的埃奇沃思盒。对正在学微观经济学、想用代码做论文插图、或者备课时需要可视化工具的读者,都能直接用上。
1. 先搞清楚埃奇沃思盒的图形逻辑再动手写码
1.1 两人两商品的交换经济设定
埃奇沃思盒回答的问题其实很朴素:一个经济里只有两个人,比如 A 和 B;只有两种商品,比如 X 和 Y;两种商品的总量是固定的,A 拿得多一点,B 就拿得少一点。这种设定叫“两人两商品纯交换经济”,是一般均衡理论里最简单的模型,但几乎所有核心结论都能在这个盒子里显现出来。
假设经济中商品 X 的总量是 W,商品 Y 的总量是 H,A 初始持有(ω_Ax, ω_Ay),B 初始持有(ω_Bx, ω_By),那么一定有ω_Ax + ω_Bx = W、ω_Ay + ω_By = H。任意一种分配方案(x_A, y_A, x_B, y_B)只要满足
x_A + x_B = W y_A + y_B = H就是可行的。把所有可行分配画到一张图上,就是那个著名的矩形——埃奇沃思盒。
这个盒子的精妙之处在于,它把一个二维的分配问题,压缩成了平面上的一个点。一个点代表一种分配,点的位置变化代表财富的转移,这样一来,交换过程、效用变化、最优配置全都可以在几何直观里讨论。
1.2 “双坐标系”是盒子的灵魂
盒子的构造听起来很简单:左边和下面是 A 的坐标系,A 的原点O_A在左下角,向右代表商品 X 数量增加,向上代表商品 Y 数量增加;右边和上面是 B 的坐标系,B 的原点O_B在右上角,向左代表商品 X 数量增加,向下代表商品 Y 数量增加。
所以,图上任意一个点(x_A, y_A),对应的 B 的消费束就是(W - x_A, H - y_A)。A 往右上移动,A 拥有的商品变多;B 往左下移动,B 拥有的商品变多。两个消费者的利益天然是“对着干”的,这个对抗关系被盒子用旋转 180 度的方法直接画在了同一张图里。
第一次接触的人很容易在这个地方绕晕。我自己的经验是:写代码时一定要把“A 的坐标系”和“B 的视角”分开。A 坐标系是画布本身,B 只是在这个画布上反向读数据。如果不把这个关系想清楚,画出来的 B 无差异曲线就会完全错误。
1.3 无差异曲线怎么“翻”进盒子
A 在左下角拥有自己的无差异曲线族,B 是右上角拥有自己的无差异曲线族。以 Cobb-Douglas 效用函数为例,A 的效用是
U_A = x_A^α * y_A^(1-α)B 的效用是
U_B = x_B^β * y_B^(1-β)在这个盒子里,B 的无差异曲线必须用“B 实际拿到的东西”来计算,也就是x_B = W - x_A、y_B = H - y_A。这本质上是一种坐标翻转:取网格点(x_A, y_A),算出 B 拿到多少,再带入 B 的效用函数。那组等高线画出来的结果,就是 B 眼中凸向O_B的无差异曲线族。
A 的无差异曲线越靠近左下角效用越低,越靠近右上角效用越高;B 则完全相反。两条曲线在某点相切时,意味着两个消费者的边际替代率相等,这个切点轨迹就是契约曲线,也是后面所有均衡分析的几何基础。
2. 为什么选择 Python 实现,整体思路怎么落
2.1 手绘和通用绘图工具的瓶颈
最早我也试过用 PPT 和几何画板这类工具画埃奇沃思盒。PPT 的优点是好上手,但曲线全靠手工调,一个点不对整条曲线就得重来,而且无法做参数实验——换一个偏好参数,全部曲线都要重新画。几何画板可以拖动点,但动画交互和批量实验并不方便,尤其是契约曲线这种需要数值求解的轨迹,很难优雅地画出来。
手绘就更不现实了。无差异曲线一族画下来,白板基本就满了,而且每次换参数都要重新画一遍,课堂上时间根本不够。
选 Python 的原因很直接:第一,NumPy 可以把整个盒子离散成网格矩阵,效用函数在每个点上的值一次算完,等高线直接由 Matplotlib 提取;第二,scipy 里有现成的数值求解器,求契约曲线和均衡价格非常方便;第三,整套逻辑是参数驱动的,改一个数字,重新运行,图就自动更新,特别适合备课、演示和论文插图。Matplotlib 虽然是通用绘图库,但它的 contour 函数天生就是为“画等高线”准备的,用来画无差异曲线再合适不过。
2.2 Matplotlib + NumPy 的实现思路
整体思路可以分成四步。
第一步,建盒子。用W和H定义矩形范围,用np.linspace在 X 轴和 Y 轴上生成离散坐标,再用np.meshgrid展开成网格矩阵。Matplotlib 里的contour函数需要的是一个二维函数值矩阵,这个网格就是它的输入。
第二步,算效用矩阵。A 的效用直接用X和Y计算;B 的效用要用翻转后的W - X和H - Y计算。这里的翻转是整篇文章最关键的操作,我已经不只一次见过有人直接把X和Y带进 B 的效用函数,画出来的两条曲线族方向完全不对。
第三步,画等高线。用ax.contour画出 A 的无差异曲线(实线)和 B 的无差异曲线(虚线)。两条曲线族的颜色和线型必须区分,否则黑白打印时根本分不清。
第四步,叠加经济分析元素。包括禀赋点、帕累托改进区域、契约曲线、竞争均衡点、价格线。这些元素才是埃奇沃思盒作为分析工具的价值所在。
2.3 代码骨架与文件组织
实际操作时,我不建议把代码写成一个巨型脚本堆在一起。更合理的结构是分成三个文件,或者至少在同一个脚本里明显分块:参数区、效用函数区、绘图区。这样以后换效用函数、调参数、加动画,都不用大改。
# param.py 或脚本顶部区域:经济参数和偏好参数 W, H = 10.0, 10.0 omega_A = [2.0, 8.0] # utility.py:效用函数和边际替代率函数 def util_a(x, y): ... def util_b(x, y): ... # plot_box.py:绘图主逻辑这种组织方式的好处是,当你从 Cobb-Douglas 换到 CES 效用函数时,只需要改util_a和util_b两个函数,其他地方基本不动。
3. 核心代码拆解:静态埃奇沃思盒
3.1 参数设定与效用函数
代码的第一部分是把所有参数显式地写在最前面。我习惯给偏好参数起一个不容易混淆的名字,尤其要避开 Matplotlib 里的alpha透明度参数——这是个很实际的坑,你很容易写着写着就把偏好参数和透明度参数搞混。
import numpy as np import matplotlib.pyplot as plt from scipy.optimize import brentq, fsolve # 经济总量 W, H = 10.0, 10.0 # A 的初始禀赋,B 自动为剩余部分 omega_A = np.array([2.0, 8.0]) omega_B = np.array([W, H]) - omega_A # A、B 对商品 X 的偏好强度(0到1之间) pref_a = 0.4 pref_b = 0.6 def util_a(x, y): return x ** pref_a * y ** (1 - pref_a) def util_b(x, y): return x ** pref_b * y ** (1 - pref_b)这里选pref_a = 0.4、pref_b = 0.6不是随便定的。如果两个偏好参数都取 0.5,契约曲线会是一条简单的对角线y = x,图虽然规则,但不容易体现交换的“机会成本”变化。让两个消费者偏好不同,契约曲线才会弯起来,图形也更接近教材里的经典效果。
3.2 网格生成与“翻转”效用矩阵
有了参数之后,下一步是生成网格并计算两张效用矩阵。网格密度我一般取 400 到 600。密度太低曲线会有明显折线感,太高计算量变大但视觉提升有限。
N = 500 x = np.linspace(0, W, N) y = np.linspace(0, H, N) X, Y = np.meshgrid(x, y) # B 在 A 坐标系中对应的实际消费束 XB, YB = W - X, H - Y # 效用矩阵 UA = util_a(X, Y) UB = util_b(XB, YB)XB和YB的生成是这段代码的灵魂。它在 A 坐标系里遍历每一个点,然后“站在 B 的角度”看这个点:如果 A 在(x, y),B 实际消耗的就是距离右上角的剩余量。用这个翻转后的量去算效用,等高线才是 B 视角的。
你可以在心里对照一下:A 坐标系里靠近左上方的点,比如(2, 8),A 拿到 2 个 X 和 8 个 Y,B 拿到 8 个 X 和 2 个 Y,这个点离 B 的原点(10, 10)是比较远的,所以 B 的效用中等。而靠近(8, 2)的点,B 反而拿到 2 个 X 和 8 个 Y,离自己的原点更近,效用更高。这套映射是完全对称的。
3.3 画矩形、无差异曲线与坐标标注
图形硬件部分很直接:先画盒子边框,再画两族无差异曲线。这里要特别注意的是等高线的levels参数。很多人第一次画时习惯用levels=np.arange(1, 10, 1),结果原点附近会出现一大团密集曲线,整个图看起来像一团毛线。
原因是原点附近 X 和 Y 都很小,效用对投入非常敏感,小范围变化就会被等高线放大。我的经验是从 A 的初始禀赋效用水平附近开始取等高线,并且把层数控制在 6 条以内,图面才干净。
u_a0 = util_a(omega_A[0], omega_A[1]) u_b0 = util_b(omega_B[0], omega_B[1]) levels_a = np.linspace(2.0, 8.0, 6) levels_b = np.linspace(2.0, 8.0, 6) fig, ax = plt.subplots(figsize=(8, 8)) ax.set_aspect('equal') ax.set_xlim(0, W) ax.set_ylim(0, H) # 盒子边框 ax.plot([0, W, W, 0, 0], [0, 0, H, H, 0], 'k-', lw=1.5) # A 的无差异曲线,蓝色实线 cs_a = ax.contour(X, Y, UA, levels=levels_a, colors='steelblue', linewidths=1.2) # B 的无差异曲线,红色虚线 cs_b = ax.contour(X, Y, UB, levels=levels_b, colors='crimson', linewidths=1.2, linestyles='dashed')用颜色区分 A 和 B,用线型区分两族曲线,这是最基本的可读性设计。我还见过有人只用红蓝颜色不用线型,彩色屏幕上没问题,但打印成黑白文档后根本分不清。所以虚线加红色、实线加蓝色,双保险。
标注 A、B 的原点位置也很有必要。A 的原点在左下角,B 的原点在右上角,这两句话写不写,图的理解难度差距很大。
ax.text(-0.6, -0.8, 'O_A', fontsize=13, ha='center') ax.text(W + 0.3, H + 0.3, 'O_B', fontsize=13, ha='center') ax.set_xlabel('商品 X 数量(A →,B ←)') ax.set_ylabel('商品 Y 数量(A ↑,B ↓)')如果需要更严谨的 B 视角刻度,可以用 Matplotlib 的secondary_xaxis在顶部和右侧加反转刻度,效果是顶部从左往右标着 10、8、6、4、2、0,代表 B 实际拿到的 X 数量。这个操作会让图更专业,但不是每一张图都需要,我通常只在自己正式输出的讲义里加。
3.4 帕累托改进区域:一个掩膜就搞定
帕累托改进区域是埃奇沃思盒里最直观的“交易收益”表达:从初始禀赋出发,如果一个新的分配能让 A 和 B 的效用都不下降且至少一个人提高,这个新分配点就位于改进区域内。
传统画法是先画出过禀赋点的两条无差异曲线,再把它们围成的透镜形区域填充出来。这个“求两条曲线交点再填充”的思路听起来简单,但真要在 Matplotlib 里实现非常痛苦,因为等高线是以线段形式存储的,得先提取路径、求交点、判断闭合关系,代码量很大。
我后来找到了一个特别巧的办法:直接用布尔掩膜。既然 A 的效用高于初始效用水平的点是一个区域,B 的效用高于初始效用水平的点也是一个区域,两个条件同时满足就是帕累托改进区域,这不就是一个逻辑与操作吗?
region = (UA >= u_a0) & (UB >= u_b0) ax.contourf(X, Y, region.astype(float), levels=[0.5, 1.0], colors='gold', alpha=0.25)用contourf画布尔数组的时候,要把布尔值转成浮点数,并且levels必须设置成[0.5, 1.0]。这样 True 的区域会被填充,False 的区域不会被填充。填充出来的形状恰好就是那两条初始效用等高线围成的透镜形区域,完全不需要求交点。这个技巧在我第一次想到的时候,确实有“原来还能这么干”的感觉。
现在跑一下这一步的代码,你会看到盒子的左下到右上之间出现了一块金色的透镜区域。那块区域里任意一点都同时好于初始禀赋,这就是 A 和 B 愿意坐下来谈判的全部空间。
4. 进阶:契约曲线与竞争均衡可视化
4.1 契约曲线:切点轨迹的数值求解
无差异曲线画好了,下一步是找 A 和 B 的无差异曲线相切的点。经济学上,交换的最优分配满足两个消费者的边际替代率相等,即
MRS_A = MRS_B对于 Cobb-Douglas 效用函数,边际替代率有解析形式
MRS_A = (pref_a / (1 - pref_a)) * (y / x) MRS_B = (pref_b / (1 - pref_b)) * ((H - y) / (W - x))如果偏好参数简单,比如pref_a = pref_b = 0.5,可以直接解出y = x。但参数一变,解析解就不好看了,更通用的做法是数值求解。对盒子里每一个 X 坐标,用scipy.optimize.brentq找满足等式的 Y 坐标,把所有点连起来就是契约曲线。
def mrs_a(x, y): return (pref_a / (1 - pref_a)) * y / x def mrs_b(x, y): return (pref_b / (1 - pref_b)) * (H - y) / (W - x) xs_cc = np.linspace(0.01, W - 0.01, 200) ys_cc = [] for xv in xs_cc: f = lambda y: mrs_a(xv, y) - mrs_b(xv, y) yv = brentq(f, 0.01, H - 0.01) ys_cc.append(yv) ax.plot(xs_cc, ys_cc, 'green', lw=2.0, label='契约曲线')这里用brentq而不是fsolve,是因为brentq是区间根求解,只要区间端点异号就一定能找到根,不需要像牛顿法那样猜初值。仔细观察函数f在区间两端的行为:在y接近 0 时,MRS_A趋近 0,MRS_B是个正数,所以f < 0;在y接近 H 时,MRS_A很大,MRS_B趋近 0,所以f > 0。有根,而且就在区间内部,brentq特别适合这种场景。
契约曲线的两个端点理论上是(0, 0)和(W, H),也就是所有商品集中到其中一个人手里,但这两个点在数值上会出现分母为 0 的问题,所以用0.01避开了端点。画出图后肉眼看不出差别。
4.2 竞争均衡:价格怎么算出来
有了契约曲线,再往前走一步就是竞争均衡。竞争均衡的意思是:给定一组价格,A 在自己预算约束下最大化效用,B 也在自己预算约束下最大化效用,同时两个市场同时出清。
在这个模型里,商品 Y 的价格可以归一化为 1,商品 X 的相对价格记为p。对于 Cobb-Douglas 效用函数,消费者花在 X 上的预算比例正好等于偏好参数。所以 A 的需求为
x_A = pref_a * M_A / p y_A = (1 - pref_a) * M_A其中M_A = omega_Ax * p + omega_Ay是 A 的总财富。B 的需求类似。
由于两种商品中只要一个市场出清,另一个市场会由瓦尔拉斯法则自动出清,我们只需要解 X 市场:
def excess_demand(p): M_A = omega_A[0] * p + omega_A[1] M_B = omega_B[0] * p + omega_B[1] x_A = pref_a * M_A / p x_B = pref_b * M_B / p return x_A + x_B - W p_star = fsolve(excess_demand, 1.0)[0]用fsolve是因为这里只有一个未知数,而且函数形式相对平滑,给一个合理初值p=1.0就能收敛。解出来的均衡价格在当前的参数设置下是p = 1.0,这个数值正好对应两个消费者偏好参数互补的情形。均衡分配是 A 拿(4, 6),B 拿(6, 4)。
你可以在这个结果上多看一眼:A 的禀赋是(2, 8),均衡持有是(4, 6),说明 A 用一部分 Y 换来了更多的 X;B 的禀赋是(8, 2),均衡持有是(6, 4),说明 B 用一部分 X 换来了更多的 Y。这是一个“各取所需”的直观例子。
4.3 把价格线和均衡点画进盒子
竞争均衡在图上由一个点表示,还要画一条经过禀赋点、斜率为-p的预算线。因为纵轴是 Y、横轴是 X,预算约束是
y = M_A - p * x所以价格线就是经过 A 的禀赋点、斜率为-p的直线。
x_line = np.array([0.0, W]) y_line = omega_A[1] - p_star * (x_line - omega_A[0]) ax.plot(x_line, y_line, 'gray', ls=':', lw=1.5, label='预算线') x_eq = pref_a * (omega_A[0] * p_star + omega_A[1]) / p_star y_eq = (1 - pref_a) * (omega_A[0] * p_star + omega_A[1]) ax.plot(omega_A[0], omega_A[1], 'ko', ms=8, label='初始禀赋') ax.plot(x_eq, y_eq, 'r*', ms=18, label='竞争均衡') ax.legend(loc='upper left')价格线两端可能会延伸出盒子,这是正常的,预算线本来就不需要完全待在盒子里。为了让画面干净,也可以用clip_on=True或直接把x_line的取值限制在(0, W)范围内,我这里直接画到矩形边界。
均衡点(4, 6)同时落在契约曲线上,也是两块无差异曲线族相切的位置,同时满足“消费者个体最优+市场出清”。三件事在一张图上全部看到,一般均衡的结论就变得非常直观。
4.4 进一步扩展的方向
画完基础版本之后,这套代码的扩展空间非常大。
最直接的扩展是换效用函数。比如 CES 效用函数U = (a * x^ρ + (1-a) * y^ρ)^(1/ρ),只要改util_a和util_b两个函数,再把网格计算稍作调整即可。契约曲线部分由于mrs_a和mrs_b也需要跟着换,但数值求解框架完全不用动。
第二个扩展是做交互。用matplotlib.widgets.Slider加两个滑杆,分别控制pref_a和pref_b,拖动滑杆时重新计算效用矩阵并更新等高线。这个交互式埃奇沃思盒特别适合课堂上演示“偏好变化如何影响均衡位置”,学生拖动滑杆,图实时变化,理解马上就不一样。
第三个扩展是动画。用matplotlib.animation.FuncAnimation让禀赋点沿某条路径移动,观察帕累托改进区域的变化。动画更适合做教学视频或者课堂放映,静态图讲基本概念,动态图讲过程。
还有一个很实用的小工具:用plt.ginput在图上点任意一点,程序返回 A 和 B 各自拿到的商品束、效用以及是否落在帕累托改进区域内。我在课堂上用过一次,学生点几下就能理解“分配”在盒子里的含义,比单纯看图要深刻得多。
5. 常见问题、坑位排查与调参心得
5.1 最容易被绕晕的“B视角”坐标问题
写代码时最常见的错误,就是画 B 的无差异曲线时直接把X和Y带入 B 的效用函数。比如:
UB = util_b(X, Y) # 错!画出来的是 A 视角的曲线这个错误画出来的结果非常迷惑:两条曲线族重叠在一起且方向一致,看起来像只有一族曲线。只要你想起来 B 在盒子里是“倒着看”的,要先把坐标翻转成W - X和H - Y,问题就迎刃而解。
我建议把翻转操作写成独立一行,并加上注释,方便自己之后回看,也方便别人读懂代码:
# 先算 B 在 A 坐标系中对应的消费束,再算效用 XB, YB = W - X, H - Y UB = util_b(XB, YB)5.2 等高线不光滑与曲线密集问题
无差异曲线出现明显折线感,基本是网格分辨率太低。把 N 从 100 提到 500,折线感基本消失。如果还不行,检查一下contour的参数是否有linewidths过小的问题,放大导出时曲线太细也会显得不平整。
曲线太密集通常是levels设置的问题。等高线层数过多或者从很小的效用值开始画,会让原点附近出现一圈一圈的密集纹理。我常用的做法是:
levels = np.linspace(2.0, 8.0, 6)而不是用np.arange(1, 10, 1)。前者控制范围和层数,后者在原点附近会画出很多无意义的低效用线。也可以用u_a0作为levels的起点,这样图形的第一圈曲线正好是“经过禀赋点的无差异曲线”,对接帕累托分析会很自然。
5.3 一点关于交互式展示的建议
如果只是个人看,静态图足够了;但如果要做课堂演示,我还是建议加一个交互方式。最简单的方案是matplotlib.widgets.Slider,把pref_a和pref_b挂到两个滑杆上。需要提醒的是,滑杆回调函数里不要每次都重新创建figure,而是更新已有等高线对象的坐标数据。
具体做法是:创建contour对象时不要把levels固定死,因为效用函数变化后,等高线对应的效用水平也应该跟着变。这时需要先清除旧等高线,再画新等高线。这一步如果处理不好,拖动滑杆时旧曲线和新曲线会叠在一起,画面一团糟。
5.4 问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| B 的无差异曲线和 A 的方向一致 | 计算 UB 时没有翻转坐标 | 用W - X、H - Y作为 B 的实际消费束 |
| 原点附近曲线挤成一团 | levels包含了过小的效用值 | 从初始效用值附近开始选等高线 |
| 帕累托区域填充不出来 | contourf处理布尔数组方式不对 | 转成float,设置levels=[0.5, 1.0] |
| 契约曲线在端点报错 | 端点分母为 0 | 用0.01或更小的值避开端点 |
| 盒子被拉成扁矩形 | 没有等比例坐标 | 加ax.set_aspect('equal') |
| 导出 PDF 后曲线有锯齿 | 默认栅格化精度低 | 提高 dpi,或开启抗锯齿 |
这些坑都是我实际调试中踩过的,每一条都对应过真实的报错或丑陋的图片。尤其是帕累托区域那一条,当时我一度以为 Matplotlib 画不了这种不规则填充区域,后来发现只是布尔数组的处理方式不对,代码量直接少了几十行。
最后再分享一个实际体验
整套代码写完之后,我已经不满足于静态图了。现在备课时最常用的是交互版:把偏好参数做成滑杆,开讲之前先把图调到一个比较典型的位置,讲“禀赋和帕累托改进”时展示透镜区域,讲“一般均衡”时把价格线放出来。学生能直观看到竞争均衡点落在契约曲线上,比对着教材念定义有效率得多。
如果让我给刚接触埃奇沃思盒的同学一个建议:不要一上来就追求画得很复杂,先把矩形、两组无差异曲线、禀赋点这三样画出来,理解 B 的坐标翻转是怎么回事,再一步步叠加帕累托区域和契约曲线。代码可以慢慢扩展,但核心的那个坐标翻转逻辑,一定要在画第一张图时就彻底想清楚。这套脚本你拿去改一改,把效用函数换成你们教材里的形式,就能直接用在你的作业或者论文里了。