02 분석 — 36→62 배분, 시변 몫, 62→69 분화, 연쇄가중 총량 재현¶
계산 구조.
- 36→62 배분. KEIT 산업 $k$의 모산업(BOK36 또는 병합 묶음)을 $b(k)$, 같은 모산업을 나누어 가지는 KEIT 집합을 $G(b)$라 할 때 $$VA^{62}_{k,t} = VA^{36}_{b(k),t}\cdot s_k,\qquad s_k = \frac{VA^{IO}_{k}}{\sum_{k'\in G(b(k))} VA^{IO}_{k'}}$$ 이다. $VA^{IO}_k$는 KEIT $k$에 대응하는 상품중분류의 2020년 부가가치 합이다. $G$에 산업이 하나뿐이면 $s_k=1$이다.
- 시변 몫. 이차전지 $s^{B}_t=\kappa\,(C282_t/C28_t)$, $\kappa=\bar b\,/\,\overline{(C282/C28)}_{2020Q1\text{–}2023Q4}$ ($\bar b$는 IO 소분류 373/37 부가가치 비중의 2020–2023 평균). 신재생 $s^{R}_t=g_t\cdot VA^{IO}_{45}/(VA^{IO}_{45}+VA^{IO}_{46})$ ($g_t$는 신재생 발전비중을 분기로 보간한 값).
- 62→69 분화. 모산업 $p$에서 신산업과 나머지를 $VA^{new}_t=VA^{p}_t\,s$, $VA^{rest}_t=VA^{p}_t\,(1-s)$로 나눈다. 따라서 69개 산업의 합은 62개 산업의 합과 같다.
- 연쇄가중 재현. 전기 명목 비중으로 산업별 실질 성장배율을 가중해 총량의 성장배율을 만든다. $$\hat Y_t=\hat Y_{t-1}\sum_i \frac{P_{i,t-1}}{\sum_j P_{j,t-1}}\cdot\frac{X_{i,t}}{X_{i,t-1}},\qquad \hat Y_{2020Q1}=Y^{GVA}_{2020Q1}$$ $P$는 명목, $X$는 실질 부가가치이다. 2020Q1 이전은 같은 식을 역으로 적용한다.
산출물. output/levels_62.csv, mapping_62_official.csv, share_battery.csv, renew_gen_actual.csv, share_renew.csv, levels_69.csv, carve_log.csv, levels_agg.csv, io_linkage_62.csv, io_linkage_69.csv, recon_total_q.csv, recon_total_a.csv
모든 단계가 수 초 안에 끝나므로 저장된 결과를 불러오는 경로(FULL 스위치)는 두지 않았다.
import os, glob
import numpy as np, pandas as pd
import config as C
import gva_data as F
pd.set_option("display.float_format", lambda v: f"{v:,.4f}")
pd.set_option("display.width", 160)
C.setup()
real = pd.read_csv(os.path.join(C.INTERIM, "va36_real_q.csv"), index_col=0)
nom = pd.read_csv(os.path.join(C.INTERIM, "va36_nominal_q.csv"), index_col=0)
qs = F.quarters(C.SAMPLE_START, C.SAMPLE_END)
cw = F.read_crosswalk(C.CROSSWALK)
io = F.read_io_va83(C.IO_VA83, C.IO_YEAR)
def save(df, name, **kw):
df.to_csv(os.path.join(C.OUTPUT, name), encoding="utf-8-sig", **kw)
Python 3.12.10 · pandas 2.2.3 · numpy 2.4.6 ONLINE=0 (원천 재수집) FULL=0 ECOS 키 없음 · KOSIS 키 없음 36개 산업 분기 있음 data\inputs\bok36_va_q.csv 총부가가치 있음 data\inputs\gva_total_q.csv 실질 GDP 있음 data\inputs\gdp_quarterly_sa.csv IO 중분류 부가가치 있음 data\inputs\io_benchmark_va_83ind.csv IO 생산유발계수 있음 data\inputs\io_M_mid_2020.csv IO 소분류 37x 있음 data\inputs\io_small_37x_va.csv 생산지수 C28·C282 있음 data\inputs\mfg_prod_c28_c282.csv 발전량 KPX 있음 data\inputs\power_gen_kpx.csv 발전량 KEPCO 있음 data\inputs\power_gen_kepco.csv 대조표 62 있음 data\crosswalk_62.json 분화 규칙 69 있음 data\carveout_rules.csv
1. 36→62 배분 — 대조표와 2020년 산업연관표 부가가치 비중¶
대조표는 상품중분류 83개 가운데 81개를 KEIT 62개 산업에 연결한다(5 농림어업 서비스와 83 기타는 대응 KEIT가 없다). KEIT 산업의 모산업 표기에 '또는'이 있으면 마지막 코드로 확정한다. 몫은 2020년 한 시점의 값이므로 2000–2025년 전 기간에 같다. 62개 산업의 합은 36개 산업의 합과 분기마다 같아야 한다.
L62, mapping = F.build_levels_62(real.loc[qs], cw, io)
save(L62, "levels_62.csv"); save(mapping, "mapping_62_official.csv", index=False)
gap62 = (L62.drop(columns="총량GDP").sum(axis=1) - real.loc[qs].sum(axis=1)).abs().max()
print(f"62 패널 {L62.shape} · 결측 {int(L62.isna().sum().sum())} · 62합−36합 최대오차 {gap62:.2e} 십억원")
print("유형:", mapping["유형"].str.split("(").str[0].value_counts().to_dict())
mapping[mapping["유형"].str.startswith("분할")].head(10)
62 패널 (104, 63) · 결측 0 · 62합−36합 최대오차 2.91e-10 십억원
유형: {'분할': 47, '1:1/병합': 15}
| KEIT | 산업명 | BOK36부모 | 유형 | 몫 | |
|---|---|---|---|---|---|
| 0 | 1 | 농업 | 1101 | 분할(3) | 0.8196 |
| 1 | 2 | 임업 | 1101 | 분할(3) | 0.0726 |
| 2 | 3 | 어업 | 1101 | 분할(3) | 0.1078 |
| 3 | 4 | 석탄ㆍ원유ㆍ천연가스 | 1102 | 분할(2) | 0.0160 |
| 4 | 5 | 금속ㆍ비금속광업 | 1102 | 분할(2) | 0.9840 |
| 5 | 6 | 식료품 | 110301 | 분할(3) | 0.6025 |
| 6 | 7 | 음료품 | 110301 | 분할(3) | 0.1611 |
| 7 | 8 | 담배 | 110301 | 분할(3) | 0.2364 |
| 8 | 9 | 섬유ㆍ의복 | 110302 | 분할(2) | 0.9250 |
| 9 | 10 | 가죽제품 | 110302 | 분할(2) | 0.0750 |
2. 이차전지 시변 몫¶
산업연관표 소분류 373(전지)의 전기장비(37) 내 부가가치 비중을 연도별로 계산하면 아래와 같다. 원 코드는 이 값을 소수 셋째 자리로 반올림한 상수
(config.BATTERY_BENCH)를 평균해 벤치마크로 썼다. 공표 결과를 재현하기 위해 같은 상수를 쓴다. 반올림 전 값과의 차이는 아래 표에 함께 보이며, 영향은 README의 쟁점에 적었다.
생산지수는 월 원지수를 분기 평균한 뒤 비율을 구한다. 2000Q1–2025Q4 밖의 분기는 쓰지 않는다.
small = pd.read_csv(C.IO_SMALL, dtype={"소분류코드": str})
bench = F.io_battery_bench(small)
tb = pd.DataFrame({"반올림 전": bench, "원 코드 상수": pd.Series(C.BATTERY_BENCH)})
tb["상수 = 반올림(3자리)"] = tb["반올림 전"].round(3).eq(tb["원 코드 상수"])
print(f"벤치마크 평균: 상수 {np.mean(list(C.BATTERY_BENCH.values())):.5f} · 반올림 전 {bench.mean():.5f}")
mfg = F.read_mfg_prod(C.MFG_PROD)
share_b, kappa, proxy = F.battery_share(mfg, qs, C.BATTERY_BENCH)
save(share_b.to_frame(), "share_battery.csv")
print(f"κ = {kappa:.4f} (proxy 2020Q1–2023Q4 평균 {proxy.loc['2020Q1':'2023Q4'].mean():.4f})")
print("이차전지 몫:", {q: round(float(share_b[q]), 4) for q in ["2000Q1", "2010Q1", "2020Q1", "2025Q4"]})
tb
벤치마크 평균: 상수 0.19875 · 반올림 전 0.19846
κ = 0.1822 (proxy 2020Q1–2023Q4 평균 1.0907)
이차전지 몫: {'2000Q1': 0.0272, '2010Q1': 0.1654, '2020Q1': 0.1846, '2025Q4': 0.1503}
| 반올림 전 | 원 코드 상수 | 상수 = 반올림(3자리) | |
|---|---|---|---|
| 2020 | 0.1816 | 0.1820 | True |
| 2021 | 0.1999 | 0.2000 | True |
| 2022 | 0.2147 | 0.2150 | True |
| 2023 | 0.1976 | 0.1980 | True |
3. 신재생에너지 시변 몫¶
연도별 발전비중을 해당 연도 1분기 값으로 두고 다음 해 1분기까지 선형으로 보간한다. 2025년 이후는 2025년 값을 유지한다. 2000–2005년은 발전원별 실측이 없어 2000년 0.9%에서 2006년 실측까지 선형으로 잇는다. 원 코드와 같이 2016–2025년 발전비중은 소수 여섯째 자리로 반올림한 값을 쓴다. 전력부문 부가가치 몫은 상품중분류 45(전력 및 신재생에너지)/(45+46 가스·증기 및 온수)이며, 원 코드의 상수와 같은지 확인한다.
elec = F.io_share(io, [45], [45, 46])
print(f"전력부문 VA몫: IO 계산 {elec:.16f} · 원 코드 상수 {C.ELEC_SHARE:.16f} · 차이 {elec - C.ELEC_SHARE:.1e}")
kpx, kepco = pd.read_csv(C.POWER_KPX), pd.read_csv(C.POWER_KEPCO)
act = F.renew_actual(kpx, kepco)
share_r, by_year = F.renew_share(act, kpx, C.ELEC_SHARE, C.RENEW_BASE2000, qs)
save(act, "renew_gen_actual.csv", index=False); save(share_r.to_frame(), "share_renew.csv")
pd.DataFrame({"발전비중(%)": {y: 100 * by_year[y] for y in [2000, 2006, 2010, 2016, 2020, 2025]},
"신재생 몫(Q1)": {y: share_r[f"{y}Q1"] for y in [2000, 2006, 2010, 2016, 2020, 2025]}})
전력부문 VA몫: IO 계산 0.7959934262013306 · 원 코드 상수 0.7959934262013307 · 차이 -1.1e-16
| 발전비중(%) | 신재생 몫(Q1) | |
|---|---|---|
| 2000 | 0.9000 | 0.0072 |
| 2006 | 1.0439 | 0.0083 |
| 2010 | 1.8965 | 0.0151 |
| 2016 | 4.7805 | 0.0381 |
| 2020 | 6.7086 | 0.0534 |
| 2025 | 11.5004 | 0.0915 |
4. 62→69 분화¶
분화 규칙표(data/carveout_rules.csv)의 순서대로 모산업 열을 신산업과 '(잔여)' 열로 나누고 모산업 열을 지운다.
반도체·조선업·철강·택배·건설은 2020년 산업연관표 상품중분류 비중(상수), 이차전지·신재생에너지는 2·3절의 시변 몫을 쓴다.
건설은 잔여 없이 건물건설·건축보수와 토목건설로 나눈다. 따라서 산업 수는 62 + 6 + 1 = 69이다.
rules = pd.read_csv(C.CARVE_RULES, dtype={"IO분자": str, "IO분모": str})
L69, carve_log = F.build_levels_69(L62, rules, io, share_b, share_r)
save(L69, "levels_69.csv"); save(carve_log, "carve_log.csv", index=False)
gap69 = (L69.drop(columns="총량GDP").sum(axis=1) - real.loc[qs].sum(axis=1)).abs().max()
print(f"69 패널 {L69.shape} · 산업 {L69.shape[1] - 1}개 · 69합−36합 최대오차 {gap69:.2e} 십억원 · 분화 항등식 최대오차 {F.mece_gap(L62, L69, rules):.2e}")
carve_log
69 패널 (104, 70) · 산업 69개 · 69합−36합 최대오차 2.33e-10 십억원 · 분화 항등식 최대오차 3.64e-12
| 신산업 | 모산업 | 나머지 열 | 방식 | 몫(평균) | |
|---|---|---|---|---|---|
| 0 | 반도체 | 22.전자부품ㆍ컴퓨터ㆍ영상ㆍ음향ㆍ통신장비 | 22.전자부품ㆍ컴퓨터ㆍ영상ㆍ음향ㆍ통신장비(잔여) | IO고정 | 0.5471 |
| 1 | 조선업 | 27.기타 운송장비 | 27.기타 운송장비(잔여) | IO고정 | 0.5703 |
| 2 | 철강 | 20.1차 금속 | 20.1차 금속(잔여) | IO고정 | 0.7041 |
| 3 | 택배 | 38.창고ㆍ운송보조서비스업 | 38.창고ㆍ운송보조서비스업(잔여) | IO고정 | 0.2066 |
| 4 | 이차전지 | 24.전기장비 | 24.전기장비(잔여) | share_battery | 0.1394 |
| 5 | 신재생에너지 | 29.전기ㆍ가스ㆍ증기ㆍ공기조절공급업 | 29.전기ㆍ가스ㆍ증기ㆍ공기조절공급업(잔여) | share_renew | 0.0335 |
| 6 | 건물건설건축보수 | 33.건설업 | 토목건설 | IO고정 | 0.7013 |
5. 총량 패널¶
총량 모형의 입력 levels_agg.csv는 실질 GDP(지출측, 계절조정) 1980Q1–2025Q4이다. 두 열(총량GDP, 총량)은 같은 값이며 모형 입력 형식을 맞추기 위한 것이다.
62·69 패널의 총량GDP 열은 이름과 달리 36개 산업 실질VA의 단순합이다(README 정정 기록 참조).
agg = F.build_levels_agg(F.read_gdp(C.GDP_Q), C.LONG_START, C.SAMPLE_END)
save(agg, "levels_agg.csv")
print(f"levels_agg {agg.shape} · {agg.index[0]}–{agg.index[-1]}")
agg.tail(3)
levels_agg (184, 2) · 1980Q1–2025Q4
| 총량GDP | 총량 | |
|---|---|---|
| 분기 | ||
| 2025Q2 | 578,719.2000 | 578,719.2000 |
| 2025Q3 | 586,599.9000 | 586,599.9000 |
| 2025Q4 | 585,964.0000 | 585,964.0000 |
6. 산업연관 연계강도(산업모형 사전용)¶
중분류 생산유발계수 $M$을 62·69 산업 단위로 묶는다. 산업 $I$의 상품집합에서 산업 $J$의 상품집합으로 가는 연계강도는 $W_{IJ}=\sum_{i\in I} va_i\sum_{j\in J}M_{ij}\,/\sum_{i\in I}va_i$이고, 오프대각 평균이 1이 되도록 나눈 뒤 하한 0.05를 둔다(대각은 1). 이차전지·신재생에너지는 상품중분류에 별도 코드가 없어 연계가 부여되지 않는다(69열 중 67열에 부여).
M = pd.read_csv(C.IO_M)
W62, W69, covered = F.io_linkage(L62, L69, cw, io, M, C.CARVE_IO)
save(W62, "io_linkage_62.csv"); save(W69, "io_linkage_69.csv")
off = W62.values[~np.eye(len(W62), dtype=bool)]
print(f"io_linkage_62 {W62.shape} · 오프대각 {off.min():.3f}–{off.max():.3f} · io_linkage_69 {W69.shape} · 연계 부여 {covered}/{W69.shape[0]}")
io_linkage_62 (62, 62) · 오프대각 0.050–27.093 · io_linkage_69 (69, 69) · 연계 부여 67/69
7. 연쇄가중 총부가가치 재현¶
36개 산업 실질값의 단순합과 공식 총부가가치(200Y104 항목 1200)의 괴리를 계산하고, 2020Q1을 기준점으로 양방향 연쇄를 이어 재현값을 만든다. 같은 방식을 분기 계열의 연간 합에 적용한 결과도 함께 계산한다(기준점 2020년).
gva = F.read_gva_total(C.GVA_TOTAL)
rq = F.recon_table(real, nom, gva, F.ANCHOR)
ra = F.recon_annual(real, nom, gva, "2020")
save(rq, "recon_total_q.csv"); save(ra, "recon_total_a.csv")
gq = rq["재현값"].pct_change() * 100 - rq["총부가가치"].pct_change() * 100
summ = pd.DataFrame({"평균 |괴리|(%)": rq[["단순합_괴리_pct", "재현_괴리_pct"]].abs().mean(),
"최대 |괴리|(%)": rq[["단순합_괴리_pct", "재현_괴리_pct"]].abs().max()})
print(f"분기 성장률 차이(재현−공식): 평균 |차이| {gq.abs().mean():.3f}%p · 최대 {gq.abs().max():.3f}%p")
print(f"연간 재현 최대 |괴리| {ra['재현_괴리_pct'].abs().max():.4f}% (단순합 {ra['단순합_괴리_pct'].abs().max():.2f}%)")
summ
분기 성장률 차이(재현−공식): 평균 |차이| 0.049%p · 최대 0.466%p 연간 재현 최대 |괴리| 0.0014% (단순합 23.10%)
| 평균 |괴리|(%) | 최대 |괴리|(%) | |
|---|---|---|
| 단순합_괴리_pct | 7.6547 | 25.2081 |
| 재현_괴리_pct | 0.2324 | 0.8232 |
8. (ONLINE=1) 재수집 자료로 다시 계산¶
01의 6절에서 재수집한 36개 산업 실질값과 생산지수로 62 패널과 이차전지 몫을 다시 계산해 스냅샷 기반 결과와의 차이를 보인다. 공표 수치 검증(03)은 스냅샷 결과만 대상으로 한다.
p_va, p_mfg = os.path.join(C.INTERIM, "online_va36_real_q.csv"), os.path.join(C.INTERIM, "online_mfg_prod.csv")
if not (C.ONLINE and os.path.exists(p_va) and os.path.exists(p_mfg)):
print("ONLINE=0 또는 재수집 자료 없음 — 건너뛴다.")
else:
live = pd.read_csv(p_va, index_col=0)
L62_live, _ = F.build_levels_62(live.loc[qs], cw, io)
sb_live, k_live, _ = F.battery_share(pd.read_csv(p_mfg, index_col=0), qs, C.BATTERY_BENCH)
print(f"62 패널 최대 절대차 {(L62_live - L62).abs().max().max():.4f} 십억원 · 이차전지 몫 최대차 {(sb_live - share_b).abs().max():.2e} · κ {k_live:.4f}")
ONLINE=0 또는 재수집 자료 없음 — 건너뛴다.