简介:这是一份面向经济学、统计学及数量经济方向学习者与教研人员的课件资料,围绕投入产出系数与投入产出模型展开,适合课堂教学、考前复习与实证建模入门参考。压缩包内共1个ppt文件,整体约2.66MB,以幻灯片形式呈现,涵盖概念定义、公式推导、矩阵表示与实例演算,便于逐页讲解与自学。内容从投入产出表的基本表式讲起,梳理中间使用、最终使用、初始投入、固定资产折旧、劳动报酬与纯收入等分配去向,重点说明直接消耗系数的定义、计算公式与性质,并给出直接消耗系数矩阵A的排列方式;随后推导完全消耗系数,说明完全消耗等于直接消耗与各次间接消耗之和,展示完全消耗系数矩阵B及矩阵运算求解过程。资料还配有假想的某国4部门价值型投入产出表,以及1997年、2000年中国价值型投入产出表和1992年实物型投入产出表的部分消耗系数比较,可帮助读者把公式与真实数据对应起来,理解经济系统运行机制、结构关联与投资决策分析的基本思路。目前已有583人学习下载。
1. 投入产出系数和投入产出模型.ppt:课件里真正能跑起来的那部分
很多人拿到一份叫《投入产出系数和投入产出模型.ppt》的课件,习惯当讲义翻:翻到直接消耗系数那一页抄个公式,翻到列昂惕夫逆矩阵那一页抄个 (I-A)⁻¹,然后关掉。真正耗时间的从来不是公式,而是把年鉴里那张几十行几十列的投入产出表塞进这套公式的过程——部门口径对不齐、行合计和列合计差了几百亿、矩阵条件数大到求逆结果不可信、系数隔几年就过期。这篇按实际动手顺序写:怎么从课件里把表格抽成结构化数据,怎么算直接消耗系数和完全消耗系数,怎么用 RAS 把旧系数外推到新年度,最后用 SDA 把增长拆成技术变动和需求变动两块。适合自己做产业关联测算、成本传导估算、区域经济影响评估的工程和数据分析同学。
2. 从投入产出表到直接消耗系数:把 .ppt 里的表格变成可算的矩阵
2.1 投入产出表的四个象限与数据落位
投入产出表看起来是一张大表,本质是四块。第一象限是部门×部门的中间使用流量,记作 Z,元素 z_ij 表示 j 部门生产过程中消耗 i 部门产品的数量。第二象限是部门×最终使用,记作 Y,包含消费、资本形成、出口这些列。第三象限是增加值×部门,记作 V,劳动者报酬、生产税净额、营业盈余都在这里。第四象限是增加值与最终使用的交叉,一般不做分析。
| 区块 | 行列范围 | 内容 | 变量 | 是否参与建模 |
|---|---|---|---|---|
| 第一象限 | 部门×部门 | 中间使用流量 | Z (n×n) | 是,核心 |
| 第二象限 | 部门×最终使用 | 消费/资本形成/出口 | Y (n×k) | 是,外生需求 |
| 第三象限 | 增加值×部门 | 报酬/税收/盈余 | V (m×n) | 是,约束校验 |
| 第四象限 | 增加值×最终使用 | 再分配 | — | 一般忽略 |
行方向:Σ_j z_ij + y_i = X_i,i 部门的总产出等于它被各部门中间消耗的部分加上最终使用。列方向:Σ_i z_ij + v_j = X_j,j 部门的总投入等于中间投入合计加增加值合计。这两条恒等式是所有后续计算的护栏,任何一步算完都要回来核对。
2.2 用 python-pptx 抽出课件里的表格
python-pptx 只能读 .pptx,遇到老的二进制 .ppt 先用 LibreOffice 无头模式转一次格式:
# 把二进制 ppt 转成 pptx,输出到当前目录 soffice --headless --convert-to pptx "投入产出系数和投入产出模型.ppt"转换后逐页找表格形状,把单元格文本抠出来拼成 DataFrame:
from pptx import Presentation import pandas as pd prs = Presentation("投入产出系数和投入产出模型.pptx") frames = {} for i, slide in enumerate(prs.slides, start=1): for shape in slide.shapes: if not shape.has_table: # 只处理表格形状,文本框跳过 continue tbl = shape.table # 单元格里常带千分位逗号、全角空格、换行,先清洗 rows = [[c.text.strip().replace(",", "").replace("\u3000", "") for c in r.cells] for r in tbl.rows] df = pd.DataFrame(rows[1:], columns=rows[0]) df = df.set_index(df.columns[0]) # 第一列是部门名,设成索引 frames[f"slide{i}_{shape.shape_id}"] = df print(list(frames.keys()))has_table是形状级判断,比按形状类型硬编码可靠。清洗那一步不能省:年鉴导出的数字普遍带千分位,全角空格会把pd.to_numeric直接打回 NaN。合并单元格会让同一段文字在多个 cell 里重复出现,抽完要人工看一眼表头,别指望自动对齐。
抽出来的是字符串,还要转数值并丢掉合计行:
def to_numeric(df): out = df.apply(pd.to_numeric, errors="coerce") # 合计行、注释行的标签转数值后是 NaN,整行整列丢 return out.dropna(how="all").dropna(axis=1, how="all").astype(float) Z = to_numeric(frames["slide3_5"]).values # 中间流量矩阵 x = to_numeric(frames["slide3_6"]).values.reshape(-1) # 总投入列errors="coerce"把不认识的文本变 NaN 而不是抛异常,配合dropna才能一次清掉合计行。注意x要 reshape 成一位数组,后面要按列做除法。
2.3 直接消耗系数 a_ij 的计算与三条校验
直接消耗系数就是列归一化:a_ij = z_ij / X_j,含义是 j 部门每生产一单位总产出,直接消耗 i 部门产品的数量。
import numpy as np def direct_coefficients(Z, x): """Z: 中间流量矩阵 (n,n),行=消耗方,列=生产方 x: 总投入/总产出列向量 (n,)""" Z = np.asarray(Z, dtype=float) x = np.asarray(x, dtype=float).reshape(-1) assert Z.shape[0] == Z.shape[1] == x.size, "维度对不上,先查表头" A = Z / x[np.newaxis, :] # 按列除,分母是 j 部门的总投入 return A A = direct_coefficients(Z, x) # 校验一:行模型平衡,X 与 A@x + Y 的差距应该在舍入误差量级 resid = np.max(np.abs(A @ x + Y.sum(axis=1) - x) / x) print("行平衡最大相对误差:", resid) # 校验二:中间投入率(A 的列和)必须落在 (0,1) colsum = A.sum(axis=0) print("中间投入率范围:", colsum.min(), colsum.max()) # 校验三:系数不允许为负 print("负值个数:", int((A < 0).sum()))x[np.newaxis, :]把列向量拉成 (1,n),触发广播,实现按列除。分母用错的典型是把增加值当成了总投入,那样列和会算出来大于 1。行平衡相对误差控制在 1e-6 以内说明搬数没搬错;如果到 1e-2 级别,多半是某个部门的最终使用列漏了出口或者存货变动。
注意:A 的行索引是「谁消耗」,列索引是「谁生产」。建模全程带着部门名当 index 和 columns,出问题时能一眼定位到是哪两个部门之间对不上,比看下标快得多。
3. 列昂惕夫逆矩阵与完全消耗系数:np.linalg 之外的三个坑
3.1 行模型推导与 (I-A) 可逆的条件
把行平衡写成矩阵形式:X = A·X + Y,移项得 (I-A)·X = Y,于是 X = (I-A)⁻¹·Y。这个逆矩阵就是列昂惕夫逆矩阵 L。(I-A)⁻¹ 还可以按级数展开成 I + A + A² + A³ + …,A 是直接消耗,A² 是经过一个中间环节的间接消耗,A³ 是经过两个环节的,累加起来就是完全消耗。
可逆的前提是级数收敛,对非负矩阵 A 来说就是谱半径 ρ(A) < 1,即最大特征值模长小于 1。实践中有一个更容易判断的充分条件:A 的每一列之和都小于 1。列和就是中间投入率,一个部门不可能把超过 100% 的产出都当中间投入用掉,所以正常数据基本都满足。
n = A.shape[0] rho = max(abs(np.linalg.eigvals(A))) print("谱半径:", rho, "| 列和最大值:", A.sum(axis=0).max()) assert rho < 1, "谱半径不小于 1,数据或口径有问题,先别求逆"3.2 用 numpy 求列昂惕夫逆矩阵
def leontief_inverse(A): n = A.shape[0] M = np.eye(n) - A cond = np.linalg.cond(M) print("cond(I-A) =", cond) # 超过 1e10 基本不可信 if cond > 1e10: raise ValueError("条件数过大,检查是否有近乎重复的部门") L = np.linalg.solve(M, np.eye(n)) # 解方程比直接 inv 数值更稳 return L L = leontief_inverse(A) B = L - np.eye(n) # 完全消耗系数 = 列昂惕夫逆 - I两个概念必须分清:L 叫完全需要系数矩阵,元素 l_ij 表示 j 部门每增加一单位最终使用,i 部门需要提供的总产出(含 j 自己那一份);B = L - I 才是完全消耗系数,扣掉了自身那一单位,表示直接加间接的消耗。做成本传导用 L,做产业链拉动测算也常用 L,别把两者混着报数。
solve和inv的结果在数学上一样,但inv内部也是解方程,直接写solve(M, np.eye(n))少一次显式求逆,条件数大时误差更可控。条件数这个数字值得每次都打出来,它本质上是「部门之间耦合得太死」的量化指标,接近 1e10 以上时,求逆结果对输入数据的第四位小数都敏感。
3.3 影响力系数与感应度系数的批量算法
这两个系数是课件里出现频率最高、也最容易被问「大于 1 说明什么」的部分。影响力系数看列,衡量 j 部门每增加一单位最终需求,对全部部门的拉动强度;感应度系数看行,衡量各部门需求同时增加时,i 部门被拉动得多厉害。
def linkage(L, names): n = L.shape[0] col = L.sum(axis=0) # 列和 -> 影响力 row = L.sum(axis=1) # 行和 -> 感应度 f = col / col.mean() e = row / row.mean() return pd.DataFrame({"影响力系数": f, "感应度系数": e}, index=names).round(4) print(linkage(L, dept_names).sort_values("影响力系数", ascending=False).head(10))| 系数 | 方向 | 公式 | 判读 |
|---|---|---|---|
| 影响力系数 | L 的列和 | Σ_i l_ij / (列和均值) | >1 拉动高于平均,适合做需求刺激落点 |
| 感应度系数 | L 的行和 | Σ_j l_ij / (行和均值) | >1 容易被需求带动,也易成供给瓶颈 |
| 两者都 >1 | 交叉 | — | 强关联部门,冲击传导路径上的关键节点 |
分母用的是列和(或行和)的算术平均,所以全部部门的系数均值恒等于 1。这意味着别去比较绝对大小,只比相对位次。想让结论稳一点,可以分别用 L 和 B 各算一遍,看排序是否一致;两个口径下都排前五的部门,通常可以直接写进报告。
4. 系数会过期:用 RAS 把旧投入产出表外推到新年度
4.1 为什么要更新 A
正规投入产出表按固定年份编制,间隔好几年。等到要用的时候,产业结构早就变了:某些部门的中间投入率降了,另一些升了。直接拿旧年的 A 去算新年的影响,误差能吃掉整个结论。RAS 法是最常用的更新手段,它假设技术系数的变动可以拆成两个方向:行方向的「替代效应」——同样的投入品被别的部门产品替代;列方向的「加工深度效应」——同样产出需要的中间投入总量变了。
RAS 需要两组外部控制量:目标年度各部门中间使用合计 u(行和)和各部门中间投入合计 v(列和)。这两个数一般从新年度的总产出和增加值反推:u_i ≈ X_i − y_i,v_j ≈ X_j − v_j(增加值)。
4.2 RAS 迭代的实现与收敛判据
def ras(A0, u, v, tol=1e-8, max_iter=1000): """A0: 基年直接消耗系数 (n,n) u : 目标年行和(各部门中间使用合计)(n,) v : 目标年列和(各部门中间投入合计)(n,)""" A = np.array(A0, dtype=float) u = np.asarray(u, dtype=float) v = np.asarray(v, dtype=float) for k in range(1, max_iter + 1): rs = A.sum(axis=1) r = np.divide(u, rs, out=np.ones_like(u), where=rs > 0) # 行乘数 A = r[:, None] * A cs = A.sum(axis=0) s = np.divide(v, cs, out=np.ones_like(v), where=cs > 0) # 列乘数 A = A * s[None, :] err = max(np.max(np.abs(A.sum(axis=1) - u)), np.max(np.abs(A.sum(axis=0) - v))) if err < tol: break return A, k, err A_new, iters, err = ras(A, u_new, v_new) print(f"迭代 {iters} 次收敛,最大误差 {err:.2e}")where=rs > 0是防零行:某些部门在基年完全没有某类中间使用,直接除会出 inf。r 是行乘数,也叫替代乘数;s 是列乘数,也叫加工乘数。最终 A_new = diag(r)·A0·diag(s),这是 RAS 的闭式形式,迭代只是求这两个向量的手段。一般几十次内收敛,超过 500 次还没到 1e-8,通常是 u 和 v 本身自相矛盾——比如两组控制量的总和差了百分之几,那是数据问题,不是算法问题。
4.3 更新结果怎么验:三条硬指标
| 校验项 | 公式 | 期望 |
|---|---|---|
| 行列和误差 | max(abs(rowsum−u), abs(colsum−v)) | < 1e-8 |
| 系数偏离度 | mean(abs(A_new − A0)) | 一般 0.005~0.05,超过 0.1 要警惕 |
| 位次稳定性 | 影响力系数排序的斯皮尔曼相关 | 大于 0.9 说明结构调整温和 |
偏离度超过 0.1 时,不要直接拿去用。先做一次对照:把 u、v 换成基年自己的行列和,跑一遍 RAS,如果结果和 A0 不一样,说明迭代实现或者控制量构造有错。位次稳定性用scipy.stats.spearmanr一行就能算,排序大幅跳变的部门,逐个回去查它的总产出和增加值数据。
提示:如果 RAS 怎么调都收敛不到目标,可以退一步用 GRAS/最小交叉熵方法,它允许部分控制量缺位,代价是实现复杂度上升一个档次。绝大多数年度更新场景,RAS 已经够用。
5. 进阶:用 SDA 拆解增长,再把结果回写成一页 PPT
系数更新完,最有信息量的动作是做结构分解分析(SDA),把两个年份之间的总产出变化拆成「技术系数变了多少」和「最终需求变了多少」。用两极分解可以避免交叉项归属的随意性:
def sda_two_polar(L0, L1, Y0, Y1): """L0/L1: 基年/目标年列昂惕夫逆矩阵 Y0/Y1: 基年/目标年最终使用列向量""" dL = L1 - L0 dY = (Y1 - Y0).reshape(-1, 1) tech = 0.5 * (dL @ ((Y0 + Y1).reshape(-1, 1))) # 技术变动贡献 final = 0.5 * ((L0 + L1) @ dY) # 需求变动贡献 return tech.ravel(), final.ravel() tech, final = sda_two_polar(L, L_new, Y.sum(axis=1), Y_new.sum(axis=1)) check = np.max(np.abs((tech + final) - (X_new - X))) print("分解残差:", check) # 应接近 0两极分解把交互项对半分给技术项和需求项,tech + final必然等于X_new - X,残差只来自浮点误差。这一步的实用价值在于回答「某部门产出涨了 20%,是它自己需求好,还是上游技术变了」。如果final占七成以上,说明是需求驱动,属于景气问题;如果tech占大头,说明产业结构真变了,旧系数不能再凑合。
结果落地时直接用 python-pptx 生成一页汇报页,比截图粘贴可控得多:
from pptx import Presentation from pptx.util import Inches, Pt prs = Presentation() slide = prs.slides.add_slide(prs.slide_layouts[5]) rows, cols = len(dept_names) + 1, 3 gt = slide.shapes.add_table(rows, cols, Inches(0.5), Inches(1.2), Inches(9), Inches(0.3 * rows)).table gt.cell(0, 0).text, gt.cell(0, 1).text, gt.cell(0, 2).text = "部门", "技术贡献", "需求贡献" for i, name in enumerate(dept_names, start=1): gt.cell(i, 0).text = name gt.cell(i, 1).text = f"{tech[i-1]:.1f}" gt.cell(i, 2).text = f"{final[i-1]:.1f}" for c in range(3): gt.cell(i, c).text_frame.paragraphs[0].font.size = Pt(10) prs.save("sda_result.pptx")一个容易忽略的细节:表格行高按0.3 * rows估算,超过 20 个部门就得拆成两页,否则字号会被自动压到看不清。另外把dept_names和三个数值列一起落成 CSV 再用pd.read_csv(..., index_col=0)读回,下一轮更新时就不用重新对表头——部门口径是这套流程里最容易漂移的东西,固定住它对结果复现的帮助比任何算法优化都大。
本文还有配套的精品资源,点击获取