
最近要在一门微观经济学课上给学生演示一般均衡理论最直接的工具就是埃奇沃思盒Edgeworth Box。以前我在白板上手画画一条无差异曲线都要小心翼翼画出一整族更是费劲更别说还要表达帕累托改进、契约曲线这些动态概念。后来换成了 Python 脚本改一个参数整张图就能重新生成一节课下来能把交换经济里的禀赋、无差异曲线、交易收益、均衡价格全套讲透。这篇文章把这套代码和背后的原理完整拆开讲清楚怎么用 Python 画一张带无差异曲线、帕累托改进区域、契约曲线和竞争均衡点的埃奇沃思盒。对正在学微观经济学、想用代码做论文插图、或者备课时需要可视化工具的读者都能直接用上。1. 先搞清楚埃奇沃思盒的图形逻辑再动手写码1.1 两人两商品的交换经济设定埃奇沃思盒回答的问题其实很朴素一个经济里只有两个人比如 A 和 B只有两种商品比如 X 和 Y两种商品的总量是固定的A 拿得多一点B 就拿得少一点。这种设定叫“两人两商品纯交换经济”是一般均衡理论里最简单的模型但几乎所有核心结论都能在这个盒子里显现出来。假设经济中商品 X 的总量是 W商品 Y 的总量是 HA 初始持有(ω_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 个 YB 拿到 8 个 X 和 2 个 Y这个点离 B 的原点(10, 10)是比较远的所以 B 的效用中等。而靠近(8, 2)的点B 反而拿到 2 个 X 和 8 个 Y离自己的原点更近效用更高。这套映射是完全对称的。3.3 画矩形、无差异曲线与坐标标注图形硬件部分很直接先画盒子边框再画两族无差异曲线。这里要特别注意的是等高线的levels参数。很多人第一次画时习惯用levelsnp.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-, lw1.5) # A 的无差异曲线蓝色实线 cs_a ax.contour(X, Y, UA, levelslevels_a, colorssteelblue, linewidths1.2) # B 的无差异曲线红色虚线 cs_b ax.contour(X, Y, UB, levelslevels_b, colorscrimson, linewidths1.2, linestylesdashed)用颜色区分 A 和 B用线型区分两族曲线这是最基本的可读性设计。我还见过有人只用红蓝颜色不用线型彩色屏幕上没问题但打印成黑白文档后根本分不清。所以虚线加红色、实线加蓝色双保险。标注 A、B 的原点位置也很有必要。A 的原点在左下角B 的原点在右上角这两句话写不写图的理解难度差距很大。ax.text(-0.6, -0.8, O_A, fontsize13, hacenter) ax.text(W 0.3, H 0.3, O_B, fontsize13, hacenter) 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], colorsgold, alpha0.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, lw2.0, label契约曲线)这里用brentq而不是fsolve是因为brentq是区间根求解只要区间端点异号就一定能找到根不需要像牛顿法那样猜初值。仔细观察函数f在区间两端的行为在y接近 0 时MRS_A趋近 0MRS_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是因为这里只有一个未知数而且函数形式相对平滑给一个合理初值p1.0就能收敛。解出来的均衡价格在当前的参数设置下是p 1.0这个数值正好对应两个消费者偏好参数互补的情形。均衡分配是 A 拿(4, 6)B 拿(6, 4)。你可以在这个结果上多看一眼A 的禀赋是(2, 8)均衡持有是(4, 6)说明 A 用一部分 Y 换来了更多的 XB 的禀赋是(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:, lw1.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, ms8, label初始禀赋) ax.plot(x_eq, y_eq, r*, ms18, label竞争均衡) ax.legend(locupper left)价格线两端可能会延伸出盒子这是正常的预算线本来就不需要完全待在盒子里。为了让画面干净也可以用clip_onTrue或直接把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 的坐标翻转是怎么回事再一步步叠加帕累托区域和契约曲线。代码可以慢慢扩展但核心的那个坐标翻转逻辑一定要在画第一张图时就彻底想清楚。这套脚本你拿去改一改把效用函数换成你们教材里的形式就能直接用在你的作业或者论文里了。