- 대한건설협회 임금실태조사 보고서 13회(2020.1 · 2020.9 · 2021.1 · 2021.9 ~ 2026.9) 원문 보관 — 2020.9 · 2021.1 은 게시판에 HWP 만 있어 HWP 로 보관 - 회차마다 전체본 md + 장별 md 4개(쪽 표기 · 표 좌표 재구성) — _pipeline/cak_report_md.py · cak_hwp_md.py - 표 안 수 기계 대조(cak_verify.py) — md 대 PDF 이상 0 · 회차 사이 충돌 1(3011 · 2020.1 원문 그대로) · 쪽 그림 대조 - 미공표직종_추적.md — 원문 4-라 산정 근거(마지막 공표값 · 전체직종 평균 · 이어 곱한 증가율) · ESTX 견줌 14 중 12 일치 - 마스터 미변경 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
266 lines
11 KiB
Python
266 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""건설노임 미공표 직종 추적표 생성 — 원문 4-라(전후 시기 값 × 그간의 전체 평균시중노임단가 증가율).
|
|
|
|
입력: 노임단가_건설업_대한건설협회/<적용일>/ 회차 md(개별직종 표 · 쪽 4 평균임금 표) — 값은 md 에서만 읽음.
|
|
출력: 노임단가_건설업_대한건설협회/미공표직종_추적.md
|
|
사용: python cak_track_unpublished.py
|
|
"""
|
|
|
|
import math
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from cak_verify import BASE, md_rows
|
|
|
|
JOBS = [
|
|
"1032",
|
|
"1052",
|
|
"1053",
|
|
"1054",
|
|
"1055",
|
|
"1059",
|
|
"1064",
|
|
"3001",
|
|
"3002",
|
|
"3009",
|
|
"3010",
|
|
"3013",
|
|
"3014",
|
|
"3015",
|
|
]
|
|
TARGET = "2026.1.1"
|
|
ESTX = { # ref/_비교_estx.md 7절 — 다산소프트(ESTX) 값
|
|
"1032": 211645,
|
|
"1052": 244498,
|
|
"1053": 215165,
|
|
"1054": 209441,
|
|
"1055": 208506,
|
|
"1059": 338645,
|
|
"1064": 217174,
|
|
"3001": 516565,
|
|
"3002": 322923,
|
|
"3009": 297221,
|
|
"3010": 293753,
|
|
"3013": 357905,
|
|
"3014": 374529,
|
|
"3015": 463729,
|
|
}
|
|
|
|
|
|
def dkey(d): # "2024.9.1" → (2024, 9, 1)
|
|
return tuple(int(x) for x in d.split("."))
|
|
|
|
|
|
def load():
|
|
series, src = {}, {}
|
|
for d in sorted(
|
|
x for x in BASE.iterdir() if x.is_dir() and re.fullmatch(r"\d{4}-\d\d-\d\d", x.name)
|
|
):
|
|
for c, (n, f, vals) in md_rows(d).items():
|
|
if c in JOBS:
|
|
for k, v in vals.items():
|
|
if v != "-" and k not in series.setdefault(c, {}):
|
|
series[c][k] = int(v.replace(",", "").strip("(※)"))
|
|
src[(c, k)] = d.name
|
|
names = {}
|
|
for c, (n, f, v) in md_rows(BASE / "2026-09-01").items():
|
|
names[c] = n
|
|
# 2026.1.1 이전 값(현행 폴더 md) — 2026상반기 폴더 md 는 네 공표일(2026.1 · 2025.9 · 2025.1 · 2024.9)
|
|
root = BASE / "3. 개별직종 노임단가.md"
|
|
dates = None
|
|
for ln in root.read_text(encoding="utf-8").splitlines():
|
|
if ln.startswith("| 직종코드"):
|
|
dates = [c.strip() for c in ln.strip("|").split("|")][3:]
|
|
m = re.match(r"\| (\d{4}) \| .*? \| \** \| (.*) \|$", ln) or re.match(
|
|
r"\| (\d{4}) \| .*? \| \| (.*) \|$", ln
|
|
)
|
|
if m and dates and m.group(1) in JOBS:
|
|
for k, v in zip(dates, [c.strip() for c in m.group(2).split("|")]):
|
|
if v != "-" and k not in series.setdefault(m.group(1), {}):
|
|
series[m.group(1)][k] = int(v.replace(",", ""))
|
|
src[(m.group(1), k)] = "(2026상반기 폴더)"
|
|
return series, src, names
|
|
|
|
|
|
AVG = { # 「1. 조사개요」 8. 평균임금현황 (쪽 3~4) — 전체직종. (직종수, 값)
|
|
"2020.1.1": {123: 222803},
|
|
"2020.9.1": {123: 227923, 127: 226947},
|
|
"2021.1.1": {123: 231779, 127: 230798},
|
|
"2021.9.1": {127: 235815},
|
|
"2022.1.1": {127: 242931},
|
|
"2022.9.1": {127: 248819},
|
|
"2023.1.1": {127: 255426},
|
|
"2023.9.1": {127: 265516},
|
|
"2024.1.1": {127: 270789},
|
|
"2024.9.1": {127: 274286},
|
|
"2025.1.1": {127: 276020, 132: 276011},
|
|
"2025.9.1": {127: 278861, 132: 278832},
|
|
"2026.1.1": {127: 279989, 132: 279988},
|
|
"2026.9.1": {127: 282859, 132: 282737},
|
|
}
|
|
ORDER = list(AVG)
|
|
|
|
|
|
# 잇는 계열: 2020.1→2020.9 = 123종 · 2020.9→2025.1 = 127종 · 2025.1 이후 = 132종 (ESTX 와 같은 이음)
|
|
def step(a, b, mixed=False):
|
|
"""(직종수, 분자, 분모). mixed=True → 2020.1(123종)→2020.9 를 127종 값으로 이음(ESTX 방식)."""
|
|
if a == "2020.1.1":
|
|
return (127, AVG[b][127], AVG[a][123]) if mixed else (123, AVG[b][123], AVG[a][123])
|
|
bs = 132 if a in ("2025.1.1", "2025.9.1", "2026.1.1") else 127
|
|
return bs, AVG[b][bs], AVG[a][bs]
|
|
|
|
|
|
def chain(last, target=TARGET, mixed=False):
|
|
i, j = ORDER.index(last), ORDER.index(target)
|
|
prod, steps = 1.0, []
|
|
for a, b in zip(ORDER[i:j], ORDER[i + 1 : j + 1]):
|
|
bs, num, den = step(a, b, mixed)
|
|
prod *= num / den
|
|
steps.append((a, b, bs, num, den))
|
|
return prod, steps
|
|
|
|
|
|
def floor_chain(v, last, target=TARGET, mixed=False):
|
|
i, j = ORDER.index(last), ORDER.index(target)
|
|
for a, b in zip(ORDER[i:j], ORDER[i + 1 : j + 1]):
|
|
bs, num, den = step(a, b, mixed)
|
|
v = math.floor(v * num / den)
|
|
return v
|
|
|
|
|
|
def main():
|
|
series, src, names = load()
|
|
out = []
|
|
W = out.append
|
|
W("# 건설노임 미공표 직종 추적 — 원문 4-라 산정 근거")
|
|
W("")
|
|
W(
|
|
"> 지식DB 근거 문서 — 값을 확정하지 않음. 원 미만 처리 · 어느 직종수 계열로 잇는지는 사용자 협의로 정함. 마스터(`resources/master_data`)에는 반영하지 않음."
|
|
)
|
|
W("> 생성: `original/_pipeline/cak_track_unpublished.py` (회차 md 에서 값을 읽음).")
|
|
W("")
|
|
W("## 1. 원문 규칙")
|
|
W("")
|
|
W(
|
|
"- 근거: 임금적용요령 「4. 참고사항」 라 (`<적용일>/2. 임금적용요령.md` 쪽 6): "
|
|
"「조사기관이 조사·공표한 당해직종의 시중노임단가가 없는 년도(또는 시기)의 경우에는 전후년도(또는 시기)의 당해직종의 시중노임단가에 "
|
|
"그간의 전체 평균시중노임단가 증가율을 적용하여 해당년도(또는 시기)의 당해직종의 노임단가를 산정할 수 있음.」"
|
|
)
|
|
W("- 직종번호 앞 「**」 = 조사되지 않은 직종 — 「Ⅱ. 임금적용요령」 참조(= 위 라).")
|
|
W("- 「전체 평균시중노임단가」 = 「1. 조사개요」 8. 평균임금현황의 「전체직종」 평균임금.")
|
|
W(
|
|
"- 산정식(마지막 공표 시기 L → 2026.1.1): **산정값 = L 의 공표값 × Π(공표일마다 전체직종 평균 ÷ 앞 공표일 평균)**. 이어 곱하면 가운데 항이 지워져 ‘2026.1.1 평균 ÷ L 평균’과 같음(같은 직종수 계열일 때)."
|
|
)
|
|
W(
|
|
"- 원문에 원 미만 처리 · 직종수 계열 · 이음 단위 규정 없음 → 아래 값은 **처리 없이 소수 둘째 자리까지** 적음."
|
|
)
|
|
W("")
|
|
W("## 2. 전체직종 평균임금 (원문 인용)")
|
|
W("")
|
|
W("| 공표일 | 123종 | 127종 | 132종 |")
|
|
W("|---|---|---|---|")
|
|
for d in ORDER:
|
|
a = AVG[d]
|
|
W(f"| {d} | " + " | ".join(f"{a[k]:,}" if k in a else "" for k in (123, 127, 132)) + " |")
|
|
W("")
|
|
W(
|
|
"- 원문 위치: `2026-09-01/1. 조사개요.md` 쪽 4 「평균임금」 표(2020.1 이후 전 공표일) · 같은 값이 각 회차 쪽 4 에도 실려 있어 회차 사이 일치를 확인함(2020.1 은 `2020-01-01` 쪽 3 · 4)."
|
|
)
|
|
W(
|
|
"- 표는 원문 쪽 4 「평균임금」 표 그대로 — 직종수가 바뀐 공표일은 원문이 두 줄로 병기(2018.9 이후 123종 · 117종 · 127종 · 132종)."
|
|
)
|
|
W(
|
|
"- 이음 계열: 2020.1→2020.9 = 123종끼리 · 2020.9→2025.1 = 127종끼리 · 2025.1 이후 = 132종끼리(2024.9→2025.1 은 132종 2024.9 값이 없어 127종)."
|
|
)
|
|
W("")
|
|
W("## 3. 14 직종 공표 이력 (원문 md 인용 · `-` = 미공표)")
|
|
W("")
|
|
dates = [d for d in ORDER]
|
|
W("| 직종 | " + " | ".join(dates) + " |")
|
|
W("|---|" + "---|" * len(dates))
|
|
for c in JOBS:
|
|
cells = []
|
|
for d in dates:
|
|
v = series.get(c, {}).get(d)
|
|
cells.append(f"{v:,}" if v else "-")
|
|
W(f"| {c} {names.get(c, '')} | " + " | ".join(cells) + " |")
|
|
W("")
|
|
W(
|
|
"- 값 출처: 각 공표일 회차 폴더의 「3. 개별직종 노임단가.md」 (2026.1 은 폴더 바로 아래 md). 같은 값이 여러 회차에 실려 있으면 회차 사이 일치를 `cak_verify.py` 로 확인함."
|
|
)
|
|
W("")
|
|
W("## 4. 이음 단계 (전체직종 평균 ÷ 앞 공표일 평균)")
|
|
W("")
|
|
W("| 앞 → 뒤 | 직종수 | 분자(뒤) | 분모(앞) | 증가율 |")
|
|
W("|---|---|---|---|---|")
|
|
for a, b in zip(ORDER[:-1], ORDER[1:]):
|
|
bs, num, den = step(a, b)
|
|
W(f"| {a} → {b} | {bs} | {num:,} | {den:,} | {num / den:.6f} |")
|
|
W("")
|
|
W("## 5. 2026.1.1 산정값")
|
|
W("")
|
|
W(
|
|
"| 직종 | 마지막 공표 | 그 값(원) | 이어 곱한 증가율 | 산정값(처리 없음) | ESTX 값 | 단계마다 버림 | 비고 |"
|
|
)
|
|
W("|---|---|---|---|---|---|---|---|")
|
|
notes = []
|
|
for c in JOBS:
|
|
s = series[c]
|
|
ds = [d for d in ORDER if d in s and dkey(d) <= dkey(TARGET)]
|
|
last = ds[-1]
|
|
v = s[last]
|
|
prod, _ = chain(last)
|
|
fl = floor_chain(v, last)
|
|
e = ESTX[c]
|
|
note = "ESTX 와 같음" if fl == e else ""
|
|
if fl != e:
|
|
flm = floor_chain(v, last, mixed=True)
|
|
note = (
|
|
f"ESTX 와 다름({fl - e:+,}) — 2020.1(123종) → 2020.9 를 127종 값 226,947 로 이으면 {flm:,} "
|
|
+ ("= ESTX" if flm == e else "")
|
|
)
|
|
W(
|
|
f"| {c} {names.get(c, '')} | {last} | {v:,} | {prod:.6f} | {v * prod:,.2f} | {e:,} | {fl:,} | {note} |"
|
|
)
|
|
W("")
|
|
W(
|
|
"- 「단계마다 버림」 = 공표일 단계마다 원 미만 버림하며 이은 값 — 다산소프트(ESTX) 방식과 견주기 위한 열이며, 원문에 처리 규정이 없으므로 채택하지 않음."
|
|
)
|
|
W(
|
|
"- ESTX 값 = `resources/master_data/ref/_비교_estx.md` 7절 표(다산소프트 EST 2026-07 자재 · 노임 배포본)."
|
|
)
|
|
W(
|
|
"- 14 직종 가운데 앞 공표값이 2024.9 이후에 있는 5 직종(연마공 · 선원 · 플랜트특수용접공 · 도편수 · 한식와공편수) 외 9 직종은 더 오래된 회차(2020.1 ~ 2024.1)에서 마지막 공표값을 찾음."
|
|
)
|
|
W("")
|
|
W("## 6. 2026.9.1 공표값이 있는 직종 (참고)")
|
|
W("")
|
|
W(
|
|
"2026.9.1 회차(적용 2026-09-01)가 이미 게시됨 — 4 직종에 2026.9.1 값이 실려 있음. 2026.1.1 은 이 값의 앞 시기이므로 원문 4-라 「전후 시기」에 해당."
|
|
)
|
|
W("")
|
|
W(
|
|
"| 직종 | 2026.9.1 공표값 | 뒤 값에서 거꾸로 산정한 2026.1.1 (처리 없음) | 앞 값(마지막 공표)에서 이은 2026.1.1 |"
|
|
)
|
|
W("|---|---|---|---|")
|
|
for c in JOBS:
|
|
v9 = series[c].get("2026.9.1")
|
|
if v9:
|
|
bs = 132
|
|
back = v9 * AVG["2026.1.1"][bs] / AVG["2026.9.1"][bs]
|
|
ds = [d for d in ORDER if d in series[c] and dkey(d) <= dkey(TARGET)]
|
|
last = ds[-1]
|
|
prod, _ = chain(last)
|
|
W(
|
|
f"| {c} {names.get(c, '')} | {v9:,} | {back:,.2f} (132종 평균 279,988 ÷ 282,737) | {series[c][last] * prod:,.2f} |"
|
|
)
|
|
W("")
|
|
W("- 두 값이 다르면 어느 쪽을 쓸지는 사용자 협의로 정함(지식DB 는 후보 병기).")
|
|
W("")
|
|
(BASE / "미공표직종_추적.md").write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
print("wrote", BASE / "미공표직종_추적.md")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|