Files
Aislo/resources/knowledge/original/_pipeline/cak_track_unpublished.py
T
eomsangdonandClaude Opus 5 6fccd9ed96 knowledge(마스터): 제조노임 미공표 16 직종 산정 반영 — 조사월마다 원 미만 버림
- 값 = 마지막 공표 조사월 값 × 조사월별 전체 평균 증가율을 이어 곱하되 곱할 때마다 원 미만 버림(건설노임 4-라 방식 준용 · 제조 원문에 규정 없음)
- 상태 「미공표」 → 「산정」 · 비고 「산정: <조사월> <값>원 × 평균 증가율(원 미만 버림) — 건설노임 4-라 방식 준용 · 제조 원문에 규정 없음」
- 빌더 build_인력_노임.py 에 estimate_make 추가 — 다시 돌려도 같은 값 · 추적 md 5절 버림 열과 기계 대조
- 구분 머리 제조업 「원천」 에 옛 회차 11 개 「3. 직종별 조사노임.md」 와 미공표직종_추적.md 를 역할 「산정근거」 로 더함
- kbiz_track_unpublished.py 에 「단계마다 버림(채택)」 열 추가 · cak_track_unpublished.py 도 같은 말로 맞춤(건설 md 는 그대로 다시 나옴)
- 시험 — 인력 상태 집합에서 「미공표」 빠짐 · 제조 산정 값 한 줄 대조

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
2026-09-20 18:47:53 +09:00

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