Files
Aislo/resources/knowledge/original/_pipeline/kbiz_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

228 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""제조노임 미공표 직종 추적표 · 직종코드 변천표 생성.
입력: 노임단가_제조업_중소기업중앙회/<적용일>/ 회차 md 의 「3. 직종별 조사노임」 표(값은 md 에서만 읽음) + 기본 폴더(2026-07-01).
출력: 노임단가_제조업_중소기업중앙회/미공표직종_추적.md · 직종코드_변천_대조표.md
사용: python kbiz_track_unpublished.py
- 미공표 직종 = 가장 최근 회차(2026-07-01 적용)에서 조사노임 칸이 「*」 인 직종.
- 산정값 = 마지막 공표값 × (최근 조사월 전체평균 ÷ 그 공표 조사월 전체평균) — 공표 조사월 단계별 증가율을 이어 곱한 값과 같음.
"""
import re
from decimal import ROUND_FLOOR, Decimal
from pathlib import Path
from kbiz_verify import BASE, load_rounds
TARGET_ROUND = "2026-07-01"
MONTH_KEY = lambda s: (int(s.split(".")[0]), int(s.split(".")[1])) # noqa: E731
def num(s):
return int(s.replace("*", "").replace(",", ""))
def series(rs):
"""(직종명 → {조사월: 값문자}) · 조사월 순서 · 조사월 → 전체평균 문자 · 조사월 → (전체평균을 실은 회차들)."""
val, avg, avg_src = {}, {}, {}
own = {}
for lab, r in rs.items():
for name, (code, vs) in r["rows"].items():
for k, (m, v) in enumerate(zip(r["months"], vs)):
if k == 0:
own[(name, m)] = v # 그 조사월을 처음 낸 회차의 값(표기 그대로)
val.setdefault(name, {}).setdefault(m, v)
a = [x for x in (r["avg"] or []) if x != "-"]
for m, x in zip(r["months"], a):
avg.setdefault(m, x)
avg_src.setdefault(m, []).append((lab, x))
for (name, m), v in own.items():
val[name][m] = v
months = sorted({m for n in val for m in val[n]}, key=MONTH_KEY)
return val, months, avg, avg_src
def mlabel(m):
y, mm = m.split(".")
return f"{y}.{int(mm)}"
def build_track(rs):
val, months, avg, avg_src = series(rs)
last = rs[TARGET_ROUND]
latest_month = last["months"][0]
un = [n for n, (c, v) in last["rows"].items() if v[0].replace("*", "").strip() in ("", "-")]
un.sort(key=lambda n: last["rows"][n][0])
lines = []
A = lines.append
A("# 제조노임 미공표 직종 추적 — 마지막 공표값 · 전체 평균 증가율 이음 (참고 계산)\n")
A(
"> 지식DB 근거 문서 — 값을 확정하지 않음. 원 미만 처리 · 어느 시기 값으로 이을지는 사용자 협의로 정함. "
"마스터 인력 「값」 은 5절 「단계마다 버림(채택)」 열(사용자 확정 · 건설노임과 같은 방식).\n"
"> 생성: `original/_pipeline/kbiz_track_unpublished.py` (회차 md 에서 값을 읽음)."
)
A("\n## 1. 원문 규칙\n")
A(
"- **중소기업중앙회 「중소제조업 직종별 임금조사 보고서」 에는 조사되지 않은(`*`) 직종의 노임을 어떻게 적용하라는 규정이 없음.** "
"2020 상반기 ~ 2026 상반기 12 회차 원문(`<적용일>/2. 조사결과 요약.md` 「조사노임 적용시 유의사항」 · `3. 직종별 조사노임.md` 표 밑 「주」)을 모두 찾아 확인함."
)
A("- 원문에 있는 것은 기호 뜻 뿐 (문장 그대로):")
A(
" - 2020.7 ~ 2023.7 적용 회차 「조사노임 적용시 유의사항」: 「‘*’ 표시는 조사업체가 3개 미만이거나 표본수가 적고 변동계수가 커 신뢰성을 확보하기 어려운 일부 직종으로 이번 조사결과 공표에서는 제외하며, ‘**’ 표시는 5개 미만인 직종으로 활용시 주의 필요함」"
)
A(
" - 2024.1 ~ 2026.7 적용 회차 「특수기호의 정의」: 「‘*’ 표시는 조사업체가 3개 미만이거나 표본수가 적은 경우, 또는 변동계수가 커 신뢰성을 확보하기 어려운 직종으로 ‘공표 제외’」 · 「‘**’ 표시는 응답기업 5개 미만인 직종으로 활용시 주의가 필요함」"
)
A(
" - 조사노임 표 밑 「주2」 (전 회차): 「‘-’표시는 조사되지 않음, ‘*’표시는 조사업체가 3개 미만이거나 표본수가 적고 변동계수가 커지는 등 신뢰성을 확인하기 어려운 직종, ‘**’표시는 5개 미만을 나타내는 것으로 활용시 주의 필요함」"
)
A(
" - 「주3」: 「(5~7)식품가공원, (20~21)일반화학원, (23~25)배합원, (125~126)특수차운전원의 통합단가는 2019년까지만 발표하고 2020년부터는 발표하지 아니함」"
)
A(
"- 건설업 임금실태조사(대한건설협회) 임금적용요령 4-라 의 「전후 시기 값 × 전체 평균 증가율」 같은 규정은 이 보고서에 **없음**. "
"아래 산정값은 건설 쪽 방식을 같은 모양으로 대입해 본 계산이며 원문 근거가 없음 — 사용자 확정으로 마스터에 반영(5절 버림 열)."
)
A(
"- 산정식(마지막 공표 조사월 L → "
f"{mlabel(latest_month)}): **산정값 = L 의 공표값 × Π(조사월마다 전체 평균 ÷ 앞 조사월 평균)** = "
f"{mlabel(latest_month)} 전체 평균 ÷ L 전체 평균 (가운데 항이 지워짐). 「원 미만 처리」 규정 없음 → 처리 없는 값과 조사월 단계마다 버린 값을 5절에 함께 적음."
)
A(
"- 「전체 평균 조사노임」 = 「전체평균」 줄(조사노임표 맨 끝) · 조사결과 요약 「전체 조사직종(129개)의 평균 조사노임(일급)」 과 같음. "
"원문 주: 각 직종별 평균은 단순평균값, 전체 조사직종의 평균은 가중평균."
)
A("\n## 2. 조사월별 전체 조사직종 평균 조사노임 (원문 인용)\n")
A("| 조사월 | 전체 평균(원) | 실린 회차(적용일) | 회차 사이 일치 |")
A("|---|---|---|---|")
for m in months:
srcs = avg_src.get(m, [])
same = len({x for _, x in srcs}) == 1
A(
f"| {mlabel(m)} | {avg[m]} | {' · '.join(l for l, _ in srcs)} | {'같음' if same else '다름 ' + str(srcs)} |"
)
A(
"\n- 표는 각 회차 「3. 직종별 조사노임」 표 맨 끝 「전체평균」 줄(회차마다 조사월 3 칸). "
"같은 조사월이 여러 회차에 실려 있어 회차 사이 일치를 확인함. 직종수: 2020.3 ~ 2020.8 조사는 130 직종, 2021.3 조사부터 129 직종(90.자동차정비원 제외) — 이음 계열이 바뀌는 자리에서도 회차 사이 같은 조사월 평균은 같은 값으로 실려 있음."
)
A(f"\n## 3. {len(un)} 직종 공표 이력 (원문 md 인용 · `*` = 미공표 · `-` = 조사되지 않음)\n")
hdr = "| 직종 (최근 판 코드) | " + " | ".join(mlabel(m) for m in months) + " |"
A(hdr)
A("|---|" + "---|" * len(months))
for n in un:
code = last["rows"][n][0]
A(f"| {code} {n} | " + " | ".join(val[n].get(m, "") for m in months) + " |")
A(
"\n- 값 출처: 각 조사월을 처음 낸 회차 폴더의 「3. 직종별 조사노임.md」 (표기 `**` 는 그 회차 표기 그대로). 직종 코드는 2024.10 적용 회차에서 재정렬됨 — 「직종코드_변천_대조표.md」 참조. "
"같은 조사월 값이 여러 회차에 실려 있으면 숫자는 모두 같음(`**` 표기만 회차에 따라 다를 수 있음) — 회차 사이 대조로 확인함."
)
A("\n## 4. 이음 단계 (조사월 전체 평균 ÷ 앞 조사월 평균)\n")
A("| 앞 → 뒤 | 분자(뒤) | 분모(앞) | 증가율 |")
A("|---|---|---|---|")
step = {}
for a, b in zip(months, months[1:]):
r = num(avg[b]) / num(avg[a])
step[b] = r
A(f"| {mlabel(a)}{mlabel(b)} | {avg[b]} | {avg[a]} | {r:.6f} |")
A(
"\n- 조사월 간격: 반기(3월 · 8월 조사) → 2024년은 연 1회(2024.6) → 2025.3 부터 다시 반기. "
"원문 연혁: 「2024. 5 작성주기 변경(연2회 → 연1회)」 · 「2025. 1 작성주기 변경(연1회 → 연2회)」. "
"이음은 실린 조사월끼리 곱함(2023.8 → 2024.6 은 두 조사월 평균의 비)."
)
A(f"\n## 5. {mlabel(latest_month)} 조사(2026-07-01 적용) 산정값\n")
A(
"| 직종 | 마지막 공표 조사월 | 그 값(원) | 이어 곱한 증가율 | 산정값(처리 없음) | 단계마다 버림(채택) | 비고 |"
)
A("|---|---|---|---|---|---|---|")
rows = []
for n in un:
code = last["rows"][n][0]
pub = [(m, val[n][m]) for m in months if val[n].get(m, "*") not in ("*", "-", "")]
m, v = pub[-1]
i = months.index(m)
mult, floor = 1.0, Decimal(num(v))
for a, b in zip(months[i:], months[i + 1 :]):
mult *= step[b]
floor = (floor * num(avg[b]) / num(avg[a])).to_integral_value(rounding=ROUND_FLOOR)
est = num(v) * mult
ratio = num(avg[latest_month]) / num(avg[m])
assert abs(mult - ratio) < 1e-9
note = "값에 `**`(5개 미만) 표기" if "**" in v else ""
rows.append((code, n, m, v, mult, est, int(floor), note))
A(f"| {code} {n} | {mlabel(m)} | {v} | {mult:.6f} | {est:,.2f} | {int(floor):,} | {note} |")
A(
"\n- 「단계마다 버림(채택)」 = 조사월 단계마다 원 미만을 버리며 이은 값(건설노임과 같은 방식) — 마스터 인력 「값」 은 이 열의 원 단위 정수."
"\n- 「산정값(처리 없음)」 = 버림 없이 이은 값 — 견주기용으로만 둠."
"\n- 「마지막 공표」 는 `**`(응답업체 5개 미만 · 활용시 주의) 표기가 붙은 값도 공표값으로 보아 셈 — `*` · `-` 만 값 없음."
"\n- 「그 값」 은 마지막으로 값이 실린 조사월의 공표 표기 그대로(`**` 포함). 산정값은 `**` 를 뗀 숫자에 곱함."
"\n- 마지막 공표값이 2020 이전인 직종은 없음 — 전 직종의 마지막 공표값이 2020.8 이후 회차에서 나와 2019 이전 회차는 받지 않음."
"\n- 최근 조사월 전체 평균 "
f"{avg[latest_month]}원 = 「조사결과 요약」 의 전체 조사직종(129개) 평균 조사노임."
)
return "\n".join(lines) + "\n", rows, un, months, val
def build_codes(rs):
labs = list(rs)
names = {}
for lab in labs:
for n, (c, v) in rs[lab]["rows"].items():
names.setdefault(n, {})[lab] = c
o = []
A = o.append
A("# 직종코드 변천·대조표 — 회차별 직종코드 (조사노임표 원문 그대로)\n")
A(
"> 지식DB 근거 문서. 생성: `original/_pipeline/kbiz_track_unpublished.py` — 각 회차 「3. 직종별 조사노임」 표의 「직종코드 및 직종명」 칸에서 읽음."
)
A(
"\n## 1. 원문에 실린 대조표 (각 회차 `3. 직종별 조사노임.md` 안 「5. 이전 직종코드·직종명으로 새 직종명 찾기」)\n"
)
A(
"- 모든 회차(2020.7 ~ 2026.7 적용)가 「기존 직종코드 및 직종명(~2018)」 → 「새로운 직종코드 및 직종명(2019~)」 표(124 줄)를 싣고 있음. "
"2020.7 ~ 2024.1 적용 회차 8 개는 표가 같고, 2024.10 · 2025.7 · 2026.1 · 2026.7 적용 회차 4 개는 「새로운 직종코드」 칸이 재정렬 뒤 코드로 바뀐 판(두 판의 다른 줄 144 · 뒤 4 회차끼리는 같음 — 회차 사이 대조로 확인). "
"2020.7 · 2021.1 적용 회차 게시글에는 같은 표의 HWP(`이전_직종명으로_새_직종명_찾기.hwp` → 같은 이름 md) 가 첨부돼 있음."
)
A(
"- 이 HWP 첨부의 「34.유리절단및재단원」 줄은 「조사보류」 로 적혀 있고 PDF 본문 표는 「130.유리절단및재단원」 — 원문 그대로 둘 다 옮김."
)
A("\n## 2. 회차 사이 직종코드가 바뀐 자리\n")
prev = None
for lab in labs:
if prev:
ch = [
n
for n in rs[lab]["rows"]
if n in rs[prev]["rows"] and rs[lab]["rows"][n][0] != rs[prev]["rows"][n][0]
]
drop = [n for n in rs[prev]["rows"] if n not in rs[lab]["rows"]]
add = [n for n in rs[lab]["rows"] if n not in rs[prev]["rows"]]
if ch or drop or add:
A(
f"- {prev}{lab}: 코드 바뀐 직종 {len(ch)} · 빠진 직종 {len(drop)}{(' (' + ', '.join(f'{rs[prev]["rows"][n][0]}.{n}' for n in drop) + ')') if drop else ''} · 새 직종 {len(add)}"
)
prev = lab
A(
"- 코드가 바뀐 것은 2024.1 → 2024.10 적용 회차(원문 연혁 「2024. 5 … 조사직종 재정렬」) 한 번 뿐. 재정렬 전 코드 → 뒤 코드는 2024.10 적용 회차부터 1 절의 표에 반영돼 있고, 아래 3절은 회차별 조사노임표에서 이름으로 맞춘 대조."
)
A("\n## 3. 직종명 × 회차별 코드\n")
A("| 직종명 | " + " | ".join(labs) + " |")
A("|---|" + "---|" * len(labs))
key = lambda n: rs[labs[-1]]["rows"].get(n, rs[labs[0]]["rows"].get(n, (9999,)))[0] # noqa: E731
for n in sorted(names, key=key):
A(f"| {n} | " + " | ".join(str(names[n].get(l, "")) for l in labs) + " |")
A(
"\n- 빈 칸 = 그 회차 조사노임표에 없음(90.자동차정비원은 2021.7 적용 회차부터 조사 대상에서 제외 — 원문 각주 「2년 연속 무응답으로 2021년 상반기 조사부터 조사 대상에서 제외」). "
"2024.1 적용 회차까지 130.유리절단및재단원 · 2024.10 적용 회차부터 17."
)
return "\n".join(o) + "\n"
if __name__ == "__main__":
rs = load_rounds()
md, rows, un, months, val = build_track(rs)
(BASE / "미공표직종_추적.md").write_text(md, encoding="utf-8")
(BASE / "직종코드_변천_대조표.md").write_text(build_codes(rs), encoding="utf-8")
print("미공표", len(un), "직종 · 조사월", len(months))
for r in rows:
print(r[0], r[1], r[2], r[3], f"{r[4]:.6f}", f"{r[5]:,.2f}")