# -*- 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("- 「산정값(처리 없음)」 = 버림 없이 이은 값 — 견주기용으로만 둠.") 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()