
当你说”无差异曲线看不懂”时,可以用Python做点什么

一、痛点
“无差异曲线为什么是凸向原点的?”
“预算线旋转的时候,均衡点到底是怎么移动的?”
这可能是我们每次看到微观经济学消费者选择时可能会发出的”灵魂拷问”。消费者选择理论又是承上启下的核心章节——它连接着效用论与需求曲线,是理解市场价格形成机制的微观基础。
但传统的静态PPT讲解,总是让自己在”切点”“均衡”“价格消费曲线”这些概念之间迷失方向。直到我决定:不如让曲线自己动起来。

二、解决方案
用Python的matplotlib.animation模块,制作三个教学动画,分别对应消费者选择理论的三个关键认知节点:
🔷 动画一:均衡的形成——从”看不见”到”切得上”
痛点: “怎么知道切点在哪里?”
动画设计: - 分5步动态演示:画预算线 → 低效用无差异曲线 → 逼近最优 → 达到均衡 → 显示切线
🔷 动画二:价格变化——看懂”价格消费曲线”的生成逻辑
痛点: PCC曲线在教材上只是一条静态连线,学生不理解”点怎么来的”
动画设计: - 固定收入M,P₂,让P₁下降
🔷 动画三:收入变化——恩格尔曲线的前置可视化
教学痛点: 收入消费曲线(ICC)与后续恩格尔曲线关联度不高
动画设计: - 固定价格,收入逐渐增加

