03 결과 도출 — 원고 표·그림과 검증¶
02_분석.ipynb의 주 산출물(5장 정비판 최종 전망 투입)로 7장 「인력수급 시사점」의 표와 그림을 만든다.
산업별 증감은 2025년 실적(대분류 취업자의 부가가치 비중 배분)에서 2035년 S1 전망까지의 변화로 나타낸다.
2026-10 정비판. 보고서1은 거시 기준선을 다시 작성하였다. BOKDPM의 장기 정상상태를 현재 여건에 맞추어 고치고(잠재성장률 1.8%, 물가목표 2%, 실질유가 일정),
실질금리 외생 투입을 1차 차분으로 바꾸었으며, 외생 경로에 전달되지 않던 시나리오 레버를 연결한 뒤 산업 전망과 정합화를 다시 계산하였다.
이 패키지는 그 결과인 5장 정비판 최종 전망으로 주 투입을 바꾸었다. 취업계수의 기준 수준·추세·계수 경로를 정하는 방법과 취업자·부가가치 이력 자료는 바꾸지 않았다.
이전 판(2026-10-08 판)의 투입·산출 파일은 data/published_prev/에 보존하여 재현 점검과 비교에 쓴다.
표 6·7은 정비판을 이전 판과 비교하고, 표 8은 인력수급 전망(고용노동부·한국고용정보원 「2024~2034 중장기 인력수급 전망」)의 취업자 증감과 비교한다.
마지막 셀은 주 산출물을 config.PUBLISHED(7장 인용 수치)와 대조하고, 이전 판 산출 파일과 최초 판(v0, 2026-08-04 게시) 파일의 바이트 단위 재현을 확인한다.
import os, json
import numpy as np, pandas as pd
import config as C
import labor_demand as F
pd.set_option("display.float_format", lambda v: f"{v:,.3f}")
C.setup()
import matplotlib.pyplot as plt
import koreanize_matplotlib # noqa: F401 (한글 글꼴)
ld = pd.read_csv(os.path.join(C.OUTPUT, "labor_demand_36.csv"))
ld_prev = pd.read_csv(os.path.join(C.OUTPUT, "labor_demand_36_prev.csv"))
ld0 = pd.read_csv(os.path.join(C.OUTPUT, "labor_demand_36_v0.csv"))
act25 = pd.read_csv(os.path.join(C.OUTPUT, "labor_actual_2025_36.csv"), index_col=0)["취업자_천명"]
par = pd.read_csv(os.path.join(C.OUTPUT, "coef_params.csv"), index_col=0)
emp_a = pd.read_csv(os.path.join(C.INTERIM, "emp_section_annual_sa.csv"), index_col=0)
va_a = pd.read_csv(os.path.join(C.INTERIM, "va36_hist_annual.csv"), index_col=0)
smap_raw = pd.read_csv(C.SECTION_MAP, dtype={"산업코드": str})
smap = smap_raw.set_index("산업명")
sec_of = dict(zip(smap_raw["산업명"], smap_raw["대분류"]))
sec_name = dict(zip(smap_raw["대분류"], smap_raw["대분류명"]))
def wide(d, val):
return {s: g.pivot(index="연도", columns="산업", values=val) for s, g in d.groupby("시나리오")}
P, P_prev = wide(ld, "취업자_천명"), wide(ld_prev, "취업자_천명")
VA = wide(pd.read_csv(os.path.join(C.INTERIM, "va36_recon_annual.csv")), "실질VA_십억원")
VA_prev = wide(pd.read_csv(os.path.join(C.INTERIM, "va36_recon_prev_annual.csv")), "실질VA_십억원")
tot = pd.DataFrame({s: p.sum(axis=1) for s, p in P.items()})
tot_prev = pd.DataFrame({s: p.sum(axis=1) for s, p in P_prev.items()})
tot_v0 = ld0.groupby(["시나리오", "연도"])["취업자_천명"].sum().unstack(0)
emp25 = act25.sum()
COLORS = {"S1": "#1f4e79", "S2": "#b5402f", "S3": "#2e7d32", "S4": "#a9781f", "S5": "#7b3fa0"}
Python 3.12.10 · pandas 2.2.3 · numpy 2.4.6 · matplotlib 3.11.0 스위치: ONLINE=0 · FULL=0 · API_KEY_BOK 없음 취업자(분기) 있음 data\inputs\emp_section_q.csv 실질VA 이력(분기) 있음 data\inputs\va36_hist_q.csv 주 투입: 정비판 VA 있음 data\inputs\va36_reconciled_annual.csv 이전 판 투입: VA 있음 data\published_prev\va36_reconciled_annual_prev.csv 이전 판 산출 JSON 있음 data\published_prev\labor_demand_viewer_data_prev.json 최초 판 투입: 분기 VA 있음 data\inputs\scenario_viewer_39q_data.json 최초 판 게시 JSON 있음 data\published\labor_demand_viewer_data_v0_pre-reconciliation.json 산업 대응표 있음 data\section_map_36_19.csv
표 1. 시나리오별 전산업 취업자 전망(천 명)¶
t1 = pd.DataFrame({F.SCENARIO_LABELS[s]: {"2025 실적": emp25, "2026": tot.loc[2026, s], "2030": tot.loc[2030, s], "2035": tot.loc[2035, s],
"증감 2025→2035": tot.loc[2035, s] - emp25,
"연평균 2025–35(%)": F.cagr(emp25, tot.loc[2035, s], 10)} for s in C.SCENARIOS}).T
t1.to_csv(os.path.join(C.OUTPUT, "table1_totals.csv"), encoding="utf-8-sig")
t1.style.format({"2025 실적": "{:,.1f}", "2026": "{:,.1f}", "2030": "{:,.1f}", "2035": "{:,.1f}",
"증감 2025→2035": "{:+,.1f}", "연평균 2025–35(%)": "{:+.2f}"})
| 2025 실적 | 2026 | 2030 | 2035 | 증감 2025→2035 | 연평균 2025–35(%) | |
|---|---|---|---|---|---|---|
| S1 기준 | 28,681.5 | 28,526.2 | 28,670.8 | 28,560.2 | -121.3 | -0.04 |
| S2 제약+위기 | 28,681.5 | 28,289.4 | 28,648.0 | 28,517.2 | -164.3 | -0.06 |
| S3 제약완화 | 28,681.5 | 28,656.1 | 28,678.1 | 28,595.3 | -86.2 | -0.03 |
| S4 기술도약 | 28,681.5 | 28,652.6 | 28,681.0 | 28,517.5 | -164.0 | -0.06 |
| S5 지정학 | 28,681.5 | 28,337.0 | 28,729.0 | 28,614.9 | -66.6 | -0.02 |
그림 1. 시나리오별 전산업 취업자 경로(2025 실적, 2026–2035 전망)¶
점선은 이전 판(2026-10-08 판) S1이다. 최초 판(v0)은 수준이 크게 달라(2035년 S1 3,628만 명) 그림에서 뺐으며 표 9에 수치를 둔다.
fig, ax = plt.subplots(figsize=(8, 4.2))
for s in C.SCENARIOS:
ax.plot([2025] + list(tot.index), [emp25] + list(tot[s]), marker="o", ms=3, color=COLORS[s], label=F.SCENARIO_LABELS[s])
ax.plot([2025] + list(tot_prev.index), [emp25] + list(tot_prev["S1"]), ls="--", color="#8592a3", lw=1.2, label="이전 판 S1(2026-10-08 판)")
ax.scatter([2025], [emp25], color="#1a2330", zorder=3, label="2025 실적")
ax.set_ylabel("천 명"); ax.grid(alpha=.3); ax.legend(frameon=False, ncol=3, fontsize=8)
ax.set_title("전산업 취업자: 시나리오별 전망(정비판)", loc="left")
fig.tight_layout(); fig.savefig(os.path.join(C.OUTPUT, "fig1_total_paths.png"), dpi=150)
표 2. 인력수요 확대 성장산업(S1, 2025→2035 증가 폭 상위 8개)¶
2025년 값은 대분류 실측 취업자를 2025년 실질부가가치 비중으로 나눈 값이다. 대분류가 산업 하나인 경우(보건복지·도소매 등 15개 대분류)에는 실측치와 같다.
s1 = P["S1"]
chg = pd.DataFrame({"대분류": smap.loc[act25.index, "대분류"].values, "2025 실적(천명)": act25, "2035 S1(천명)": s1.loc[2035, act25.index]})
chg["증감(천명)"] = chg["2035 S1(천명)"] - chg["2025 실적(천명)"]
chg["증감률(%)"] = 100 * chg["증감(천명)"] / chg["2025 실적(천명)"]
chg["연평균(%)"] = 100 * ((chg["2035 S1(천명)"] / chg["2025 실적(천명)"]) ** 0.1 - 1)
chg["2035 비중(%)"] = 100 * chg["2035 S1(천명)"] / chg["2035 S1(천명)"].sum()
chg = chg.sort_values("증감(천명)", ascending=False)
chg.to_csv(os.path.join(C.OUTPUT, "table2_change_s1.csv"), encoding="utf-8-sig")
fmt = {"2025 실적(천명)": "{:,.1f}", "2035 S1(천명)": "{:,.1f}", "증감(천명)": "{:+,.1f}", "증감률(%)": "{:+.1f}",
"연평균(%)": "{:+.2f}", "2035 비중(%)": "{:.1f}"}
hw = "의료, 보건업 및 사회복지서비스업"
print(f"보건복지 비중: 2025년 {100 * act25[hw] / emp25:.1f}% → 2035년 S1 {chg.loc[hw, '2035 비중(%)']:.1f}%")
chg.head(8).style.format(fmt)
보건복지 비중: 2025년 11.1% → 2035년 S1 16.2%
| 대분류 | 2025 실적(천명) | 2035 S1(천명) | 증감(천명) | 증감률(%) | 연평균(%) | 2035 비중(%) | |
|---|---|---|---|---|---|---|---|
| 산업 | |||||||
| 의료, 보건업 및 사회복지서비스업 | Q | 3,175.8 | 4,626.7 | +1,450.9 | +45.7 | +3.83 | 16.2 |
| 컴퓨터, 전자 및 광학기기 제조업 | C | 1,597.7 | 2,588.6 | +990.8 | +62.0 | +4.94 | 9.1 |
| 출판, 방송, 영상, 정보서비스 | J | 825.2 | 981.1 | +155.9 | +18.9 | +1.75 | 3.4 |
| 통신업 | J | 314.1 | 412.1 | +98.0 | +31.2 | +2.75 | 1.4 |
| 공공행정, 국방 및 사회보장 | O | 1,329.8 | 1,411.6 | +81.9 | +6.2 | +0.60 | 4.9 |
| 전문, 과학 및 기술관련 서비스업 | M | 1,475.8 | 1,556.8 | +81.1 | +5.5 | +0.54 | 5.5 |
| 수도, 하수 및 폐기물 처리, 원료 재생업 | E | 148.2 | 193.9 | +45.6 | +30.8 | +2.72 | 0.7 |
| 예술, 스포츠 및 여가관련 서비스업 | R | 553.2 | 578.5 | +25.2 | +4.6 | +0.45 | 2.0 |
표 3. 인력 재배치가 필요한 정체·감소산업(S1, 2025→2035 감소 폭 상위 6개)¶
chg.tail(6).sort_values("증감(천명)").style.format(fmt)
| 대분류 | 2025 실적(천명) | 2035 S1(천명) | 증감(천명) | 증감률(%) | 연평균(%) | 2035 비중(%) | |
|---|---|---|---|---|---|---|---|
| 산업 | |||||||
| 도소매업 | G | 3,213.5 | 2,226.2 | -987.3 | -30.7 | -3.60 | 7.8 |
| 숙박 및 음식점업 | I | 2,319.2 | 1,844.5 | -474.8 | -20.5 | -2.26 | 6.5 |
| 기타 서비스업 | S | 1,145.5 | 893.4 | -252.1 | -22.0 | -2.46 | 3.1 |
| 교육서비스업 | P | 1,943.0 | 1,708.3 | -234.7 | -12.1 | -1.28 | 6.0 |
| 금융 및 보험업 | K | 811.2 | 626.9 | -184.3 | -22.7 | -2.54 | 2.2 |
| 토목건설업 | F | 562.0 | 438.1 | -123.9 | -22.0 | -2.46 | 1.5 |
그림 2. 36개 산업 취업자 증감(S1, 2025 실적→2035)¶
q = chg["증감(천명)"].sort_values()
fig, ax = plt.subplots(figsize=(8, 9))
ax.barh(q.index, q.values, color=["#b5402f" if v < 0 else "#1f4e79" for v in q.values])
ax.set_xlabel("천 명"); ax.grid(axis="x", alpha=.3); ax.tick_params(axis="y", labelsize=7)
ax.set_title("산업별 취업자 증감(S1, 2025 실적→2035)", loc="left")
fig.tight_layout(); fig.savefig(os.path.join(C.OUTPUT, "fig2_change_s1.png"), dpi=150)
표 4. 시나리오별 인력수급 위험(2035년, 시나리오 간 범위 상위 10개 산업)¶
같은 산업이라도 거시 시나리오에 따라 2035년 취업자 전망이 달라진다. 범위(최고 − 최저)가 큰 산업일수록 시나리오에 따른 인력수급 불확실성이 크다.
y35 = pd.DataFrame({s: P[s].loc[2035] for s in C.SCENARIOS})
t4 = pd.DataFrame({"S1(천명)": y35["S1"], "최저": y35.idxmin(axis=1), "최저(천명)": y35.min(axis=1),
"최고": y35.idxmax(axis=1), "최고(천명)": y35.max(axis=1)})
t4["범위(천명)"] = t4["최고(천명)"] - t4["최저(천명)"]
t4["범위/S1(%)"] = 100 * t4["범위(천명)"] / t4["S1(천명)"]
t4 = t4.sort_values("범위(천명)", ascending=False)
t4.to_csv(os.path.join(C.OUTPUT, "table4_scenario_range_2035.csv"), encoding="utf-8-sig")
t4.head(10).style.format({"S1(천명)": "{:,.1f}", "최저(천명)": "{:,.1f}", "최고(천명)": "{:,.1f}", "범위(천명)": "{:,.1f}", "범위/S1(%)": "{:.1f}"})
| S1(천명) | 최저 | 최저(천명) | 최고 | 최고(천명) | 범위(천명) | 범위/S1(%) | |
|---|---|---|---|---|---|---|---|
| 산업 | |||||||
| 사업지원 서비스업 | 1,393.3 | S2 | 1,326.7 | S4 | 1,467.4 | 140.7 | 10.1 |
| 농림어업 | 1,374.1 | S4 | 1,311.0 | S5 | 1,437.4 | 126.4 | 9.2 |
| 전문, 과학 및 기술관련 서비스업 | 1,556.8 | S5 | 1,508.2 | S4 | 1,591.5 | 83.3 | 5.4 |
| 운수업 | 1,674.4 | S2 | 1,654.5 | S5 | 1,731.9 | 77.3 | 4.6 |
| 숙박 및 음식점업 | 1,844.5 | S2 | 1,818.4 | S5 | 1,895.0 | 76.6 | 4.2 |
| 의료, 보건업 및 사회복지서비스업 | 4,626.7 | S4 | 4,588.4 | S2 | 4,660.3 | 71.9 | 1.6 |
| 도소매업 | 2,226.2 | S4 | 2,189.8 | S2 | 2,256.9 | 67.2 | 3.0 |
| 비주거용 건물 건설업 | 650.1 | S5 | 616.9 | S4 | 681.8 | 65.0 | 10.0 |
| 출판, 방송, 영상, 정보서비스 | 981.1 | S4 | 948.5 | S2 | 1,007.5 | 59.0 | 6.0 |
| 주거용 건물 건설업 | 399.8 | S4 | 380.5 | S2 | 428.7 | 48.2 | 12.1 |
표 5. 대분류 취업계수의 기준 수준과 추세¶
t5 = pd.DataFrame({"대분류명": par["대분류명"], "기준 수준(명/10억원)": par["기준수준_b"] * 1000,
"연간 로그변화율(%)": par["연간로그변화율_g"] * 100, "추세 R²": par["추세_R2"]})
t5.to_csv(os.path.join(C.OUTPUT, "table5_coef_params.csv"), encoding="utf-8-sig")
t5.style.format({"기준 수준(명/10억원)": "{:.2f}", "연간 로그변화율(%)": "{:+.2f}", "추세 R²": "{:.2f}"})
| 대분류명 | 기준 수준(명/10억원) | 연간 로그변화율(%) | 추세 R² | |
|---|---|---|---|---|
| A | 농업, 임업 및 어업 | 43.62 | +1.03 | 0.29 |
| B | 광업 | 5.15 | -3.48 | 0.24 |
| C | 제조업 | 7.10 | -2.60 | 0.94 |
| D | 전기, 가스, 증기 및 공기조절 공급업 | 2.49 | -1.94 | 0.49 |
| E | 수도, 하수 및 폐기물 처리, 원료 재생업 | 10.59 | +3.43 | 0.74 |
| F | 건설업 | 19.64 | +0.34 | 0.07 |
| G | 도매 및 소매업 | 18.46 | -3.27 | 0.95 |
| H | 운수 및 창고업 | 16.05 | -2.21 | 0.51 |
| I | 숙박 및 음식점업 | 47.07 | -0.12 | 0.01 |
| J | 정보통신업 | 10.29 | -0.40 | 0.11 |
| K | 금융 및 보험업 | 6.15 | -5.15 | 0.93 |
| L | 부동산업 | 3.26 | -0.50 | 0.23 |
| M | 전문, 과학 및 기술 서비스업 | 14.26 | +0.32 | 0.10 |
| N | 사업시설 관리, 사업 지원 및 임대 서비스업 | 19.85 | -1.47 | 0.56 |
| O | 공공행정, 국방 및 사회보장 행정 | 9.01 | -0.21 | 0.09 |
| P | 교육 서비스업 | 19.07 | -1.07 | 0.76 |
| Q | 보건업 및 사회복지 서비스업 | 23.26 | +0.01 | 0.00 |
| R | 예술, 스포츠 및 여가관련 서비스업 | 18.88 | +0.67 | 0.08 |
| S | 협회 및 단체, 수리 및 기타 개인 서비스업 | 38.69 | -0.76 | 0.40 |
표 6. 정비판과 이전 판의 비교 ① — 시나리오별 2035년 전산업 취업자(천 명)¶
이전 판(2026-10-08 판)은 정비 이전 5장 최종 전망을, 정비판은 2026-10 정비판 최종 전망을 투입했다. 계수 경로가 같으므로 차이는 모두 투입 부가가치의 차이에서 생긴다. '부가가치 차이율'은 36개 산업 실질부가가치 합계(2035년)의 정비판 대비 이전 판 차이율이며, 취업자 차이율과의 차이는 산업 구성(대분류별 취업계수의 차이)에서 생긴다.
t6 = pd.DataFrame({F.SCENARIO_LABELS[s]: {
"이전 판 2035": tot_prev.loc[2035, s], "정비판 2035": tot.loc[2035, s],
"차이": tot.loc[2035, s] - tot_prev.loc[2035, s], "차이율(%)": 100 * (tot.loc[2035, s] / tot_prev.loc[2035, s] - 1),
"부가가치 차이율(%)": 100 * (VA[s].loc[2035].sum() / VA_prev[s].loc[2035].sum() - 1),
"이전 판 증감 2025→2035": tot_prev.loc[2035, s] - emp25, "정비판 증감 2025→2035": tot.loc[2035, s] - emp25} for s in C.SCENARIOS}).T
t6.to_csv(os.path.join(C.OUTPUT, "table6_vs_prev_scenario.csv"), encoding="utf-8-sig")
r_prev, r_new = tot_prev.loc[2035].max() - tot_prev.loc[2035].min(), tot.loc[2035].max() - tot.loc[2035].min()
print(f"2035년 시나리오 간 범위(최고 − 최저): 이전 판 {r_prev:,.1f}천 명 → 정비판 {r_new:,.1f}천 명")
t6.style.format({"이전 판 2035": "{:,.1f}", "정비판 2035": "{:,.1f}", "차이": "{:+,.1f}", "차이율(%)": "{:+.2f}",
"부가가치 차이율(%)": "{:+.2f}", "이전 판 증감 2025→2035": "{:+,.1f}", "정비판 증감 2025→2035": "{:+,.1f}"})
2035년 시나리오 간 범위(최고 − 최저): 이전 판 121.8천 명 → 정비판 97.6천 명
| 이전 판 2035 | 정비판 2035 | 차이 | 차이율(%) | 부가가치 차이율(%) | 이전 판 증감 2025→2035 | 정비판 증감 2025→2035 | |
|---|---|---|---|---|---|---|---|
| S1 기준 | 29,040.1 | 28,560.2 | -479.9 | -1.65 | -2.96 | +358.6 | -121.3 |
| S2 제약+위기 | 28,988.9 | 28,517.2 | -471.7 | -1.63 | -1.68 | +307.4 | -164.3 |
| S3 제약완화 | 29,079.6 | 28,595.3 | -484.3 | -1.67 | -3.56 | +398.1 | -86.2 |
| S4 기술도약 | 29,110.8 | 28,517.5 | -593.3 | -2.04 | -4.85 | +429.3 | -164.0 |
| S5 지정학 | 29,046.2 | 28,614.9 | -431.3 | -1.49 | -2.03 | +364.7 | -66.6 |
표 7. 정비판과 이전 판의 비교 ② — 대분류별 2035년 취업자(천 명)¶
S1의 이전 판·정비판 값과 차이를 보이고, 오른쪽 열에 S2–S5의 차이(정비판 − 이전 판)를 함께 둔다. 같은 대분류 안에서는 계수가 같으므로 대분류별 취업자 차이율은 그 대분류 투입 부가가치의 차이율과 같다.
def by_sec(Pd, s, y=2035):
return Pd[s].loc[y].groupby(pd.Series(sec_of)).sum()
t7 = pd.DataFrame({"대분류명": pd.Series(sec_name)}).loc[F.SECTIONS]
t7["이전 판 S1"] = by_sec(P_prev, "S1")
t7["정비판 S1"] = by_sec(P, "S1")
t7["차이 S1"] = t7["정비판 S1"] - t7["이전 판 S1"]
t7["차이율 S1(%)"] = 100 * (t7["정비판 S1"] / t7["이전 판 S1"] - 1)
for s in C.SCENARIOS[1:]:
t7[f"차이 {s}"] = by_sec(P, s) - by_sec(P_prev, s)
tsum = t7.drop(columns="대분류명").sum()
tsum["차이율 S1(%)"] = 100 * (tsum["정비판 S1"] / tsum["이전 판 S1"] - 1)
t7.loc["계"] = pd.Series({"대분류명": "19개 대분류 합계", **tsum.to_dict()})
t7.to_csv(os.path.join(C.OUTPUT, "table7_vs_prev_section.csv"), encoding="utf-8-sig")
f7 = {c: "{:+,.1f}" for c in t7.columns if c.startswith("차이 ")}
f7.update({"이전 판 S1": "{:,.1f}", "정비판 S1": "{:,.1f}", "차이율 S1(%)": "{:+.2f}"})
t7.style.format(f7)
| 대분류명 | 이전 판 S1 | 정비판 S1 | 차이 S1 | 차이율 S1(%) | 차이 S2 | 차이 S3 | 차이 S4 | 차이 S5 | |
|---|---|---|---|---|---|---|---|---|---|
| A | 농업, 임업 및 어업 | 1,467.3 | 1,374.1 | -93.2 | -6.35 | -56.1 | -104.9 | -130.6 | -55.6 |
| B | 광업 | 2.8 | 2.9 | +0.1 | +1.84 | +0.2 | -0.0 | -0.0 | +0.1 |
| C | 제조업 | 5,299.4 | 4,805.9 | -493.5 | -9.31 | -238.1 | -615.8 | -847.5 | -321.0 |
| D | 전기, 가스, 증기 및 공기조절 공급업 | 86.2 | 89.1 | +2.9 | +3.41 | +0.8 | +4.4 | +5.8 | +1.8 |
| E | 수도, 하수 및 폐기물 처리, 원료 재생업 | 212.8 | 193.9 | -19.0 | -8.91 | -3.4 | -28.4 | -42.7 | -8.7 |
| F | 건설업 | 1,537.1 | 1,618.9 | +81.8 | +5.32 | +53.8 | +93.7 | +96.0 | +61.9 |
| G | 도매 및 소매업 | 2,180.5 | 2,226.2 | +45.7 | +2.10 | +16.7 | +48.8 | +85.7 | +30.5 |
| H | 운수 및 창고업 | 1,629.5 | 1,674.4 | +44.9 | +2.76 | -92.1 | +114.0 | +207.5 | -38.7 |
| I | 숙박 및 음식점업 | 1,894.9 | 1,844.5 | -50.5 | -2.66 | -117.4 | -10.6 | +23.0 | -85.7 |
| J | 정보통신업 | 1,367.3 | 1,393.2 | +25.9 | +1.90 | +4.7 | +40.3 | +58.5 | +14.6 |
| K | 금융 및 보험업 | 639.9 | 626.9 | -13.0 | -2.04 | +10.3 | -24.3 | -47.1 | +2.3 |
| L | 부동산업 | 521.5 | 541.6 | +20.1 | +3.85 | +16.8 | +22.3 | +29.1 | +16.7 |
| M | 전문, 과학 및 기술 서비스업 | 1,636.6 | 1,556.8 | -79.7 | -4.87 | -29.9 | -98.4 | -166.9 | -42.9 |
| N | 사업시설 관리, 사업 지원 및 임대 서비스업 | 1,466.6 | 1,393.3 | -73.4 | -5.00 | -57.9 | -88.5 | -119.7 | -70.3 |
| O | 공공행정, 국방 및 사회보장 행정 | 1,358.3 | 1,411.6 | +53.3 | +3.93 | +25.6 | +66.0 | +93.0 | +38.3 |
| P | 교육 서비스업 | 1,715.2 | 1,708.3 | -6.9 | -0.40 | +43.4 | -39.3 | -63.1 | +20.5 |
| Q | 보건업 및 사회복지 서비스업 | 4,491.3 | 4,626.7 | +135.4 | +3.01 | +20.3 | +186.4 | +282.8 | +69.5 |
| R | 예술, 스포츠 및 여가관련 서비스업 | 616.7 | 578.5 | -38.2 | -6.20 | -37.3 | -34.2 | -39.7 | -38.2 |
| S | 협회 및 단체, 수리 및 기타 개인 서비스업 | 916.0 | 893.4 | -22.6 | -2.47 | -32.0 | -15.6 | -17.5 | -26.5 |
| 계 | 19개 대분류 합계 | 29,040.1 | 28,560.2 | -479.9 | -1.65 | -471.7 | -484.3 | -593.3 | -431.3 |
표 8. 중장기 인력수급 전망과의 비교 — 취업자 증감(만 명)¶
인력수급 전망은 고용노동부·한국고용정보원 「2024~2034 중장기 인력수급 전망」(2026-02-12)의 취업자 증감이다(2024→2029년 +36.7만 명, 2029→2034년 −30.3만 명, 2024→2034년 +6.4만 명). 본 연구의 노동수요 전망은 이 값에 맞추어 조정하지 않았다. 이 표는 같은 기간의 증감을 나란히 놓고 방향과 크기를 비교하기 위한 것이며, 두 전망 사이에 제약 관계를 두지 않는다.
- 인력수급 전망은 전체 취업자, 본 연구는 19개 대분류(A–S) 합계이다. 가구 내 고용활동(T)·국제 및 외국기관(U)은 전체 취업자의 0.3% 수준이므로 증감 비교에 주는 영향은 작다.
- 본 연구의 2024년 값은 두 가지로 둔다. 실적 기준은 경제활동인구조사 계절조정 계열의 2024년 평균이고, 계수 경로 기준은 계수 경로의 2024년 값(기준 수준)에 2024년 실적 부가가치를 곱한 값이다. 기준 수준은 2023–2025년 평균 계수이므로 계수 경로 기준 값은 실적과 다르며, 그 차이만큼 2024년에서 출발하는 증감이 달라진다(README 알려진 문제 3). 2029→2034년 증감은 두 기준이 같다.
- 본 연구의 값은 부가가치 전망에 취업계수를 곱한 노동수요이며, 인구 구조나 경제활동참가율 같은 인력공급 여건을 반영하지 않는다.
periods = list(C.KEIS_EMP_CHANGE)
ch_act, ch_path, L24, L24p = F.changes_from_base(tot, emp_a, va_a, par, sec_of, periods, 2024, C.ANCHOR)
ch_prev = F.demand_changes(tot_prev, L24, 2024, periods)
keis = pd.Series({f"{a}→{b}": v for (a, b), v in C.KEIS_EMP_CHANGE.items()})
t8 = pd.DataFrame({"인력수급 전망": keis,
"정비판 S1(실적 기준)": ch_act["S1"] / 10, "차이(실적 기준)": ch_act["S1"] / 10 - keis,
"정비판 S1(계수 경로 기준)": ch_path["S1"] / 10, "차이(계수 경로 기준)": ch_path["S1"] / 10 - keis,
"정비판 S1–S5 최저": ch_act.min(axis=1) / 10, "정비판 S1–S5 최고": ch_act.max(axis=1) / 10,
"이전 판 S1(실적 기준)": ch_prev["S1"] / 10})
t8.to_csv(os.path.join(C.OUTPUT, "table8_keis_comparison.csv"), encoding="utf-8-sig")
kc = {"S1_증감_2024_2029": ch_act.loc["2024→2029", "S1"], "S1_증감_2029_2034": ch_act.loc["2029→2034", "S1"],
"S1_증감_2024_2034": ch_act.loc["2024→2034", "S1"],
"S1_증감_2024_2029_경로": ch_path.loc["2024→2029", "S1"], "S1_증감_2024_2034_경로": ch_path.loc["2024→2034", "S1"]}
print(f"2024년 19개 대분류 취업자: 실적 {L24:,.1f}천 명 · 계수 경로 기준 {L24p:,.1f}천 명(차이 {L24p - L24:+,.1f}천 명)")
print(f"S1 정점: {int(tot['S1'].idxmax())}년 {tot['S1'].max():,.1f}천 명 · 인력수급 전망: 2024→2029년 {keis.iloc[0]:+.1f}만 명, 2029→2034년 {keis.iloc[1]:+.1f}만 명 · "
f"정비판 S1: {ch_act['S1'].iloc[0] / 10:+.1f}만 명(계수 경로 기준 {ch_path['S1'].iloc[0] / 10:+.1f}만 명), {ch_act['S1'].iloc[1] / 10:+.1f}만 명")
t8.style.format("{:+,.1f}")
2024년 19개 대분류 취업자: 실적 28,482.2천 명 · 계수 경로 기준 28,581.6천 명(차이 +99.4천 명) S1 정점: 2030년 28,670.8천 명 · 인력수급 전망: 2024→2029년 +36.7만 명, 2029→2034년 -30.3만 명 · 정비판 S1: +17.2만 명(계수 경로 기준 +7.2만 명), -5.2만 명
| 인력수급 전망 | 정비판 S1(실적 기준) | 차이(실적 기준) | 정비판 S1(계수 경로 기준) | 차이(계수 경로 기준) | 정비판 S1–S5 최저 | 정비판 S1–S5 최고 | 이전 판 S1(실적 기준) | |
|---|---|---|---|---|---|---|---|---|
| 2024→2029 | +36.7 | +17.2 | -19.5 | +7.2 | -29.5 | +14.2 | +22.4 | +31.1 |
| 2029→2034 | -30.3 | -5.2 | +25.1 | -5.2 | +25.1 | -13.7 | -3.1 | +26.3 |
| 2024→2034 | +6.4 | +11.9 | +5.5 | +2.0 | -4.4 | +7.7 | +17.6 | +57.4 |
표 9. 판본 이력 — 2035년 전산업 취업자(천 명)¶
최초 판(v0, 2026-08-04 게시)은 정합화 이전 부가가치 전망을 투입해 2035년 취업자를 크게 산정했다(README 정정 기록). 이전 판과 정비판은 같은 계수에 각 시점의 5장 최종 전망을 투입한 값이다.
t9 = pd.DataFrame({"최초 판(v0)": tot_v0.loc[2035], "이전 판(2026-10-08)": tot_prev.loc[2035], "정비판": tot.loc[2035]})
t9["정비판 − 이전 판"] = t9["정비판"] - t9["이전 판(2026-10-08)"]
t9["정비판 − 최초 판"] = t9["정비판"] - t9["최초 판(v0)"]
t9.index = [F.SCENARIO_LABELS[s] for s in t9.index]
t9.to_csv(os.path.join(C.OUTPUT, "table9_vintages_2035.csv"), encoding="utf-8-sig")
t9.style.format({c: ("{:+,.1f}" if "−" in c else "{:,.1f}") for c in t9.columns})
| 최초 판(v0) | 이전 판(2026-10-08) | 정비판 | 정비판 − 이전 판 | 정비판 − 최초 판 | |
|---|---|---|---|---|---|
| S1 기준 | 36,284.8 | 29,040.1 | 28,560.2 | -479.9 | -7,724.6 |
| S2 제약+위기 | 35,703.4 | 28,988.9 | 28,517.2 | -471.7 | -7,186.1 |
| S3 제약완화 | 36,569.3 | 29,079.6 | 28,595.3 | -484.3 | -7,974.0 |
| S4 기술도약 | 36,450.6 | 29,110.8 | 28,517.5 | -593.3 | -7,933.1 |
| S5 지정학 | 36,076.5 | 29,046.2 | 28,614.9 | -431.3 | -7,461.6 |
포털 게시본 대조(ONLINE=1)¶
01_입수.ipynb가 내려받은 포털 노동수요 JSON을 산출물과 비교한다. 현행 주소의 파일은 정비판 주 산출물과, 최초 판 주소의 파일은 최초 판 게시 파일과 같아야 한다.
포털이 아직 이전 판이나 최초 판을 현행 주소로 게시하고 있으면 그 사실을 보고하며, 이 비교는 실행을 멈추지 않는다.
dl_main = os.path.join(C.PORTAL_DL, "labor_demand_viewer_data.json")
dl_v0 = os.path.join(C.PORTAL_DL, "labor_demand_viewer_data_v0_pre-reconciliation.json")
if not C.ONLINE:
print("ONLINE=0: 포털 게시본 대조를 건너뛴다.")
else:
known = {F.sha256(C.OUT_JSON): "정비판", F.sha256(C.PREV_JSON): "이전 판(2026-10-08 판)", F.sha256(C.V0_JSON): "최초 판(v0)"}
rows = []
for label, p, want, ref in [("현행 주소", dl_main, "정비판", C.OUT_JSON), ("최초 판 주소", dl_v0, "최초 판(v0)", C.V0_JSON)]:
if not os.path.exists(p):
rows.append({"포털 파일": label, "결과": "내려받지 못했다(아직 게시되지 않았거나 접속 실패)"})
continue
h = F.sha256(p)
if h in known:
msg = f"{known[h]}과 SHA-256 일치" + ("" if known[h] == want else f" — 기대한 판({want})이 아니므로 포털 갱신이 필요하다")
else:
cmpv = F.compare_viewer(json.load(open(ref, encoding="utf-8")), json.load(open(p, encoding="utf-8")))
msg = f"SHA-256 다름 · 산업값 최대 차이 {cmpv['산업값 최대절대오차']:.1f}천 명 · 합계 최대 차이 {cmpv['합계 최대절대오차']:.1f}천 명"
rows.append({"포털 파일": label, "결과": msg})
display(pd.DataFrame(rows))
ONLINE=0: 포털 게시본 대조를 건너뛴다.
검증 — 정비판 주 산출물, 이전 판 재현, 최초 판 재현¶
정비판 주 산출물. output/labor_demand_viewer_data.json을 config.PUBLISHED(7장 인용 수치)와 대조한다. 파일 SHA-256, 최초 판·이전 판 파일과의 구조 동일성(뷰어 호환),
반올림 전 계산값과 JSON 값의 일치, 전산업 합계 15개, 주요 산업 6개, 표 8(인력수급 전망 비교)의 S1 증감 5개를 확인한다.
이전 판(2026-10-08 판) 재현. data/published_prev/labor_demand_viewer_data_prev.json의 SHA-256이 config.PREVIOUS의 기록값과 같고, 재현본이 이 파일과 바이트 단위로 같으며,
시나리오별 2035년 전산업 합계가 config.PREVIOUS와 같은지 확인한다.
최초 판(v0) 재현. 2026-08-04 게시 파일(data/published/labor_demand_viewer_data_v0_pre-reconciliation.json)을 재현본과 바이트 단위로 대조한다.
공표 값은 소수 첫째 자리(100명 단위)로 반올림되어 있으므로 허용오차는 0.05천 명이다. 반올림 전 오차의 최댓값은 반올림 단위의 절반(0.05)에 가깝게 나타나는 것이 정상이며, 반올림 후 오차가 0이면 모든 값이 일치한다는 뜻이다.
PB, PV, P0 = C.PUBLISHED, C.PREVIOUS, C.PUBLISHED_V0
main = json.load(open(C.OUT_JSON, encoding="utf-8"))
prev_pub = json.load(open(C.PREV_JSON, encoding="utf-8"))
prev_out = json.load(open(C.OUT_PREV_JSON, encoding="utf-8"))
v0_pub = json.load(open(C.V0_JSON, encoding="utf-8"))
v0_out = json.load(open(C.OUT_V0_JSON, encoding="utf-8"))
h_main = F.sha256(C.OUT_JSON)
h_prevpub, h_prevout = F.sha256(C.PREV_JSON), F.sha256(C.OUT_PREV_JSON)
h_v0pub, h_v0out = F.sha256(C.V0_JSON), F.sha256(C.OUT_V0_JSON)
raw_err = max(abs(P[s].loc[y, i] - v) for s in main["scenarios"] for i, vals in main["scenarios"][s].items()
for y, v in zip(main["years"], vals))
raw_tot = max(abs(tot.loc[y, s] - v) for s, vals in main["scenarios_total"].items() for y, v in zip(main["years"], vals))
same_st = F.same_structure(main, v0_pub) and F.same_structure(main, prev_pub)
cmpp, cmp0 = F.compare_viewer(prev_out, prev_pub), F.compare_viewer(v0_out, v0_pub)
M, R, V = "[정비판 주 산출물] ", "[이전 판(2026-10-08 판) 재현] ", "[최초 판(v0) 재현] "
checks = [
(M + "JSON SHA-256 = 기록값", h_main[:16], PB["JSON_SHA256"][:16], "동일", h_main == PB["JSON_SHA256"]),
(M + "구조(키·연도·시나리오·산업 순서) = 최초 판·이전 판 파일", "동일" if same_st else "다름", "동일", "동일", same_st),
(M + "산업 수", len(main["scenarios"]["S1"]), PB["산업수"], 0, None),
(M + "연도 수", len(main["years"]), PB["연도수"], 0, None),
(M + "시나리오 수", len(main["scenarios"]), PB["시나리오수"], 0, None),
(M + "산업값 1,800개: 반올림 전 계산값 대비 최대 절대오차(천명)", raw_err, 0.0, 0.05, None),
(M + "시나리오 합계 50개: 반올림 전 계산값 대비 최대 절대오차(천명)", raw_tot, 0.0, 0.05, None),
]
for s in C.SCENARIOS:
for y in (2026, 2030, 2035):
checks.append((M + f"전산업 {s} {y}(천명)", tot.loc[y, s], PB[f"전산업_{s}_{y}"], 0.05, None))
for k, name in C.INDUSTRY_KEYS.items():
checks.append((M + f"{name} S1 2035(천명)", P["S1"].loc[2035, name], PB[f"{k}_S1_2035"], 0.05, None))
for k, v in kc.items():
checks.append((M + f"표 8 {k}(천명)", v, PB[k], 0.05, None))
checks += [
(R + "보존 파일 SHA-256 = 기록값", h_prevpub[:16], PV["JSON_SHA256"][:16], "동일", h_prevpub == PV["JSON_SHA256"]),
(R + "재현본 SHA-256 = 보존 파일", h_prevout[:16], h_prevpub[:16], "동일", h_prevout == h_prevpub),
(R + "산업값 1,800개: 반올림 후 최대 절대오차(천명)", cmpp["산업값 최대절대오차"], 0.0, 0.05, None),
(R + "시나리오 합계 50개: 반올림 후 최대 절대오차(천명)", cmpp["합계 최대절대오차"], 0.0, 0.05, None),
]
for s in C.SCENARIOS:
checks.append((R + f"전산업 {s} 2035(천명)", tot_prev.loc[2035, s], PV[f"전산업_{s}_2035"], 0.05, None))
checks += [
(V + "게시 파일 SHA-256 = 기록값", h_v0pub[:16], P0["JSON_SHA256"][:16], "동일", h_v0pub == P0["JSON_SHA256"]),
(V + "재현본 SHA-256 = 게시 파일", h_v0out[:16], h_v0pub[:16], "동일", h_v0out == h_v0pub),
(V + "산업값 1,800개: 반올림 후 최대 절대오차(천명)", cmp0["산업값 최대절대오차"], 0.0, 0.05, None),
(V + "시나리오 합계 50개: 반올림 후 최대 절대오차(천명)", cmp0["합계 최대절대오차"], 0.0, 0.05, None),
(V + "전산업 S1 2035(천명)", tot_v0.loc[2035, "S1"], P0["전산업_S1_2035"], 0.05, None),
]
res = pd.DataFrame([{"항목": k, "계산": v, "공표": p, "허용오차": tol,
"통과": bool(ok) if ok is not None else bool(abs(v - p) <= tol)} for k, v, p, tol, ok in checks])
res.to_csv(os.path.join(C.OUTPUT, "verification.csv"), index=False, encoding="utf-8-sig")
display(res)
assert res["통과"].all(), "공표 수치와 계산 결과가 다르다 — 위 표에서 통과=False 항목을 확인한다"
print(f"검증 통과({len(res)}개 항목): 정비판 주 산출물이 7장 인용 수치와 일치하고, 이전 판(2026-10-08 판) 산출 파일과 "
"최초 판(v0, 2026-08-04 게시) 파일을 바이트 단위까지 재현했다.")
| 항목 | 계산 | 공표 | 허용오차 | 통과 | |
|---|---|---|---|---|---|
| 0 | [정비판 주 산출물] JSON SHA-256 = 기록값 | 776c43a570648a03 | 776c43a570648a03 | 동일 | True |
| 1 | [정비판 주 산출물] 구조(키·연도·시나리오·산업 순서) = 최초 판·이전 판 파일 | 동일 | 동일 | 동일 | True |
| 2 | [정비판 주 산출물] 산업 수 | 36 | 36 | 0 | True |
| 3 | [정비판 주 산출물] 연도 수 | 10 | 10 | 0 | True |
| 4 | [정비판 주 산출물] 시나리오 수 | 5 | 5 | 0 | True |
| 5 | [정비판 주 산출물] 산업값 1,800개: 반올림 전 계산값 대비 최대 절대오차(천명) | 0.050 | 0.000 | 0.050 | True |
| 6 | [정비판 주 산출물] 시나리오 합계 50개: 반올림 전 계산값 대비 최대 절대오차(천명) | 0.049 | 0.000 | 0.050 | True |
| 7 | [정비판 주 산출물] 전산업 S1 2026(천명) | 28,526.159 | 28,526.200 | 0.050 | True |
| 8 | [정비판 주 산출물] 전산업 S1 2030(천명) | 28,670.837 | 28,670.800 | 0.050 | True |
| 9 | [정비판 주 산출물] 전산업 S1 2035(천명) | 28,560.194 | 28,560.200 | 0.050 | True |
| 10 | [정비판 주 산출물] 전산업 S2 2026(천명) | 28,289.437 | 28,289.400 | 0.050 | True |
| 11 | [정비판 주 산출물] 전산업 S2 2030(천명) | 28,647.957 | 28,648.000 | 0.050 | True |
| 12 | [정비판 주 산출물] 전산업 S2 2035(천명) | 28,517.237 | 28,517.200 | 0.050 | True |
| 13 | [정비판 주 산출물] 전산업 S3 2026(천명) | 28,656.081 | 28,656.100 | 0.050 | True |
| 14 | [정비판 주 산출물] 전산업 S3 2030(천명) | 28,678.064 | 28,678.100 | 0.050 | True |
| 15 | [정비판 주 산출물] 전산업 S3 2035(천명) | 28,595.314 | 28,595.300 | 0.050 | True |
| 16 | [정비판 주 산출물] 전산업 S4 2026(천명) | 28,652.640 | 28,652.600 | 0.050 | True |
| 17 | [정비판 주 산출물] 전산업 S4 2030(천명) | 28,680.978 | 28,681.000 | 0.050 | True |
| 18 | [정비판 주 산출물] 전산업 S4 2035(천명) | 28,517.456 | 28,517.500 | 0.050 | True |
| 19 | [정비판 주 산출물] 전산업 S5 2026(천명) | 28,337.017 | 28,337.000 | 0.050 | True |
| 20 | [정비판 주 산출물] 전산업 S5 2030(천명) | 28,729.033 | 28,729.000 | 0.050 | True |
| 21 | [정비판 주 산출물] 전산업 S5 2035(천명) | 28,614.873 | 28,614.900 | 0.050 | True |
| 22 | [정비판 주 산출물] 의료, 보건업 및 사회복지서비스업 S1 2035(천명) | 4,626.675 | 4,626.700 | 0.050 | True |
| 23 | [정비판 주 산출물] 도소매업 S1 2035(천명) | 2,226.205 | 2,226.200 | 0.050 | True |
| 24 | [정비판 주 산출물] 컴퓨터, 전자 및 광학기기 제조업 S1 2035(천명) | 2,588.553 | 2,588.600 | 0.050 | True |
| 25 | [정비판 주 산출물] 숙박 및 음식점업 S1 2035(천명) | 1,844.457 | 1,844.500 | 0.050 | True |
| 26 | [정비판 주 산출물] 금융 및 보험업 S1 2035(천명) | 626.903 | 626.900 | 0.050 | True |
| 27 | [정비판 주 산출물] 농림어업 S1 2035(천명) | 1,374.148 | 1,374.100 | 0.050 | True |
| 28 | [정비판 주 산출물] 표 8 S1_증감_2024_2029(천명) | 171.620 | 171.600 | 0.050 | True |
| 29 | [정비판 주 산출물] 표 8 S1_증감_2029_2034(천명) | -52.209 | -52.200 | 0.050 | True |
| 30 | [정비판 주 산출물] 표 8 S1_증감_2024_2034(천명) | 119.411 | 119.400 | 0.050 | True |
| 31 | [정비판 주 산출물] 표 8 S1_증감_2024_2029_경로(천명) | 72.260 | 72.300 | 0.050 | True |
| 32 | [정비판 주 산출물] 표 8 S1_증감_2024_2034_경로(천명) | 20.051 | 20.100 | 0.050 | True |
| 33 | [이전 판(2026-10-08 판) 재현] 보존 파일 SHA-256 = 기록값 | f3346aee10c08f89 | f3346aee10c08f89 | 동일 | True |
| 34 | [이전 판(2026-10-08 판) 재현] 재현본 SHA-256 = 보존 파일 | f3346aee10c08f89 | f3346aee10c08f89 | 동일 | True |
| 35 | [이전 판(2026-10-08 판) 재현] 산업값 1,800개: 반올림 후 최대 절... | 0.000 | 0.000 | 0.050 | True |
| 36 | [이전 판(2026-10-08 판) 재현] 시나리오 합계 50개: 반올림 후 최대 ... | 0.000 | 0.000 | 0.050 | True |
| 37 | [이전 판(2026-10-08 판) 재현] 전산업 S1 2035(천명) | 29,040.139 | 29,040.100 | 0.050 | True |
| 38 | [이전 판(2026-10-08 판) 재현] 전산업 S2 2035(천명) | 28,988.941 | 28,988.900 | 0.050 | True |
| 39 | [이전 판(2026-10-08 판) 재현] 전산업 S3 2035(천명) | 29,079.629 | 29,079.600 | 0.050 | True |
| 40 | [이전 판(2026-10-08 판) 재현] 전산업 S4 2035(천명) | 29,110.763 | 29,110.800 | 0.050 | True |
| 41 | [이전 판(2026-10-08 판) 재현] 전산업 S5 2035(천명) | 29,046.214 | 29,046.200 | 0.050 | True |
| 42 | [최초 판(v0) 재현] 게시 파일 SHA-256 = 기록값 | 31c1c9c5dfadd831 | 31c1c9c5dfadd831 | 동일 | True |
| 43 | [최초 판(v0) 재현] 재현본 SHA-256 = 게시 파일 | 31c1c9c5dfadd831 | 31c1c9c5dfadd831 | 동일 | True |
| 44 | [최초 판(v0) 재현] 산업값 1,800개: 반올림 후 최대 절대오차(천명) | 0.000 | 0.000 | 0.050 | True |
| 45 | [최초 판(v0) 재현] 시나리오 합계 50개: 반올림 후 최대 절대오차(천명) | 0.000 | 0.000 | 0.050 | True |
| 46 | [최초 판(v0) 재현] 전산업 S1 2035(천명) | 36,284.817 | 36,284.800 | 0.050 | True |
검증 통과(47개 항목): 정비판 주 산출물이 7장 인용 수치와 일치하고, 이전 판(2026-10-08 판) 산출 파일과 최초 판(v0, 2026-08-04 게시) 파일을 바이트 단위까지 재현했다.