三、技术实现
技术栈: Python + NumPy + Matplotlib(FuncAnimation)
经济学模型: 柯布-道格拉斯效用函数 U = X₁^α · X₂^(1-α)
核心代码逻辑(节选):
"""微观经济学消费者选择理论 - 动态可视化功能:1. 无差异曲线和预算约束线相切的动态过程2. 商品1价格P1变化导致的均衡点动态变化(价格消费曲线)3. 收入变化导致的均衡点动态变化(收入消费曲线)"""import numpy as npimport matplotlib.pyplot as pltfrom matplotlib.animation import FuncAnimation# ==================== 设置中文字体 ====================plt.rcParams['font.size'] =12# 如果需要显示中文,取消下面注释并设置合适的中文字体# plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']# plt.rcParams['axes.unicode_minus'] = False# ==================== 核心函数定义 ====================def utility(x1, x2, alpha=0.5):"""柯布-道格拉斯效用函数:U = X1^α * X2^(1-α)默认α=0.5,即U = X1^0.5 * X2^0.5"""return (x1 ** alpha) * (x2 ** (1- alpha))def indifference_curve(x1, u, alpha=0.5):"""无差异曲线函数:给定效用水平u和x1,计算x2U = X1^α * X2^(1-α) => X2 = (U / X1^α)^(1/(1-α))"""x2 = np.zeros_like(x1)for i, val inenumerate(x1):if u <=0or val <=0:x2[i] = np.nanelse:x2[i] = (u / (val ** alpha)) ** (1/ (1- alpha))return x2def budget_line(x1, m, p1, p2):"""预算约束线:m = p1*x1 + p2*x2 => x2 = (m - p1*x1) / p2m: 收入p1: 商品1价格p2: 商品2价格"""return (m - p1 * x1) / p2def calculate_equilibrium(m, p1, p2, alpha=0.5):"""计算消费者均衡点(最优选择)对于柯布-道格拉斯效用函数,最优解为:X1* = α*M/P1X2* = (1-α)*M/P2"""x1_star = (alpha * m) / p1x2_star = ((1- alpha) * m) / p2u_star = utility(x1_star, x2_star, alpha)return x1_star, x2_star, u_star# ==================== 动画1:均衡形成过程 ====================def create_animation1(output_path='animation1_equilibrium_formation.gif'):"""动画1:无差异曲线和预算约束线相切的动态过程展示均衡点从无到有的形成过程"""fig, ax = plt.subplots(figsize=(10, 8), dpi=120)# 参数设置m =100# 收入p1 =2# 商品1价格p2 =5# 商品2价格alpha =0.5# 效用函数参数# 计算均衡点x1_star, x2_star, u_star = calculate_equilibrium(m, p1, p2, alpha)# 生成x1的取值范围x1 = np.linspace(0.1, 50, 500)# 初始化线条line_budget, = ax.plot([], [], 'b-', linewidth=3, label='Budget Constraint', alpha=0.8)line_indiff, = ax.plot([], [], 'r-', linewidth=2.5, label='Indifference Curve', alpha=0.8)point_eq, = ax.plot([], [], 'go', markersize=15, markeredgecolor='darkgreen', markeredgewidth=2, label='Equilibrium Point')tangent_line, = ax.plot([], [], 'g--', linewidth=2, alpha=0.6, label='Tangent Line')# 添加注释文本text_info = ax.text(0.02, 0.98, '', transform=ax.transAxes, fontsize=11, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))# 设置坐标轴ax.set_xlim(0, 55)ax.set_ylim(0, 25)ax.set_xlabel('Good 1 (X₁)', fontsize=14, fontweight='bold')ax.set_ylabel('Good 2 (X₂)', fontsize=14, fontweight='bold')ax.set_title('Consumer Equilibrium Formation\n(Indifference Curve & Budget Line Tangency)', fontsize=16, fontweight='bold', pad=20)ax.grid(True, alpha=0.3, linestyle='--')ax.legend(loc='upper right', fontsize=11, framealpha=0.9)n_frames =100def init():line_budget.set_data([], [])line_indiff.set_data([], [])point_eq.set_data([], [])tangent_line.set_data([], [])text_info.set_text('')return line_budget, line_indiff, point_eq, tangent_line, text_infodef animate(frame):if frame <20:# 步骤1:画出预算约束线x1_budget = np.linspace(0, m/p1 * (frame/20), 100)x2_budget = budget_line(x1_budget, m, p1, p2)line_budget.set_data(x1_budget, x2_budget)text_info.set_text(f'Step 1: Drawing Budget Constraint\nIncome M={m}, P₁={p1}, P₂={p2}')elif frame <40:# 步骤2:低效用无差异曲线x1_budget = np.linspace(0, m/p1, 200)x2_budget = budget_line(x1_budget, m, p1, p2)line_budget.set_data(x1_budget, x2_budget)u_current = u_star *0.3+ (u_star *0.4) * ((frame-20)/20)x2_indiff = indifference_curve(x1, u_current, alpha)x2_indiff = np.clip(x2_indiff, 0.1, 100)line_indiff.set_data(x1, x2_indiff)text_info.set_text(f'Step 2: Lower Indifference Curve (U={u_current:.1f})\nNot optimal - can reach higher utility')elif frame <60:# 步骤3:接近最优效用x1_budget = np.linspace(0, m/p1, 200)x2_budget = budget_line(x1_budget, m, p1, p2)line_budget.set_data(x1_budget, x2_budget)u_current = u_star *0.7+ (u_star *0.25) * ((frame-40)/20)x2_indiff = indifference_curve(x1, u_current, alpha)x2_indiff = np.clip(x2_indiff, 0.1, 100)line_indiff.set_data(x1, x2_indiff)text_info.set_text(f'Step 3: Approaching Optimal Utility\nCurrent U={u_current:.1f}, Target U={u_star:.1f}')elif frame <80:# 步骤4:达到均衡效用x1_budget = np.linspace(0, m/p1, 200)x2_budget = budget_line(x1_budget, m, p1, p2)line_budget.set_data(x1_budget, x2_budget)x2_indiff = indifference_curve(x1, u_star, alpha)x2_indiff = np.clip(x2_indiff, 0.1, 100)line_indiff.set_data(x1, x2_indiff)point_alpha = (frame -60) /20point_eq.set_data([x1_star], [x2_star])point_eq.set_alpha(point_alpha)text_info.set_text(f'Step 4: Optimal Indifference Curve U={u_star:.1f}\nEquilibrium Point E: X₁*={x1_star:.1f}, X₂*={x2_star:.1f}')else:# 步骤5:显示切线x1_budget = np.linspace(0, m/p1, 200)x2_budget = budget_line(x1_budget, m, p1, p2)line_budget.set_data(x1_budget, x2_budget)x2_indiff = indifference_curve(x1, u_star, alpha)x2_indiff = np.clip(x2_indiff, 0.1, 100)line_indiff.set_data(x1, x2_indiff)point_eq.set_data([x1_star], [x2_star])point_eq.set_alpha(1.0)# 切线tangent_x = np.linspace(x1_star-8, x1_star+8, 100)mrs = p1/p2tangent_y = x2_star + mrs * (x1_star - tangent_x)tangent_line.set_data(tangent_x, tangent_y)tangent_line.set_alpha((frame-80)/20)text_info.set_text(f'Equilibrium Achieved!\nX₁*={x1_star:.1f}, X₂*={x2_star:.1f}\nMRS = P₁/P₂ = {p1}/{p2} = {mrs:.2f}')return line_budget, line_indiff, point_eq, tangent_line, text_infoanim = FuncAnimation(fig, animate, init_func=init, frames=n_frames, interval=100, blit=True, repeat=True)anim.save(output_path, writer='pillow', fps=10, dpi=120)plt.close()print(f"✅ 动画1已保存至: {output_path}")# ==================== 动画2:价格变化 ====================def create_animation2(output_path='animation2_price_change.gif'):"""动画2:商品1价格P1变化导致的均衡点动态变化展示价格消费曲线(Price Consumption Curve)的形成"""fig, ax = plt.subplots(figsize=(11, 8), dpi=120)m =100# 收入保持不变p2 =5# 商品2价格保持不变alpha =0.5p1_start =5# 初始高价p1_end =1# 最终低价n_frames =100x1 = np.linspace(0.1, 110, 500)# 初始化线条line_budget_current, = ax.plot([], [], 'b-', linewidth=3, label='Current Budget Line', alpha=0.9)line_budget_previous, = ax.plot([], [], 'b--', linewidth=2, label='Previous Budget Lines', alpha=0.4)line_indiff_current, = ax.plot([], [], 'r-', linewidth=2.5, label='Current Indifference Curve', alpha=0.9)line_indiff_previous, = ax.plot([], [], 'r--', linewidth=1.5, alpha=0.3)points_eq, = ax.plot([], [], 'go', markersize=8, alpha=0.6)path_eq, = ax.plot([], [], 'g-', linewidth=2, marker='o', markersize=4, label='Price Consumption Curve', alpha=0.8)point_current_eq, = ax.plot([], [], 'ro', markersize=15, markeredgecolor='darkred', markeredgewidth=2, label='Current Equilibrium', zorder=5)text_info = ax.text(0.02, 0.98, '', transform=ax.transAxes, fontsize=11, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9))ax.set_xlim(0, 110)ax.set_ylim(0, 25)ax.set_xlabel('Good 1 (X₁)', fontsize=14, fontweight='bold')ax.set_ylabel('Good 2 (X₂)', fontsize=14, fontweight='bold')ax.set_title('Effect of Price Change on Consumer Equilibrium\n(Price Consumption Curve Formation)', fontsize=16, fontweight='bold', pad=20)ax.grid(True, alpha=0.3, linestyle='--')ax.legend(loc='upper right', fontsize=10, framealpha=0.9)history_x1, history_x2, history_p1 = [], [], []def init():line_budget_current.set_data([], [])line_budget_previous.set_data([], [])line_indiff_current.set_data([], [])line_indiff_previous.set_data([], [])points_eq.set_data([], [])path_eq.set_data([], [])point_current_eq.set_data([], [])text_info.set_text('')history_x1.clear()history_x2.clear()history_p1.clear()return (line_budget_current, line_budget_previous, line_indiff_current, line_indiff_previous, points_eq, path_eq, point_current_eq, text_info)def animate(frame):t = frame / n_framesp1_current = p1_start - (p1_start - p1_end) * tx1_star, x2_star, u_star = calculate_equilibrium(m, p1_current, p2, alpha)history_x1.append(x1_star)history_x2.append(x2_star)history_p1.append(p1_current)# 当前预算线x1_budget = np.linspace(0, m/p1_current, 200)x2_budget = budget_line(x1_budget, m, p1_current, p2)line_budget_current.set_data(x1_budget, x2_budget)# 当前无差异曲线x2_indiff = indifference_curve(x1, u_star, alpha)x2_indiff = np.clip(x2_indiff, 0.1, 100)line_indiff_current.set_data(x1, x2_indiff)# 当前均衡点point_current_eq.set_data([x1_star], [x2_star])# 均衡点轨迹iflen(history_x1) >1:path_eq.set_data(history_x1, history_x2)points_eq.set_data(history_x1[:-1], history_x2[:-1])# 显示之前的预算线和无差异曲线if frame >0andlen(history_x1) >1:prev_p1 = history_p1[-2]prev_x1 = (alpha * m) / prev_p1prev_u = utility(prev_x1, x2_star, alpha)x1_budget_prev = np.linspace(0, m/prev_p1, 200)x2_budget_prev = budget_line(x1_budget_prev, m, prev_p1, p2)line_budget_previous.set_data(x1_budget_prev, x2_budget_prev)x2_indiff_prev = indifference_curve(x1, prev_u, alpha)x2_indiff_prev = np.clip(x2_indiff_prev, 0.1, 100)line_indiff_previous.set_data(x1, x2_indiff_prev)text_info.set_text(f'Price Change: P₁ decreases from {p1_start} to {p1_end}\n'f'Current P₁ = {p1_current:.2f}\n'f'Equilibrium: X₁* = {x1_star:.1f}, X₂* = {x2_star:.1f}\n'f'Utility U = {u_star:.2f}\n'f'Income M = {m} (constant), P₂ = {p2} (constant)')return (line_budget_current, line_budget_previous, line_indiff_current, line_indiff_previous, points_eq, path_eq, point_current_eq, text_info)anim = FuncAnimation(fig, animate, init_func=init, frames=n_frames, interval=100, blit=True, repeat=True)anim.save(output_path, writer='pillow', fps=10, dpi=120)plt.close()print(f"✅ 动画2已保存至: {output_path}")# ==================== 动画3:收入变化 ====================def create_animation3(output_path='animation3_income_change.gif'):"""动画3:收入变化导致的均衡点动态变化展示收入消费曲线(Income Consumption Curve)的形成"""fig, ax = plt.subplots(figsize=(11, 8), dpi=120)p1 =2# 商品1价格保持不变p2 =5# 商品2价格保持不变alpha =0.5m_start =50# 初始收入m_end =200# 最终收入n_frames =100x1 = np.linspace(0.1, 110, 500)# 初始化线条line_budget_current, = ax.plot([], [], 'b-', linewidth=3, label='Current Budget Line', alpha=0.9)line_budget_previous, = ax.plot([], [], 'b--', linewidth=2, label='Previous Budget Lines', alpha=0.4)line_indiff_current, = ax.plot([], [], 'r-', linewidth=2.5, label='Current Indifference Curve', alpha=0.9)line_indiff_previous, = ax.plot([], [], 'r--', linewidth=1.5, alpha=0.3)points_eq, = ax.plot([], [], 'go', markersize=8, alpha=0.6)path_eq, = ax.plot([], [], 'g-', linewidth=2, marker='o', markersize=4, label='Income Consumption Curve', alpha=0.8)point_current_eq, = ax.plot([], [], 'ro', markersize=15, markeredgecolor='darkred', markeredgewidth=2, label='Current Equilibrium', zorder=5)text_info = ax.text(0.02, 0.98, '', transform=ax.transAxes, fontsize=11, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='lightcyan', alpha=0.9))ax.set_xlim(0, 110)ax.set_ylim(0, 50)ax.set_xlabel('Good 1 (X₁)', fontsize=14, fontweight='bold')ax.set_ylabel('Good 2 (X₂)', fontsize=14, fontweight='bold')ax.set_title('Effect of Income Change on Consumer Equilibrium\n(Income Consumption Curve Formation)', fontsize=16, fontweight='bold', pad=20)ax.grid(True, alpha=0.3, linestyle='--')ax.legend(loc='upper left', fontsize=10, framealpha=0.9)history_x1, history_x2, history_m = [], [], []def init():line_budget_current.set_data([], [])line_budget_previous.set_data([], [])line_indiff_current.set_data([], [])line_indiff_previous.set_data([], [])points_eq.set_data([], [])path_eq.set_data([], [])point_current_eq.set_data([], [])text_info.set_text('')history_x1.clear()history_x2.clear()history_m.clear()return (line_budget_current, line_budget_previous, line_indiff_current, line_indiff_previous, points_eq, path_eq, point_current_eq, text_info)def animate(frame):t = frame / n_framesm_current = m_start + (m_end - m_start) * tx1_star, x2_star, u_star = calculate_equilibrium(m_current, p1, p2, alpha)history_x1.append(x1_star)history_x2.append(x2_star)history_m.append(m_current)# 当前预算线x1_budget = np.linspace(0, m_current/p1, 200)x2_budget = budget_line(x1_budget, m_current, p1, p2)line_budget_current.set_data(x1_budget, x2_budget)# 当前无差异曲线x2_indiff = indifference_curve(x1, u_star, alpha)x2_indiff = np.clip(x2_indiff, 0.1, 100)line_indiff_current.set_data(x1, x2_indiff)# 当前均衡点point_current_eq.set_data([x1_star], [x2_star])# 均衡点轨迹iflen(history_x1) >1:path_eq.set_data(history_x1, history_x2)points_eq.set_data(history_x1[:-1], history_x2[:-1])# 显示之前的预算线和无差异曲线if frame >0andlen(history_x1) >1:prev_m = history_m[-2]prev_x1 = (alpha * prev_m) / p1prev_u = utility(prev_x1, ((1-alpha)*prev_m)/p2, alpha)x1_budget_prev = np.linspace(0, prev_m/p1, 200)x2_budget_prev = budget_line(x1_budget_prev, prev_m, p1, p2)line_budget_previous.set_data(x1_budget_prev, x2_budget_prev)x2_indiff_prev = indifference_curve(x1, prev_u, alpha)x2_indiff_prev = np.clip(x2_indiff_prev, 0.1, 100)line_indiff_previous.set_data(x1, x2_indiff_prev)text_info.set_text(f'Income Change: M increases from {m_start} to {m_end}\n'f'Current M = {m_current:.1f}\n'f'Equilibrium: X₁* = {x1_star:.1f}, X₂* = {x2_star:.1f}\n'f'Utility U = {u_star:.2f}\n'f'P₁ = {p1} (constant), P₂ = {p2} (constant)\n'f'ICC Slope: ΔX₂/ΔX₁ = {(1-alpha)/alpha * p1/p2:.3f}')return (line_budget_current, line_budget_previous, line_indiff_current, line_indiff_previous, points_eq, path_eq, point_current_eq, text_info)anim = FuncAnimation(fig, animate, init_func=init, frames=n_frames, interval=100, blit=True, repeat=True)anim.save(output_path, writer='pillow', fps=10, dpi=120)plt.close()print(f"✅ 动画3已保存至: {output_path}")# ==================== 主程序 ====================if__name__=="__main__":print("开始生成微观经济学消费者选择理论动画...")print("="*60)# 生成三个动画create_animation1('animation1_equilibrium_formation.gif')create_animation2('animation2_price_change.gif')create_animation3('animation3_income_change.gif')print("="*60)print("所有动画生成完成!")print("\n文件列表:")print("1. animation1_equilibrium_formation.gif - 均衡形成过程")print("2. animation2_price_change.gif - 价格变化效应")print("3. animation3_income_change.gif - 收入变化效应")