chore(조사): 338 외부 일위대가 맞춤 설계 — 연결된 단가산출 블록 → 우리 로직 + 옵션 → 일위대가 구성 · 창 셋 · 시험 · 기준 수치 + 실험 자료 사본
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""실패 까닭 갈래 — 1순위(엄격) 실패 줄마다 주 갈래 하나(첫 칸) + 곁 갈래.
|
||||
이름만(씨앗 끔) 실패 · 지금 실패를 따로 봄(같은 줄이라도 까닭이 다르면 둘째 표에서 고침)."""
|
||||
|
||||
A = "같은 뜻 다른 말"
|
||||
B = "줄임말"
|
||||
C = "낱말 차례"
|
||||
D = "괄호 속 규격"
|
||||
E = "비율 표기(인력100%·9:1)"
|
||||
F = "단위 표기(hr·공/㎥·개소)"
|
||||
G = "장비 · 노임 이름 줄"
|
||||
H = "책 갈래(산림↔건설)"
|
||||
I = "우리 로직이 더 잘게/굵게"
|
||||
J = "엑셀 복합 호표"
|
||||
K = "근거 조항 비어 있음 · 엉뚱"
|
||||
LL = "품셈 판 차이(번호 바뀜)"
|
||||
M = "그 밖(오타 · 비슷한 이름 함정 · 접힘)"
|
||||
ORDER = [A, B, C, D, E, F, G, H, I, J, K, LL, M]
|
||||
|
||||
NAME = { # 이름만(씨앗 끔) 실패 까닭
|
||||
"N000": [B, J, H], "N013": [B, J], "N024": [B, J], "N030": [B, J], "N043": [B, J],
|
||||
"S052": [B, J], "S053": [B, J], "S069": [B, J], "S075": [B, J],
|
||||
"N001": [H], "N025": [H], "N014": [H], "N015": [H], "N032": [H], "N044": [H, F], "N046": [H, F],
|
||||
"S056": [H], "S057": [H, F], "S058": [H, D], "S054": [H, D], "S064": [H], "S070": [H], "S086": [H],
|
||||
"N007": [H, A], "N027": [H, A], "N028": [H, A, C], "N029": [H, A, C], "N031": [H, A],
|
||||
"S098": [H, A, C], "S099": [H, A, C], "N038": [H, A], "N040": [H, A], "N037": [H, M],
|
||||
"S084": [H, B], "Y001": [H], "Y009": [H], "Y021": [H], "Y029": [H],
|
||||
"N003": [I, C, M], "N019": [I, C, M], "N045": [I, C, M], "S055": [I, C, M], "S059": [I, C, M],
|
||||
"Y010": [I, C, M],
|
||||
"N005": [A, J, K, M], "N012": [A, J, K, M], "N036": [A, J, K, M], "S081": [A, J, K], "S091": [A, J, K],
|
||||
"Y015": [A, J],
|
||||
"N006": [A, LL], "Y024": [A, LL], "Y025": [A, LL],
|
||||
"N008": [A, E], "S076": [A, H], "N018": [A], "N022": [A, J], "S092": [A, J], "S096": [A],
|
||||
"N039": [A, J], "S061": [A, K], "S072": [A, M, F], "S083": [A, M, F], "Y016": [A, J],
|
||||
"Y017": [A, M], "Y023": [A, B], "Y027": [A, M], "Y032": [A], "Y033": [A, J],
|
||||
"N023": [D], "N042": [D], "S066": [D, J], "S082": [D], "N050": [J, D],
|
||||
"N035": [M], "N047": [M], "S067": [M], "Y000": [M], "Y020": [M], "Y028": [M],
|
||||
"Y008": [LL, H],
|
||||
"S085": [G, F], "Y005": [G, F],
|
||||
}
|
||||
|
||||
NOW = { # 지금(330 뒤) 실패 까닭
|
||||
"N001": [H], "N025": [H], "N015": [H], "S086": [H], "N032": [H], "N044": [H, F], "S057": [H, F],
|
||||
"N046": [H, F], "S056": [H], "N037": [H, M], "Y029": [H, M],
|
||||
"N003": [I, LL], "N019": [I, LL], "N045": [I, LL], "S055": [I, LL], "S059": [I, LL],
|
||||
"Y010": [M, I],
|
||||
"N005": [A, K, J], "N012": [A, K, J], "N036": [A, K, J], "S081": [A, K, J], "S091": [A, K, J],
|
||||
"Y015": [A, J],
|
||||
"N006": [A, LL], "Y024": [A, LL], "Y025": [A, LL], "S061": [A, K], "Y027": [A, M],
|
||||
"S062": [J, LL], "S066": [D, J], "S085": [G, F], "Y005": [G, F],
|
||||
"Y009": [M], "Y021": [M], "Y020": [M], "Y028": [M],
|
||||
}
|
||||
|
||||
NOTE = { # 표 예시 글(갈래별 셋)
|
||||
A: ["휠라설치 → 신축이음 채움재 설치", "구조물터파기(인력100%) → 굴착(인력/토사)", "고임돌 · 조약돌 채집 → 모래·자갈·약돌 채집(1위 궤도 자갈채집)"],
|
||||
B: ["레미콘타설(장비사용) → 레디믹스트콘크리트 타설(1위 펌프카 타설)", "배수관부설및접합(P.V.C) → PVC관 고무링 접합 및 부설", "B/H 90%(백호 = 굴착기)"],
|
||||
C: ["철근가공조립(현장) ↔ 철근 현장가공 및 조립", "큰돌찰붙임 ↔ 큰돌붙이기 찰붙이기", "(곁 갈래만 · 주 까닭 아님)"],
|
||||
D: ["큰돌쌓기(찰쌓기) → 1위 메쌓기(괄호 「찰쌓기」 버림)", "구조물터파기(토사) → 1위 터파기(기계) 암", "구조물터파기(암) → 1위 육상토사"],
|
||||
E: ["구조물터파기(인력100%)", "고임돌채집및운반(9:1)", "(곁 갈래만 — 비율 글은 지금도 낱말로 안 셈)"],
|
||||
F: ["대형브레이카 hr ↔ 시간", "강관동바리 공/㎥ ↔ 공㎥", "도로반사경 개소 ↔ 본(환산 「입력」)"],
|
||||
G: ["콘크리트 펌프차 41m [hr] → 기계 시간당 기계경비(기계 입력)", "크레인(타이어) 10톤 [시간]", "철골공 · 특별인부 [인] = 노임(우리에 없음)"],
|
||||
H: ["합판거푸집 → 같음 산림 12-4(엑셀은 건설 6-3-1)", "강관비계 · 강관동바리 → 같음 산림(엑셀 건설 2-7-1 · 2-6-1)", "큰돌메붙임 → 1위 건설 메붙임(굴착기+집게)(엑셀 산림 13-7-1)"],
|
||||
I: ["철근가공조립(현장) = 우리 현장가공 + 현장조립 둘", "잔토처리(인력) = 우리 굴착(인력) 입력 「잔토처리」에 묶임", "원형돌망태 조립설치 · 채움 = 우리 원형 돌망태 하나"],
|
||||
J: ["레미콘타설 + 굴착기 시간", "고임돌 = 채집 + 상차 + 운반", "도장(철재면2회) = 바탕 + 녹막이 + 유성 셋"],
|
||||
K: ["철골설치(출처 없음)", "조약돌채집운반(9:1)(출처 없음)", "고임돌 「1-10」 = 단위중량 인용(작업 조항 아님)"],
|
||||
LL: ["철근 「건설 6-2」 = 옛 한 조항 → 지금 6-2-2 · 6-2-3", "야면석채집 「건설 8-2 (2016 삭제)」", "영월 「건설 3-1-3 · 9-3 · 1-9-3 · 임도품셈 5-5」 옛 번호"],
|
||||
M: ["흙깍기(오타 깍/깎) → 후보 없음", "원형돌망태 → 1위 타원형(원형 ⊂ 타원형)", "유로폼 설치,해체 → 같은 이름 산림 · 건설 둘이 이름 후보에서 하나로 접힘"],
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
"""정답 표시 — 고유 로직 줄 100(북부청 임도 N000~N051 · 사방 S052~S099).
|
||||
|
||||
정답 = 엑셀 단가산출 블록의 출처 조항 · 직종 · 품 · 계산식이 같은 우리 로직(복합이면 주된 몫 · 여럿 가능).
|
||||
허용 = 같은 일의 다른 책(산림 ↔ 건설) 로직 · 복합의 곁 몫(느슨 지표에서만 맞음).
|
||||
부분 = 「우리에 없음」 줄의 가까운 조각(지표에 안 셈 · 참고).
|
||||
짐작 = 블록에 출처가 없거나 우리 로직이 일부만 덮어 사람 판단이 섞임.
|
||||
갈래 = 이름이 엇나간 까닭(실패 갈래 표에 씀).
|
||||
"""
|
||||
|
||||
CONCRETE = {
|
||||
"정답": ["GC000198"],
|
||||
"허용": ["GF000155"],
|
||||
"근거": "건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력)",
|
||||
"갈래": ["같은 뜻 다른 말", "엑셀 복합"],
|
||||
}
|
||||
REBAR = {
|
||||
"정답": ["GC000206", "GC000207"],
|
||||
"허용": ["GF000159"],
|
||||
"근거": "건설 6-2 Type 가공(철근공 0.67 · 보통 0.22) + 조립 — 우리 6-2-2 가공 · 6-2-3 조립 둘(복합) · 산림 12-3 GF000159 같은 일 · 이름 1위 GF000187 은 산림 12-18 복잡 고정 4.2인(다른 품)",
|
||||
"갈래": ["우리 로직이 더 잘게", "엑셀 조항이 윗마디", "비슷한 이름 엉뚱"],
|
||||
}
|
||||
PLY = {
|
||||
"정답": ["GC000211"],
|
||||
"허용": ["GF000160"],
|
||||
"근거": "건설 6-3-1 형틀목공 5 · 보통인부 2 ÷ 일 시공량 = GC000211 6-3-1 · 이름 같음 GF000160 은 산림 12-4",
|
||||
"갈래": ["같은 이름 다른 책"],
|
||||
}
|
||||
FORM_PATTERN = {
|
||||
"정답": ["GC000215"],
|
||||
"허용": ["GF000161"],
|
||||
"근거": "건설 6-3-4 형틀목공 0.07 · 보통 0.03 = GC000215 · 이름 같음 GF000161 은 산림 12-5",
|
||||
"갈래": ["같은 이름 다른 책"],
|
||||
}
|
||||
EURO = {
|
||||
"정답": ["GC000214"],
|
||||
"허용": ["GF000210"],
|
||||
"근거": "건설 6-3-3 형틀목공 4 · 보통 1 ÷ 35~40 = GC000214",
|
||||
"갈래": [],
|
||||
}
|
||||
WEDGE = {
|
||||
"정답": ["GF000211"],
|
||||
"허용": ["GF000212"],
|
||||
"근거": "채집(기계 90 · 인력 10% 보통인부 0.3인/㎥ = 산림 13-2-1 막자갈) + 상차 + 운반 — GF000211 은 인력 채집 몫만 · 출처 1-10 은 단위중량 인용 · 1위 GC000363 은 궤도 자갈(토목 4-6-1)",
|
||||
"짐작": True,
|
||||
"갈래": ["같은 뜻 다른 말", "엑셀 복합", "근거 조항 엉뚱", "비슷한 이름 엉뚱"],
|
||||
}
|
||||
PEBBLE = {
|
||||
**WEDGE,
|
||||
"허용": [],
|
||||
"근거": "채집 인력 10%(보통인부 0.3 · 0.6인/㎥ = 산림 13-2-1 막자갈 · 조약돌) + 상차 + 운반 — GF000211(부분) · 1위 GC000363 궤도 자갈",
|
||||
}
|
||||
FIELDSTONE = {
|
||||
"정답": ["GF000214"],
|
||||
"허용": [],
|
||||
"근거": "인력 10% 보통인부 0.17인/㎡(= 산림 13-2-4 뒷길이 35㎝) + 굴착기 채집 + 운반 — GF000214 은 인력 채집 몫(부분) · 건설 8-2 는 2016 삭제",
|
||||
"짐작": True,
|
||||
"갈래": ["엑셀 복합", "품셈 판 번호 바뀜"],
|
||||
}
|
||||
SCAFFOLD = {
|
||||
"정답": ["GC000022"],
|
||||
"허용": ["GF000188"],
|
||||
"근거": "건설 2-7-1 비계공 3 · 보통 1 ÷ 55㎡ = GC000022 강관비계(쌍줄) · 이름 같음 GF000188 은 산림 12-19(공㎥)",
|
||||
"갈래": ["같은 이름 다른 책", "단위 다름"],
|
||||
}
|
||||
SHORING = {
|
||||
"정답": ["GC000015"],
|
||||
"허용": ["GF000189"],
|
||||
"근거": "건설 2-6-1 형틀목공 3 · 보통 1 ÷ 50공㎥ = GC000015 강관 동바리(토목) · 이름 같음 GF000189 은 산림 12-20",
|
||||
"갈래": ["같은 이름 다른 책"],
|
||||
}
|
||||
PAVE = {
|
||||
"정답": ["GC000297"],
|
||||
"허용": ["GF000162"],
|
||||
"근거": "건설 1-6-2 포장공 4 · 보통 2 ÷ 100㎥(15% 이상 ×1.2) = GC000297 콘크리트 표층 인력포설 · 산림 12-6 GF000162(종단경사 입력) 같은 일",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
}
|
||||
SOIL_LEFT_HAND = {
|
||||
"정답": [],
|
||||
"허용": [],
|
||||
"부분": ["GC000083"],
|
||||
"근거": "보통인부 0.2인/㎥ = 건설 3-2-1 비고 잔토처리 — 우리 단독 로직 없음(GC000083 입력 「잔토처리」로 굴착과 묶임)",
|
||||
"갈래": ["우리 로직이 더 굵게"],
|
||||
}
|
||||
LABOR_ONLY = {
|
||||
"정답": [],
|
||||
"허용": [],
|
||||
"근거": "노임 한 줄(인 단위) — 로직 아님(인력 단가)",
|
||||
"갈래": ["노임 줄"],
|
||||
}
|
||||
BIG_STONE = {
|
||||
"정답": ["GF000213"],
|
||||
"허용": [],
|
||||
"근거": "산림 13-2-3 작업반장 0.026인/개 × 4개/㎡ × 75% × 50 · 40% + 굴착기 = GF000213 큰돌 채집(기계)(개 ↔ ㎡)",
|
||||
"갈래": ["단위 다름"],
|
||||
}
|
||||
BACKFILL_STONE = {
|
||||
"정답": ["GC000106"],
|
||||
"허용": ["GF000218"],
|
||||
"근거": "채집(9:1) · 운반 + 뒷채움 및 다짐 건설 3-4-4(소형장비) = GC000106(부분)",
|
||||
"짐작": True,
|
||||
"갈래": ["엑셀 복합", "같은 뜻 다른 말"],
|
||||
}
|
||||
BASE_STONE = {
|
||||
"정답": ["GC000110", "GF000168"],
|
||||
"허용": [],
|
||||
"근거": "채집 · 적사 · 운반 + 기초지정 건설 3-4-8 = GC000110(부분) · 산림 12-12(옛 번호) → 지금 12-25 기초잡석 GF000168",
|
||||
"짐작": True,
|
||||
"갈래": ["엑셀 복합", "품셈 판 번호 바뀜"],
|
||||
}
|
||||
DRAIN = {
|
||||
"정답": ["GC000404"],
|
||||
"허용": ["GF000176", "GF000179"],
|
||||
"근거": "건설 6-7-2 파형강관 배관공(수도) · 보통 ÷ 6 = GC000404 파형강관 부설 및 접합(본 ↔ M) · 산림 12-11-3 GF000176",
|
||||
"갈래": ["같은 뜻 다른 말", "단위 다름"],
|
||||
}
|
||||
DUMP = {
|
||||
"정답": ["GF000135", "GF000284"],
|
||||
"허용": [],
|
||||
"근거": "적사(굴착기 0.8) + 덤프 15t 운반 18.7㎞(출처 없음) = 산림 10-3-2 골재운반 잡석 · 10-12-1 덤프운반",
|
||||
"짐작": True,
|
||||
"갈래": ["근거 조항 비어 있음"],
|
||||
}
|
||||
|
||||
L = {
|
||||
"N000": CONCRETE,
|
||||
"N001": PLY,
|
||||
"N002": BASE_STONE,
|
||||
"N003": REBAR,
|
||||
"N004": FIELDSTONE,
|
||||
"N005": WEDGE,
|
||||
"N006": {
|
||||
"정답": ["GF000280"],
|
||||
"허용": ["GF000279"],
|
||||
"근거": "건설 1-2-7 인력운반 공식(575㎏/㎡ ÷ 25㎏ ÷ 42회) — 우리 건설 1-2-7 없음 · 산림 10-6-2 인력운반 돌(지게) 같은 일(㎡ ↔ ㎥)",
|
||||
"짐작": True,
|
||||
"갈래": ["같은 뜻 다른 말", "우리에 그 책 조항 없음"],
|
||||
},
|
||||
"N007": {
|
||||
"정답": ["GF000225"],
|
||||
"허용": ["GC000263"],
|
||||
"근거": "산림 13-5-2 석공 0.11 · 보통 0.04 + 굴착기 집게 = GF000225 돌붙임 찰붙임(장비)",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"N008": {
|
||||
"정답": ["GC000083"],
|
||||
"허용": ["GF000101"],
|
||||
"근거": "건설 3-2-1 특별인부 (3.6+2.7+2.2)/3 = GC000083 굴착(인력/토사)",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"N009": {
|
||||
"정답": ["GF000113"],
|
||||
"허용": [],
|
||||
"근거": "산림 9-14 인력품 보통인부 0.1인 = GF000113 되메우기(9-14-1)",
|
||||
"갈래": ["엑셀 조항이 윗마디"],
|
||||
},
|
||||
"N010": SOIL_LEFT_HAND,
|
||||
"N011": FIELDSTONE,
|
||||
"N012": WEDGE,
|
||||
"N013": CONCRETE,
|
||||
"N014": EURO,
|
||||
"N015": FORM_PATTERN,
|
||||
"N016": {
|
||||
"정답": ["GF000192"],
|
||||
"허용": [],
|
||||
"근거": "산림 12-22 재료비 + 설치비 5% = GF000192 P.V.C 파이프 설치(개소 ↔ M)",
|
||||
"갈래": ["단위 다름"],
|
||||
},
|
||||
"N017": {
|
||||
"정답": ["GC000226"],
|
||||
"허용": ["GF000201"],
|
||||
"근거": "건설 6-3-10 다웰바 형틀목공 0.043 = GC000226 · 산림 12-30-1 GF000201(D32 · L1000) 같은 일",
|
||||
"갈래": [],
|
||||
},
|
||||
"N018": {
|
||||
"정답": ["GC000227"],
|
||||
"허용": [],
|
||||
"근거": "건설 6-3-10 휠라(채움재) 형틀목공 0.029 = GC000227 신축이음 채움재 설치",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"N019": REBAR,
|
||||
"N020": BIG_STONE,
|
||||
"N021": {
|
||||
"정답": ["GF000226"],
|
||||
"허용": ["GC000260"],
|
||||
"근거": "산림 13-6-1 = GF000226 큰돌쌓기 메쌓기",
|
||||
"갈래": [],
|
||||
},
|
||||
"N022": BACKFILL_STONE,
|
||||
"N023": {
|
||||
"정답": ["GF000227"],
|
||||
"허용": [],
|
||||
"근거": "산림 13-6-2 = GF000227 큰돌쌓기 찰쌓기",
|
||||
"갈래": [],
|
||||
},
|
||||
"N024": CONCRETE,
|
||||
"N025": PLY,
|
||||
"N026": {
|
||||
"정답": ["GC000310"],
|
||||
"허용": [],
|
||||
"근거": "건설 1-8-3 = GC000310 도로반사경 설치(본 ↔ 개소)",
|
||||
"갈래": ["단위 다름"],
|
||||
},
|
||||
"N027": {
|
||||
"정답": ["GF000224"],
|
||||
"허용": ["GC000262"],
|
||||
"근거": "산림 13-5-2 석공 0.13 · 보통 0.04 = GF000224 돌붙임 메붙임(장비)",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"N028": {
|
||||
"정답": ["GF000229"],
|
||||
"허용": ["GC000262"],
|
||||
"근거": "산림 13-7-1 = GF000229 큰돌붙이기 메붙이기",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"N029": {
|
||||
"정답": ["GF000230"],
|
||||
"허용": ["GC000263"],
|
||||
"근거": "산림 13-7-2 = GF000230 큰돌붙이기 찰붙이기",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"N030": CONCRETE,
|
||||
"N031": {
|
||||
"정답": ["GF000222"],
|
||||
"허용": ["GC000261"],
|
||||
"근거": "산림 13-4-5 석공 0.09 · 보통 0.05 = GF000222 돌쌓기 찰쌓기(장비)",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"N032": {
|
||||
"정답": ["GC000111"],
|
||||
"허용": ["GF000124"],
|
||||
"근거": "건설 3-5-1 절토면 고르기 굴착기 1.0 = GC000111 · 씨앗 같음 GF000124 은 산림 9-19-1",
|
||||
"갈래": ["같은 이름 다른 책"],
|
||||
},
|
||||
"N033": {
|
||||
"정답": ["GC000271"],
|
||||
"허용": [],
|
||||
"근거": "기계 hr 대형브레이카 + 굴착기 0.7 = GC000271",
|
||||
"갈래": ["장비 이름"],
|
||||
},
|
||||
"N034": {"정답": ["GC000665"], "허용": [], "근거": "건설 8-1-3 = GC000665", "갈래": []},
|
||||
"N035": {
|
||||
"정답": ["GF000328"],
|
||||
"허용": [],
|
||||
"근거": "산림 13-11-1 조립 0.08 · 돌채움 0.17 = GF000328 원형 돌망태",
|
||||
"갈래": [],
|
||||
},
|
||||
"N036": PEBBLE,
|
||||
"N037": {
|
||||
"정답": ["GF000235"],
|
||||
"허용": ["GC000342"],
|
||||
"근거": "산림 13-114(= 13-11-4 오타) 사각돌망태 = GF000235 · 이름 같음 GC000342 은 건설 토목 2-2-3",
|
||||
"갈래": ["같은 이름 다른 책", "근거 조항 엉뚱"],
|
||||
},
|
||||
"N038": PAVE,
|
||||
"N039": {
|
||||
"정답": ["GC000143"],
|
||||
"허용": ["GF000032"],
|
||||
"근거": "건설 4-1-1 조경공 1 · 보통 4 ÷ 150 + 떼꽂이(산림 5-23-3) = GC000143 잔디붙임(주)",
|
||||
"갈래": ["같은 뜻 다른 말", "엑셀 복합"],
|
||||
},
|
||||
"N040": PAVE,
|
||||
"N041": {
|
||||
"정답": [],
|
||||
"허용": [],
|
||||
"부분": ["GF000124", "GF000126"],
|
||||
"근거": "고화매트120 설치(건설품셈 p.266 · 우리 없음) + 토사면 고르기(산림 9-19-1) + 크레인 · 살수",
|
||||
"갈래": ["우리에 없음(새 공종)"],
|
||||
},
|
||||
"N042": {
|
||||
"정답": ["GF000227"],
|
||||
"허용": [],
|
||||
"근거": "산림 13-6-2 = GF000227",
|
||||
"갈래": [],
|
||||
},
|
||||
"N043": CONCRETE,
|
||||
"N044": SHORING,
|
||||
"N045": REBAR,
|
||||
"N046": SCAFFOLD,
|
||||
"N047": {
|
||||
"정답": ["GC000085"],
|
||||
"허용": ["GF000089"],
|
||||
"근거": "건설 3-2-3 굴착기 1.0 310㎥/일 = GC000085 흙깎기(기계) 보통토사",
|
||||
"갈래": ["오타"],
|
||||
},
|
||||
"N048": {
|
||||
"정답": [],
|
||||
"허용": [],
|
||||
"부분": ["GC000267"],
|
||||
"근거": "굴착기 0.6 사이클 공식만(출처 없음) — 우리 「잔토정리」 로직 없음",
|
||||
"짐작": True,
|
||||
"갈래": ["우리에 없음(새 공종)"],
|
||||
},
|
||||
"N049": {
|
||||
"정답": ["GC000676"],
|
||||
"허용": [],
|
||||
"근거": "건설(건축) 8-3-1 = GC000676 잡철물 제작 및 설치",
|
||||
"갈래": [],
|
||||
},
|
||||
"N050": {
|
||||
"정답": ["GC000705", "GC000714", "GC000712"],
|
||||
"허용": [],
|
||||
"근거": "건설(건축) 11-1-3 바탕 + 11-2-6 녹막이 + 11-2-4 유성 — 우리 셋(복합)",
|
||||
"갈래": ["엑셀 복합"],
|
||||
},
|
||||
"N051": {
|
||||
"정답": ["GC000308"],
|
||||
"허용": [],
|
||||
"근거": "건설 1-8-1 지주 · 표지판 = GC000308 교통 안전표지판 설치",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
# ── 사방 ──
|
||||
"S052": CONCRETE,
|
||||
"S053": CONCRETE,
|
||||
"S054": {**EURO, "갈래": ["괄호 속 영문"]},
|
||||
"S055": REBAR,
|
||||
"S056": SCAFFOLD,
|
||||
"S057": SHORING,
|
||||
"S058": EURO,
|
||||
"S059": REBAR,
|
||||
"S060": {
|
||||
"정답": [],
|
||||
"허용": [],
|
||||
"부분": ["GF000283"],
|
||||
"근거": "구역화물자동차 운임(왕복 150㎞ + 60㎞) — 우리 화물 운임 로직 없음(덤프 운반 GF000283 은 다른 셈)",
|
||||
"짐작": True,
|
||||
"갈래": ["우리에 없음(새 공종)"],
|
||||
},
|
||||
"S061": {
|
||||
"정답": ["GC000563"],
|
||||
"허용": ["GC000561"],
|
||||
"근거": "크레인 10Ton 1hr/Ton 철골세우기 기계만(출처 없음) = GC000563 철골세우기 장비 손료(ton당)",
|
||||
"짐작": True,
|
||||
"갈래": ["근거 조항 비어 있음", "같은 뜻 다른 말"],
|
||||
},
|
||||
"S062": {
|
||||
"정답": ["GC000559"],
|
||||
"허용": ["GC000560"],
|
||||
"근거": "건설(건축) 1-1 철골공 2.48 × 0.71(89%) + 1-1-3 용접 — GC000559 주 · GC000560 곁",
|
||||
"갈래": ["엑셀 복합", "엑셀 조항이 윗마디"],
|
||||
},
|
||||
"S063": {
|
||||
"정답": ["GC000676"],
|
||||
"허용": [],
|
||||
"근거": "건설 8-3-1 = GC000676",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"S064": PLY,
|
||||
"S065": {"정답": ["GC000665"], "허용": [], "근거": "건설 8-1-3 = GC000665", "갈래": []},
|
||||
"S066": {
|
||||
"정답": ["GC000089"],
|
||||
"허용": ["GC000088"],
|
||||
"근거": "인력 10% + 기계 90% 혼합토사 건설 3-2-4 = GC000089 터파기(기계) 혼합토사",
|
||||
"갈래": ["괄호 속 규격", "엑셀 복합"],
|
||||
},
|
||||
"S067": {
|
||||
"정답": ["GC000109"],
|
||||
"허용": [],
|
||||
"근거": "건설 3-4-7 대형장비 290㎥/일 = GC000109",
|
||||
"갈래": [],
|
||||
},
|
||||
"S068": {
|
||||
"정답": [],
|
||||
"허용": [],
|
||||
"근거": "굴착기 0.8 사이클 기계 100%(출처 없음) — 우리 잔토처리 로직 없음",
|
||||
"짐작": True,
|
||||
"갈래": ["우리에 없음(새 공종)"],
|
||||
},
|
||||
"S069": CONCRETE,
|
||||
"S070": PLY,
|
||||
"S071": BASE_STONE,
|
||||
"S072": DRAIN,
|
||||
"S073": LABOR_ONLY,
|
||||
"S074": LABOR_ONLY,
|
||||
"S075": CONCRETE,
|
||||
"S076": {
|
||||
"정답": ["GC000083"],
|
||||
"허용": ["GF000101"],
|
||||
"근거": "건설 3-2-1 특별인부 = GC000083",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"S077": {
|
||||
"정답": ["GF000113"],
|
||||
"허용": [],
|
||||
"근거": "보통인부 0.1인/㎥(출처 없음) = 산림 9-14-1 GF000113(임도 파일 단산12 와 같은 값)",
|
||||
"짐작": True,
|
||||
"갈래": ["근거 조항 비어 있음"],
|
||||
},
|
||||
"S078": SOIL_LEFT_HAND,
|
||||
"S079": {
|
||||
"정답": ["GF000328"],
|
||||
"허용": [],
|
||||
"근거": "산림 13-11-1 조립설치 = GF000328(시공 입력 · 조 ↔ ㎡)",
|
||||
"갈래": ["단위 다름", "우리 로직이 더 굵게"],
|
||||
},
|
||||
"S080": {
|
||||
"정답": ["GF000328"],
|
||||
"허용": [],
|
||||
"근거": "산림 13-11-1 돌채움 = GF000328",
|
||||
"갈래": ["단위 다름", "우리 로직이 더 굵게"],
|
||||
},
|
||||
"S081": PEBBLE,
|
||||
"S082": {"정답": ["GC000090"], "허용": [], "근거": "건설 3-2-4 암 = GC000090", "갈래": []},
|
||||
"S083": DRAIN,
|
||||
"S084": {
|
||||
"정답": ["GC000201"],
|
||||
"허용": ["GF000182"],
|
||||
"근거": "건설 6-1-4 펌프차 편성 = GC000201",
|
||||
"갈래": [],
|
||||
},
|
||||
"S085": {
|
||||
"정답": ["GC000268", "GC000269", "GC000275"],
|
||||
"허용": [],
|
||||
"근거": "기계 hr 콘크리트펌프차 41m — 우리 「기계 시간당 기계경비」(기계 입력)",
|
||||
"짐작": True,
|
||||
"갈래": ["장비 이름"],
|
||||
},
|
||||
"S086": FORM_PATTERN,
|
||||
"S087": {
|
||||
"정답": ["GF000196"],
|
||||
"허용": [],
|
||||
"근거": "산림 12-27-1 = GF000196 지수판 설치",
|
||||
"갈래": [],
|
||||
},
|
||||
"S088": {
|
||||
"정답": ["GF000192"],
|
||||
"허용": [],
|
||||
"근거": "산림 12-22 = GF000192(개소 ↔ M)",
|
||||
"갈래": ["단위 다름"],
|
||||
},
|
||||
"S089": {
|
||||
"정답": ["GF000025"],
|
||||
"허용": ["GF000032"],
|
||||
"근거": "산림 5-12 보통인부 0.06 + 떼 소운반 = GF000025 떼붙임(재배잔디)",
|
||||
"갈래": [],
|
||||
},
|
||||
"S090": {"정답": ["GF000226"], "허용": ["GC000260"], "근거": "산림 13-6-1", "갈래": []},
|
||||
"S091": WEDGE,
|
||||
"S092": BACKFILL_STONE,
|
||||
"S093": {"정답": ["GF000227"], "허용": ["GC000261"], "근거": "산림 13-6-2", "갈래": []},
|
||||
"S094": BIG_STONE,
|
||||
"S095": DUMP,
|
||||
"S096": {
|
||||
"정답": ["GC000106"],
|
||||
"허용": [],
|
||||
"근거": "건설 3-4-4 소형장비 = GC000106",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"S097": DUMP,
|
||||
"S098": {
|
||||
"정답": ["GF000229"],
|
||||
"허용": ["GC000262"],
|
||||
"근거": "산림 13-7-1",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
"S099": {
|
||||
"정답": ["GF000230"],
|
||||
"허용": ["GC000263"],
|
||||
"근거": "산림 13-7-2",
|
||||
"갈래": ["같은 뜻 다른 말"],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"""셋째 파일(동부청 영월 2024 · STmate 꼴) 정답 — 씨앗(일위대가조합.json 메모 = 사용자 실무 이름 → 로직)이
|
||||
있는 줄은 사용자 선택을 정답으로 봄(「씨앗」 표시)."""
|
||||
|
||||
CON = {"정답": ["GC000198"], "허용": ["GF000155"], "근거": "6-1-1 콘크리트공 3 · 보통 ÷ 일 시공량", "갈래": ["같은 뜻 다른 말"]}
|
||||
EURO = {"정답": ["GC000214"], "허용": ["GF000210"], "근거": "건설 6-3-3 유로폼 — 이름 같은 산림 GF000210 과 접힘(이름 후보 중복 빼기)", "갈래": ["같은 이름 다른 책"]}
|
||||
LY = {
|
||||
"Y000": {**CON, "근거": "건설 6-1-1 인력운반 23㎥/일 · 씨앗 같음"},
|
||||
"Y001": {"정답": ["GC000211"], "허용": ["GF000160"], "근거": "건설 6-3-1 · 씨앗 같음", "갈래": []},
|
||||
"Y002": {"정답": ["GF000101"], "허용": ["GC000083"], "근거": "옛 건설 3-1-3 인력 터파기 보통인부 0.26 · 씨앗 = GF000101", "짐작": True, "갈래": ["품셈 판 번호 바뀜"]},
|
||||
"Y003": {"정답": ["GF000113"], "허용": [], "근거": "보통인부 0.1 · 씨앗 같음", "갈래": ["품셈 판 번호 바뀜"]},
|
||||
"Y004": {"정답": [], "허용": [], "근거": "잔토처리 인력 0.2인 — 단독 로직 없음", "갈래": ["우리 로직이 더 굵게"]},
|
||||
"Y005": {"정답": ["GC000268", "GC000269", "GC000275"], "허용": [], "근거": "기계 시간 크레인(타이어) 10톤 — 기계 시간당 기계경비(기계 입력)", "짐작": True, "갈래": ["장비 이름"]},
|
||||
"Y006": {"정답": ["GF000101"], "허용": ["GC000089"], "근거": "임도품셈 5-1-2(옛) 기계 90 + 인력 10 · 씨앗 = GF000101", "짐작": True, "갈래": ["품셈 판 번호 바뀜"]},
|
||||
"Y007": {"정답": [], "허용": [], "근거": "잔토처리 기계 100%(백호 0.7) — 로직 없음", "갈래": ["우리에 없음(새 공종)"]},
|
||||
"Y008": {"정답": ["GC000198"], "허용": ["GF000155", "GF000166"], "근거": "임도품셈 5-5(옛) 콘크리트공 0.15 · 보통 0.27 · 씨앗 = GC000198", "짐작": True, "갈래": ["품셈 판 번호 바뀜"]},
|
||||
"Y009": {**EURO, "근거": "건설 6-3-3 유로폼(복잡) — GC000214 · 이름 후보에서 산림 GF000210 과 한 줄로 접혀 근거 점수만 받음"},
|
||||
"Y010": {"정답": ["GC000206", "GC000207"], "허용": ["GF000159"], "근거": "건설 6-2-3 · 6-2-2 가공 + 조립", "갈래": ["우리 로직이 더 잘게", "비슷한 이름 엉뚱"]},
|
||||
"Y011": {"정답": ["GF000168"], "허용": ["GC000110"], "근거": "채집(9:1) · 운반 · 부설 — 산림 12-25 기초잡석(짐작)", "짐작": True, "갈래": ["엑셀 복합", "품셈 판 번호 바뀜"]},
|
||||
"Y012": {"정답": ["GC000308"], "허용": [], "근거": "옛 1-9-3 표지판 · 씨앗 같음", "갈래": []},
|
||||
"Y013": {"정답": ["GF000214"], "허용": [], "근거": "기계 8 : 인력 2 채집 + 운반 — 인력 채집 몫(부분)", "짐작": True, "갈래": ["엑셀 복합"]},
|
||||
"Y014": {"정답": ["GC000263"], "허용": ["GF000225"], "근거": "건설 7-2-2 석공 0.05 + 굴착기 집게 = GC000263", "갈래": ["같은 뜻 다른 말"]},
|
||||
"Y015": {"정답": ["GF000211"], "허용": ["GF000212"], "근거": "채집(9:1) + 덤프 — 인력 채집 몫(짐작)", "짐작": True, "갈래": ["같은 뜻 다른 말", "엑셀 복합"]},
|
||||
"Y016": {"정답": ["GF000218"], "허용": [], "근거": "씨앗 같음(잡석운반및다짐)", "갈래": []},
|
||||
"Y017": {"정답": ["GF000124"], "허용": ["GC000111"], "근거": "보통인부 0.009 + 굴착기 0.7 · 씨앗 같음", "갈래": []},
|
||||
"Y018": {"정답": ["GF000101"], "허용": ["GC000083"], "근거": "씨앗 같음", "짐작": True, "갈래": []},
|
||||
"Y019": {"정답": ["GC000310"], "허용": [], "근거": "씨앗 같음", "갈래": []},
|
||||
"Y020": {**CON, "근거": "건설 6-1-1 장비사용 55㎥/일 + 굴삭기 — GC000198(1위 펌프차 · 토목 6-1-1 소켓관이 부문 없이 조항 같음)", "갈래": ["같은 뜻 다른 말", "부문 없는 조항"]},
|
||||
"Y021": EURO,
|
||||
"Y022": {"정답": ["GC000224"], "허용": [], "근거": "씨앗 같음", "갈래": []},
|
||||
"Y023": {"정답": ["GC000393"], "허용": [], "근거": "토목 6-4-2 · 씨앗 같음", "갈래": []},
|
||||
"Y024": {"정답": ["GF000280"], "허용": ["GF000279"], "근거": "옛 건설 9-1 인력운반(지게) 575㎏/㎡ — 산림 10-6-2(짐작)", "짐작": True, "갈래": ["같은 뜻 다른 말", "품셈 판 번호 바뀜"]},
|
||||
"Y025": {"정답": ["GF000279", "GF000281"], "허용": [], "근거": "옛 건설 9-1 인력운반 2,300㎏/㎥ — 산림 10-6 인력운반(짐작)", "짐작": True, "갈래": ["같은 뜻 다른 말", "품셈 판 번호 바뀜"]},
|
||||
"Y026": {"정답": ["GF000224", "GC000262"], "허용": [], "근거": "건설 7-2-1 석공 0.13 = GC000262 · 씨앗 = GF000224", "갈래": []},
|
||||
"Y027": {"정답": ["GC000251"], "허용": [], "근거": "공통 6-7-1 플륨관 특별 · 보통 + 트럭크레인 = GC000251 플륨관 설치(1위 PC관 = 토목 6-7-1)", "갈래": ["같은 뜻 다른 말", "부문 없는 조항"]},
|
||||
"Y028": {**CON, "근거": "6-1-1 장비사용(소형) — GC000198 · 1위 GF000166 은 산림 12-37 다른 품", "갈래": ["비슷한 이름 엉뚱"]},
|
||||
"Y029": {"정답": ["GC000211"], "허용": ["GF000160"], "근거": "건설 6-3-1 4회 보통", "갈래": ["같은 이름 다른 책"]},
|
||||
"Y030": {"정답": ["GF000136"], "허용": [], "근거": "골재장 → 현장 덤프 운반(옛 9-7) = 산림 10-3-3(짐작)", "짐작": True, "갈래": []},
|
||||
"Y031": {"정답": ["GF000162"], "허용": ["GC000297"], "근거": "임도품셈 7-1-1(옛) · 씨앗 같음", "갈래": []},
|
||||
"Y032": {"정답": ["GF000163"], "허용": [], "근거": "임도품셈 7-1-2(옛) · 씨앗 같음", "갈래": []},
|
||||
"Y033": {"정답": ["GF000218"], "허용": [], "근거": "씨앗 같음", "갈래": []},
|
||||
"Y034": {"정답": ["GF000226"], "허용": [], "근거": "사방품셈 P52 · 씨앗 같음", "갈래": []},
|
||||
"Y035": {"정답": ["GF000213"], "허용": [], "근거": "큰돌 채집 장비 80 + 인력 20", "갈래": ["단위 다름"]},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
"""req225 C1 — 실무 엑셀 일위대가 줄마다 지금 맞춤 결과(기억 없음) + 단가산출 블록 글 뽑기.
|
||||
|
||||
읽기만: 엑셀 · master_data · 우리 로직. 서버 · DB · storage 안 씀.
|
||||
출력 = 같은 폴더 out_lines.json · out_ours.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as li # noqa: E402
|
||||
|
||||
SAMPLES = ROOT / "resources/knowledge/original/실무문서/2026_실무일위대가 모음"
|
||||
FILES = {
|
||||
"북부청임도": SAMPLES / "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls",
|
||||
"북부청사방": SAMPLES / "2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls",
|
||||
}
|
||||
|
||||
|
||||
def block_text(rows, span):
|
||||
start, col, _ = hb._columns(rows)
|
||||
out = []
|
||||
for at in range(span[0], span[1] + 1):
|
||||
row = rows[at - 1]
|
||||
text = row[col] if col < len(row) else ""
|
||||
if isinstance(text, str) and text.strip():
|
||||
out.append(text.rstrip())
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
got = hm.targets()
|
||||
lines_out = []
|
||||
for tag, path in FILES.items():
|
||||
data = path.read_bytes()
|
||||
book = hx.read_book(path.name, data)
|
||||
sheets = hx._sheets(path.name, data)
|
||||
calc = sheets.get(hb.SHEET) or []
|
||||
heads = {b["번"]: b for b in book["단산"]}
|
||||
units = hm.units_of(book)
|
||||
for h in book["호표"]:
|
||||
for line in h["줄"]:
|
||||
m = hm.match_line(line, [], units, True, got)
|
||||
ref = line.get("참조") or {}
|
||||
rec = {
|
||||
"파일": tag,
|
||||
"호표": h["번"],
|
||||
"호표이름": h["이름"],
|
||||
"호표규격": h["규격"],
|
||||
"호표단위": h["단위"],
|
||||
"차례": line["차례"],
|
||||
"이름": line["이름"],
|
||||
"규격": line["규격"],
|
||||
"수량": line["수량"],
|
||||
"단위": line["단위"],
|
||||
"단가": line["단가"],
|
||||
"갈래": line["갈래"],
|
||||
"참조": ref or None,
|
||||
"근거": line.get("근거"),
|
||||
"맞춤": m,
|
||||
}
|
||||
if ref.get("종류") == "단산" and ref["번"] in heads:
|
||||
b = heads[ref["번"]]
|
||||
rec["단산머리"] = {"이름": b["이름"], "단위": b["단위"]}
|
||||
rec["단산글"] = block_text(calc, b["행"])
|
||||
lines_out.append(rec)
|
||||
(HERE / "out_lines.json").write_text(
|
||||
json.dumps(lines_out, ensure_ascii=False, indent=1), encoding="utf-8"
|
||||
)
|
||||
# 우리 돈 로직 목록(읽기만)
|
||||
table = hb.ours()
|
||||
whole = m01.loaded()[1]
|
||||
ours = []
|
||||
for one in got["로직"]:
|
||||
row = whole.logic(one["로직"])
|
||||
o = table.get(one["로직"], {})
|
||||
ours.append(
|
||||
{
|
||||
"로직": one["로직"],
|
||||
"이름": one["이름"],
|
||||
"단위": one["단위"],
|
||||
"구분": o.get("구분"),
|
||||
"조항": sorted(o.get("조항") or []),
|
||||
"직종": sorted(o.get("직종") or []),
|
||||
"출처": str(li.sources(row) or ""),
|
||||
"원문번호": row.get("원문번호"),
|
||||
"입력": [str(s.get("이름")) for s in row.get("입력") or []],
|
||||
}
|
||||
)
|
||||
(HERE / "out_ours.json").write_text(
|
||||
json.dumps(ours, ensure_ascii=False, indent=1), encoding="utf-8"
|
||||
)
|
||||
print("lines", len(lines_out), "ours", len(ours), "mats", len(got["자재"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
"""고유 로직 줄(단가산출 · 기계 · 기타)마다 엑셀 근거 · 블록 글 · 지금 후보 다섯 — 사람 판정용 보기.
|
||||
|
||||
python s02_view.py <파일 태그> <시작> <끝> → 고유 줄 번호 범위만 찍음 · out_items.json 저장
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
LOGIC = ("단가산출", "기계", "기타")
|
||||
|
||||
|
||||
def items():
|
||||
lines = json.loads((HERE / "out_lines.json").read_text(encoding="utf-8"))
|
||||
out, seen = [], {}
|
||||
for x in lines:
|
||||
if x["갈래"] not in LOGIC:
|
||||
continue
|
||||
ref = (x.get("참조") or {}).get("번")
|
||||
key = (x["파일"], x["이름"], x["규격"], x["단위"], ref)
|
||||
if key in seen:
|
||||
seen[key]["줄수"] += 1
|
||||
seen[key]["호표들"].append(x["호표"])
|
||||
continue
|
||||
rec = {**x, "줄수": 1, "호표들": [x["호표"]]}
|
||||
seen[key] = rec
|
||||
out.append(rec)
|
||||
for i, r in enumerate(out):
|
||||
r["id"] = f"{'N' if r['파일'] == '북부청임도' else 'S'}{i:03d}"
|
||||
return out
|
||||
|
||||
|
||||
def show(r):
|
||||
m = r["맞춤"]
|
||||
b = r.get("근거") or {}
|
||||
print(f"## {r['id']} [{r['파일']}] 줄{r['줄수']} 호표{sorted(set(r['호표들']))[:6]}")
|
||||
print(f" 엑셀: {r['이름']} | 규격 {r['규격']} | 단위 {r['단위']} | 갈래 {r['갈래']} | 단가 {r['단가']}")
|
||||
if r.get("단산머리"):
|
||||
print(f" 단산{r['참조']['번']}: {r['단산머리']} 출처 {b.get('출처')} 직종 {b.get('직종')}")
|
||||
for t in r.get("단산글", [])[1:16]:
|
||||
t = " ".join(t.split())
|
||||
if t.startswith(("2. 합", "노무비 :", "재료비 :", "경 비 :", "계 :")):
|
||||
continue
|
||||
print(f" · {t[:110]}")
|
||||
tgt = m.get("대상")
|
||||
print(f" 상태 {m['상태']} 대상 {tgt}")
|
||||
for c in m.get("후보") or []:
|
||||
why = (c.get("근거") or {}).get("까닭", "")
|
||||
print(f" - {c.get('로직') or c.get('자재')} {c['이름']} [{c['단위']}] {c['점수']} {why}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
allx = items()
|
||||
(HERE / "out_items.json").write_text(
|
||||
json.dumps(allx, ensure_ascii=False, indent=1), encoding="utf-8"
|
||||
)
|
||||
tag = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
lo, hi = (int(sys.argv[2]), int(sys.argv[3])) if len(sys.argv) > 3 else (0, 10**6)
|
||||
pick = [r for r in allx if r["파일"].endswith(tag) or not tag]
|
||||
print("총", len(allx), "고른 파일", len(pick))
|
||||
for r in pick[lo:hi]:
|
||||
show(r)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""우리 돈 로직 찾기 — python s03_find.py <정규식> [<정규식> …] (이름 · 출처 · 키 · 조항에서)."""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ours = json.loads((HERE / "out_ours.json").read_text(encoding="utf-8"))
|
||||
for pat in sys.argv[1:]:
|
||||
rx = re.compile(pat)
|
||||
print(f"== /{pat}/")
|
||||
for o in ours:
|
||||
hay = f"{o['로직']} {o['이름']} {o['출처']} {' '.join(o['조항'])}"
|
||||
if rx.search(hay):
|
||||
print(
|
||||
f" {o['로직']} {o['이름']} [{o['단위']}] {o['구분']} | {o['출처'][:60]} | 직종 {','.join(o['직종'])} | 입력 {','.join(o['입력'])[:70]}"
|
||||
)
|
||||
@@ -0,0 +1,257 @@
|
||||
"""req225 C1 — 정답 표시 대비 지표 · 개선 방법 흉내(같은 프로세스 안 함수 바꿔 끼움 · 저장소 안 고침).
|
||||
|
||||
python s04_eval.py → 지표 표(지금 · 이름만 · 방법별) 찍고 out_eval.json 저장
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
|
||||
from labels import L # noqa: E402
|
||||
|
||||
ITEMS = json.loads((HERE / "out_items.json").read_text(encoding="utf-8"))
|
||||
BY_ID = {r["id"]: r for r in ITEMS}
|
||||
FILES = ("북부청임도", "북부청사방")
|
||||
|
||||
|
||||
# ── 지표 ─────────────────────────────────────────────────────────────
|
||||
def score(results: dict, ids=None) -> dict:
|
||||
"""results = {id: {상태, top: [키 …]}} → 지표(고유 줄 · 줄 수 무게)."""
|
||||
ids = ids or list(BY_ID)
|
||||
out = {}
|
||||
for weight in ("고유", "줄"):
|
||||
w = (lambda i: 1) if weight == "고유" else (lambda i: BY_ID[i]["줄수"])
|
||||
have = [i for i in ids if L[i]["정답"]]
|
||||
none = [i for i in ids if not L[i]["정답"]]
|
||||
tot = sum(w(i) for i in have)
|
||||
|
||||
def rate(pred):
|
||||
return round(100 * sum(w(i) for i in have if pred(i)) / tot, 1) if tot else 0
|
||||
|
||||
def strict(i):
|
||||
return set(L[i]["정답"])
|
||||
|
||||
def loose(i):
|
||||
return set(L[i]["정답"]) | set(L[i]["허용"])
|
||||
|
||||
top = {i: results[i]["top"] for i in ids}
|
||||
out[weight] = {
|
||||
"있음": sum(w(i) for i in have),
|
||||
"없음": sum(w(i) for i in none),
|
||||
"1위 엄격": rate(lambda i: top[i][:1] and top[i][0] in strict(i)),
|
||||
"1위 느슨": rate(lambda i: top[i][:1] and top[i][0] in loose(i)),
|
||||
"5안 엄격": rate(lambda i: bool(set(top[i][:5]) & strict(i))),
|
||||
"5안 느슨": rate(lambda i: bool(set(top[i][:5]) & loose(i))),
|
||||
"없음인데 후보": sum(w(i) for i in none if top[i]),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def table(name: str, results: dict) -> dict:
|
||||
row = {"전체": score(results)}
|
||||
for f in FILES:
|
||||
row[f] = score(results, [r["id"] for r in ITEMS if r["파일"] == f])
|
||||
return {"이름": name, **row}
|
||||
|
||||
|
||||
def show(row: dict) -> None:
|
||||
def cell(s):
|
||||
u = s["고유"]
|
||||
return f"1위 {u['1위 엄격']:>5}/{u['1위 느슨']:>5} 5안 {u['5안 엄격']:>5}/{u['5안 느슨']:>5} 없음후보 {u['없음인데 후보']}"
|
||||
|
||||
print(f"{row['이름'][:22]:<22} | 전체 {cell(row['전체'])} | 임도 {cell(row['북부청임도'])} | 사방 {cell(row['북부청사방'])}")
|
||||
|
||||
|
||||
# ── 돌리기 ───────────────────────────────────────────────────────────
|
||||
def stored() -> dict:
|
||||
out = {}
|
||||
for r in ITEMS:
|
||||
m = r["맞춤"]
|
||||
if m["상태"] == "같음":
|
||||
top = [m["대상"].get("로직")]
|
||||
else:
|
||||
top = [c.get("로직") for c in m.get("후보") or []]
|
||||
out[r["id"]] = {"상태": m["상태"], "top": [k for k in top if k]}
|
||||
return out
|
||||
|
||||
|
||||
def line_of(r: dict) -> dict:
|
||||
keep = ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")
|
||||
return {k: r.get(k) for k in keep}
|
||||
|
||||
|
||||
def run(fn=None) -> dict:
|
||||
"""지금 파이프라인(hm.match_line) — 바꿔 끼운 함수가 있으면 그것으로."""
|
||||
hm._targets.cache_clear()
|
||||
hb._ours.cache_clear()
|
||||
got = hm.targets()
|
||||
out = {}
|
||||
for r in ITEMS:
|
||||
line = line_of(r)
|
||||
m = (fn or default_line)(line, got, r)
|
||||
out[r["id"]] = m
|
||||
return out
|
||||
|
||||
|
||||
def default_line(line, got, r):
|
||||
m = hm.match_line(line, [], {}, True, got)
|
||||
if m["상태"] == "같음":
|
||||
top = [m["대상"].get("로직")]
|
||||
else:
|
||||
top = [c.get("로직") for c in m.get("후보") or []]
|
||||
return {"상태": m["상태"], "top": [k for k in top if k]}
|
||||
|
||||
|
||||
def name_only(line, got, r):
|
||||
"""330 앞(이름만) — 같음 · 씨앗 + 글자 두 개 묶음 후보 다섯."""
|
||||
found = hm._exact(line, "로직", got)
|
||||
if len(found) == 1:
|
||||
return {"상태": "같음", "top": [found[0]["로직"]]}
|
||||
top = [x["로직"] for x in found] + [c["로직"] for c in hm.candidates(line, "로직", got, 5)]
|
||||
return {"상태": "후보" if top else "없음", "top": list(dict.fromkeys(top))[:5]}
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def patched(obj, **funcs):
|
||||
old = {k: getattr(obj, k) for k in funcs}
|
||||
for k, v in funcs.items():
|
||||
setattr(obj, k, v)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for k, v in old.items():
|
||||
setattr(obj, k, v)
|
||||
hm._targets.cache_clear()
|
||||
hb._ours.cache_clear()
|
||||
|
||||
|
||||
# ── 방법 ① 이름 다듬기 — 괄호 글 살림 · 수 · 단위 · 규격 조각 뺌 ─────────────
|
||||
_SPECISH = re.compile(
|
||||
r"[a-z]\d?\s*[=:]\s*[\d.,]+\s*(mm|cm|m|km)?" # L=500m · D=1000mm
|
||||
r"|[\d.,]+\s*(㎥|㎡|m3|m2|mm|cm|km|m|톤|ton|t|%|회|kg|인|본|개)?" # 0.6㎥ · 4.5톤 · 100%
|
||||
r"|[×x*~∼:/+]",
|
||||
re.I,
|
||||
)
|
||||
_FILLER = ("적용", "사용", "포함", "이상", "미만", "이하", "내외", "산림사업품셈", "산림사업", "품셈")
|
||||
|
||||
|
||||
def tidy_keep(name) -> str:
|
||||
text = unicodedata.normalize("NFKC", str(name or "")).lower()
|
||||
for old, new in hm.MACH_WORDS:
|
||||
text = text.replace(old, new)
|
||||
text = _SPECISH.sub(" ", text)
|
||||
for w in _FILLER:
|
||||
text = text.replace(w, " ")
|
||||
text = re.sub(r"[()\[\]{}<>,._\-·]", " ", text)
|
||||
return hm.mm.plain(text)
|
||||
|
||||
|
||||
# ── 방법 ② 같은 뜻 말 사전(임도 파일에서만 뽑음 · 사방은 처음 보는 셈) ─────────
|
||||
SYN_NORTH = [
|
||||
("레미콘", "레디믹스트콘크리트"),
|
||||
("깍기", "깎기"),
|
||||
("뒷채움", "뒤채움"),
|
||||
("붙이기", "붙임"),
|
||||
("휠라", "채움재"),
|
||||
("메쉬", "메시"),
|
||||
("야면석", "돌"),
|
||||
("소운반", "인력운반"),
|
||||
("면고르기", "절토면고르기"),
|
||||
("구조물터파기", "굴착"),
|
||||
("터파기", "굴착"),
|
||||
("비탈면보호공", "잔디붙임"),
|
||||
("평떼", "잔디붙임"),
|
||||
("콘크리트포장", "콘크리트표층포설"),
|
||||
("고임돌", "약돌"),
|
||||
("조약돌", "약돌"),
|
||||
]
|
||||
|
||||
|
||||
def with_syn(base, pairs):
|
||||
def tidy_syn(name):
|
||||
text = str(name or "")
|
||||
flat = re.sub(r"\s+", "", unicodedata.normalize("NFKC", text))
|
||||
for old, new in pairs:
|
||||
flat = flat.replace(old, new)
|
||||
return base(flat)
|
||||
|
||||
return tidy_syn
|
||||
|
||||
|
||||
# ── 방법 ④ 글자 n-그램 ─────────────────────────────────────────────────
|
||||
def grams_123(text: str) -> set:
|
||||
out = set(text)
|
||||
out |= {text[i : i + 2] for i in range(len(text) - 1)}
|
||||
out |= {text[i : i + 3] for i in range(len(text) - 2)}
|
||||
return out or {text}
|
||||
|
||||
|
||||
def grams_3(text: str) -> set:
|
||||
return {text[i : i + 3] for i in range(len(text) - 2)} or {text}
|
||||
|
||||
|
||||
# ── 방법 ⑥ 근거 조항 — 엑셀이 윗마디(6-2 ⊃ 6-2-2) · 오타 13-114 → 13-11-4 ────────────
|
||||
def clause_prefix(theirs, our):
|
||||
nums = [n for book, n in theirs if book == our["책"]]
|
||||
same = sorted(n for n in nums if n in our["조항"])
|
||||
if same:
|
||||
return 1.0, [f"{our['구분']} {n}" for n in same], "조항 같음"
|
||||
kids = sorted(c for c in our["조항"] for n in nums if c.startswith(n + "-"))
|
||||
if kids:
|
||||
return 0.9, [f"{our['구분']} {n}" for n in kids], "조항 같음" # 윗마디 = 같음 취급
|
||||
heads = {hb._head(n) for n in nums}
|
||||
near = sorted(n for n in our["조항"] if hb._head(n) in heads)
|
||||
if near:
|
||||
return 0.5, [f"{our['구분']} {n}" for n in near], "앞 마디 같음"
|
||||
return 0.0, [], ""
|
||||
|
||||
|
||||
_TYPO_CLAUSE = re.compile(r"^(\d+)-(\d)(\d)(\d)$") # 13-114 → 13-11-4
|
||||
|
||||
|
||||
def fix_typo(theirs):
|
||||
out = []
|
||||
for book, n in theirs:
|
||||
m = _TYPO_CLAUSE.match(n)
|
||||
out.append([book, f"{m.group(1)}-{m.group(2)}{m.group(3)}-{m.group(4)}" if m else n])
|
||||
return out
|
||||
|
||||
|
||||
def basis_prefix_line(line, got, r):
|
||||
b = dict(line.get("근거") or {})
|
||||
if b.get("출처"):
|
||||
b["출처"] = fix_typo(b["출처"])
|
||||
line = {**line, "근거": b}
|
||||
return default_line(line, got, r)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rows = []
|
||||
rows.append(table("저장된 지금", stored()))
|
||||
rows.append(table("지금 다시 돌림", run()))
|
||||
rows.append(table("이름만(330 앞)", run(name_only)))
|
||||
with patched(hm, tidy=tidy_keep):
|
||||
rows.append(table("①이름 다듬기", run()))
|
||||
with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)):
|
||||
rows.append(table("②사전(임도에서 뽑음)", run()))
|
||||
with patched(hm, _grams=grams_123):
|
||||
rows.append(table("④1·2·3그램", run()))
|
||||
with patched(hm, _grams=grams_3):
|
||||
rows.append(table("④3그램", run()))
|
||||
with patched(hb, _clause=clause_prefix):
|
||||
rows.append(table("⑥조항 윗마디", run()))
|
||||
rows.append(table("⑥+오타 조항", run(basis_prefix_line)))
|
||||
for row in rows:
|
||||
show(row)
|
||||
(HERE / "out_eval.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8")
|
||||
@@ -0,0 +1,30 @@
|
||||
"""두 결과의 1위 · 5안 바뀐 줄 — python s04b_diff.py (s04_eval 함수 씀)."""
|
||||
from s04_eval import * # noqa: F401,F403
|
||||
|
||||
|
||||
def mark(i, top):
|
||||
s, l = set(L[i]["정답"]), set(L[i]["정답"]) | set(L[i]["허용"])
|
||||
t1 = top[0] if top else None
|
||||
return ("O" if t1 in s else "o" if t1 in l else "x") + ("5" if set(top[:5]) & l else "-")
|
||||
|
||||
|
||||
def diff(a, b, title):
|
||||
print("==", title)
|
||||
for i in BY_ID:
|
||||
if not L[i]["정답"]:
|
||||
continue
|
||||
ma, mb = mark(i, a[i]["top"]), mark(i, b[i]["top"])
|
||||
if ma != mb:
|
||||
r = BY_ID[i]
|
||||
print(f" {i} {r['이름'][:18]:<18} {ma}->{mb} {a[i]['top'][:2]} -> {b[i]['top'][:2]} 정답{L[i]['정답']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
base = run()
|
||||
with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)):
|
||||
diff(base, run(), "② 사전")
|
||||
with patched(hm, tidy=tidy_keep):
|
||||
diff(base, run(), "① 다듬기")
|
||||
with patched(hb, _clause=clause_prefix):
|
||||
diff(base, run(), "⑥ 윗마디")
|
||||
diff(run(name_only), base, "이름만 → 지금")
|
||||
@@ -0,0 +1,330 @@
|
||||
"""req225 C1 — 방법 더: 이름만 쪽 방법(①②③④) · 책 맞춤 · 단위 거름망 · 직종 · 장비 겹침 · 금액 ·
|
||||
같음 근거 검사 · 결합. 저장소 안 고침(같은 프로세스 안 바꿔 끼움 · 읽기만).
|
||||
|
||||
python s05_more.py money → 정답 로직 값 ↔ 엑셀 단가 비(진단) · out_money.json
|
||||
python s05_more.py → 방법별 지표 · out_more.json
|
||||
"""
|
||||
|
||||
import difflib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from functools import lru_cache
|
||||
|
||||
from s04_eval import ( # noqa: F401
|
||||
BY_ID,
|
||||
HERE,
|
||||
ITEMS,
|
||||
L,
|
||||
SYN_NORTH,
|
||||
clause_prefix,
|
||||
default_line,
|
||||
grams_123,
|
||||
hb,
|
||||
hm,
|
||||
name_only,
|
||||
patched,
|
||||
run,
|
||||
show,
|
||||
table,
|
||||
tidy_keep,
|
||||
with_syn,
|
||||
)
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
|
||||
from M02_MasterTemplete import M02_Labor_Doc as ldoc # noqa: E402
|
||||
|
||||
WHOLE = m01.loaded()[1]
|
||||
_PER = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?\s*([^\s·(\d,.][^\s·(]*)")
|
||||
|
||||
|
||||
# ── 이름만 쪽 방법 ────────────────────────────────────────────────────
|
||||
def name_words(line, got, r):
|
||||
"""③ 낱말 차례 무시 — 괄호 글 포함 뜻 낱말 겹침(부분 글 포함)을 점수에 더함."""
|
||||
base = name_only(line, got, r)
|
||||
if base["상태"] == "같음":
|
||||
return base
|
||||
mine = hb._loose_words(f"{line.get('이름')} {line.get('규격')}")
|
||||
scored = []
|
||||
for one in got["로직"]:
|
||||
theirs = hb._loose_words(one["이름"])
|
||||
hit = sum(1 for a in mine if any(a in b or b in a for b in theirs))
|
||||
if not hit:
|
||||
continue
|
||||
g1, g2 = hm._grams(hm.tidy(line.get("이름"))), one["grams"]
|
||||
dice = 2 * len(g1 & g2) / (len(g1) + len(g2))
|
||||
s = dice + 0.5 * hit / max(len(mine), 1) + (0.15 if hm.fits(line.get("단위"), one["단위"]) else 0)
|
||||
scored.append((s, one["로직"]))
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
top = list(dict.fromkeys(k for _, k in scored))[:5]
|
||||
return {"상태": "후보" if top else "없음", "top": top}
|
||||
|
||||
|
||||
def name_difflib(line, got, r):
|
||||
"""④' difflib 비(가장 긴 같은 조각) — 정리 이름끼리."""
|
||||
base = name_only(line, got, r)
|
||||
if base["상태"] == "같음":
|
||||
return base
|
||||
me = hm.tidy(line.get("이름"))
|
||||
scored = []
|
||||
for one in got["로직"]:
|
||||
s = difflib.SequenceMatcher(None, me, one["tidy"]).ratio()
|
||||
s += 0.15 if hm.fits(line.get("단위"), one["단위"]) else 0
|
||||
scored.append((s, one["로직"]))
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
return {"상태": "후보", "top": [k for _, k in scored[:5]]}
|
||||
|
||||
|
||||
# ── 책 맞춤 — 엑셀 출처 책(산림 · 건설)과 같은 책 덤(지금 = 산림이면 늘 0.05) ──────────
|
||||
def book_of(line):
|
||||
src = ((line.get("근거") or {}).get("출처") or [])
|
||||
return src[0][0] if src else None
|
||||
|
||||
|
||||
def candidates_book(line, pool, got=None, limit=5):
|
||||
me = hm.tidy(line.get("이름"))
|
||||
grams, unit = hm._grams(me), line.get("단위")
|
||||
text = hm.mm.plain_spec(f"{line.get('이름')}{line.get('규격')}")
|
||||
want = book_of(line)
|
||||
scored, seen = [], set()
|
||||
mine = hm.words(line.get("이름"))
|
||||
for one in (got or hm.targets())[pool]:
|
||||
if not grams & one["grams"] or not hm.near(mine, one["words"]):
|
||||
continue
|
||||
s = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"]))
|
||||
s += 1 if one["tidy"] == me else 0
|
||||
s += 0.15 if hm.fits(unit, one["단위"]) else 0
|
||||
if want:
|
||||
s += 0.2 if ("산림품셈" if one.get("산림") else "건설품셈") == want else 0
|
||||
else:
|
||||
s += 0.05 if one.get("산림") else 0
|
||||
s += 0.5 if one.get("spec") and one["spec"] in text else 0
|
||||
scored.append((s, one))
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
out = []
|
||||
for s, one in scored:
|
||||
label = (one["이름"], one.get("규격"), one["단위"])
|
||||
if label in seen:
|
||||
continue
|
||||
seen.add(label)
|
||||
out.append({**hm._public(one), "점수": round(s, 3)})
|
||||
if len(out) == limit:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
# ── ⑤ 단위 거름망 ─────────────────────────────────────────────────────
|
||||
def unit_filter_line(line, got, r):
|
||||
m = default_line(line, got, r)
|
||||
if m["상태"] == "같음":
|
||||
return m
|
||||
units = {x["로직"]: x["단위"] for x in got["로직"]}
|
||||
keep = [k for k in m["top"] if hm.fits(line.get("단위"), units.get(k))]
|
||||
return {"상태": "후보" if keep else "없음", "top": keep}
|
||||
|
||||
|
||||
# ── ⑦ 직종 · 장비 구성 겹침(조항 없이도) ───────────────────────────────────
|
||||
MACH = {
|
||||
"굴착기": ("굴착기", "굴삭기", "백호", "back-hoe", "backhoe"),
|
||||
"불도저": ("불도저", "불도우저"),
|
||||
"덤프": ("덤프",),
|
||||
"크레인": ("크레인",),
|
||||
"롤러": ("롤러", "로울러"),
|
||||
"브레이커": ("브레이커", "브레이카"),
|
||||
"펌프차": ("펌프차", "펌프카"),
|
||||
"집게": ("집게",),
|
||||
"콤팩터": ("콤팩터", "컴팩터"),
|
||||
"살수": ("살수차", "물탱크"),
|
||||
"화물차": ("화물자동차", "화물차"),
|
||||
}
|
||||
_EQ = re.compile(r"\d{4}-\d{4}")
|
||||
|
||||
|
||||
def mach_types(text: str) -> set:
|
||||
t = unicodedata.normalize("NFKC", text).lower()
|
||||
return {k for k, ws in MACH.items() if any(w in t for w in ws)}
|
||||
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def our_mach(code: str) -> frozenset:
|
||||
row = WHOLE.logic(code)
|
||||
eq = WHOLE.numbers.get("EQ", {})
|
||||
bits = []
|
||||
for h in row.get("호표") or []:
|
||||
el = str(h.get("요소") or "")
|
||||
bits.append(el)
|
||||
bits += [f"{eq[c].get('상세구분')} {eq[c].get('이름')}" for c in _EQ.findall(el) if c in eq]
|
||||
for s in row.get("입력") or []:
|
||||
for v in s.get("고르기") or []:
|
||||
if isinstance(v, str) and v in eq:
|
||||
bits.append(f"{eq[v].get('상세구분')} {eq[v].get('이름')}")
|
||||
bits.append(str(s.get("이름") or ""))
|
||||
return frozenset(mach_types(" ".join(bits)))
|
||||
|
||||
|
||||
def excel_mach(r) -> set:
|
||||
return mach_types(" ".join(r.get("단산글") or []) + " " + str(r.get("규격") or ""))
|
||||
|
||||
|
||||
def compose_line(line, got, r, pool_n=40):
|
||||
"""지금 합친 차례(다섯 자르기 전) + 직종 · 장비 겹침 덤 — 다시 다섯."""
|
||||
m = hm.match_line(line, [], {}, True, got)
|
||||
if m["상태"] == "같음":
|
||||
return {"상태": "같음", "top": [m["대상"]["로직"]]}
|
||||
basis = hb.candidates(line, got)
|
||||
named = hm.candidates(line, "로직", got, None)
|
||||
ordered = hb.merge(basis, named)[:pool_n]
|
||||
trades = set((line.get("근거") or {}).get("직종") or []) - {"보통인부"}
|
||||
em = excel_mach(r)
|
||||
ours = hb.ours()
|
||||
rescored = []
|
||||
for at, c in enumerate(ordered):
|
||||
k = c.get("로직")
|
||||
s = c["점수"]
|
||||
ot = ours.get(k, {}).get("직종", set()) - {"보통인부"}
|
||||
if trades:
|
||||
s += 0.4 * len(trades & ot) / len(trades)
|
||||
om = our_mach(k)
|
||||
if em or om:
|
||||
s += 0.3 * len(em & om) / max(len(em | om), 1)
|
||||
rescored.append((s, -at, k))
|
||||
rescored.sort(reverse=True)
|
||||
top = [k for _, _, k in rescored]
|
||||
first = next((c.get("로직") for c in named), None)
|
||||
five = top[:5] if first in top[:5] or first is None else top[:4] + [first]
|
||||
return {"상태": "후보" if five else "없음", "top": five}
|
||||
|
||||
|
||||
# ── ⑧ 금액 — 후보 로직 값(자동값 + 규격 입력) ↔ 엑셀 단가 ────────────────────
|
||||
@lru_cache(maxsize=8192)
|
||||
def price(code: str, given_json: str) -> tuple:
|
||||
got = ldoc.run_line(WHOLE, code, json.loads(given_json))
|
||||
base = got.get("기준")
|
||||
if not base:
|
||||
return (None, got.get("까닭"))
|
||||
row = WHOLE.logic(code)
|
||||
per = _PER.findall(str(row.get("결과단위") or ""))
|
||||
times = float(per[0][0].replace(",", "")) if per and per[0][0] else 1.0
|
||||
return (float(base["계"]) / times, None)
|
||||
|
||||
|
||||
def money_ratio(line, code, unit_of) -> float | None:
|
||||
try:
|
||||
specs = hm._logic_specs(code)
|
||||
values, _ = hm.spec_inputs(line, specs)
|
||||
except Exception: # noqa: BLE001 — 진단용
|
||||
values = {}
|
||||
p, _why = price(code, json.dumps(values, ensure_ascii=False, sort_keys=True, default=str))
|
||||
excel = (line.get("단가") or {}).get("계")
|
||||
if p is None or not excel:
|
||||
return None
|
||||
conv = hm.factor(line.get("단위"), unit_of.get(code), line.get("규격"))
|
||||
if conv["배"] is None:
|
||||
return None
|
||||
return p * float(conv["배"]) / float(excel)
|
||||
|
||||
|
||||
def money_line(line, got, r, pool_n=15):
|
||||
m = hm.match_line(line, [], {}, True, got)
|
||||
if m["상태"] == "같음":
|
||||
return {"상태": "같음", "top": [m["대상"]["로직"]]}
|
||||
basis = hb.candidates(line, got)
|
||||
named = hm.candidates(line, "로직", got, None)
|
||||
ordered = hb.merge(basis, named)[:pool_n]
|
||||
unit_of = {x["로직"]: x["단위"] for x in got["로직"]}
|
||||
rescored = []
|
||||
for at, c in enumerate(ordered):
|
||||
k = c.get("로직")
|
||||
s = c["점수"]
|
||||
q = money_ratio(line, k, unit_of)
|
||||
if q is not None and q > 0:
|
||||
gap = abs(q - 1)
|
||||
s += 0.6 if gap <= 0.15 else 0.3 if gap <= 0.35 else 0
|
||||
rescored.append((s, -at, k))
|
||||
rescored.sort(reverse=True)
|
||||
top = [k for _, _, k in rescored]
|
||||
first = next((c.get("로직") for c in named), None)
|
||||
five = top[:5] if first in top[:5] or first is None else top[:4] + [first]
|
||||
return {"상태": "후보" if five else "없음", "top": five}
|
||||
|
||||
|
||||
# ── ⑪ 같음도 근거 검사 — 이름 같음이 엑셀 출처 책 · 조항과 어긋나면 근거 후보를 앞에 ───────
|
||||
def same_check_line(line, got, r):
|
||||
m = hm.match_line(line, [], {}, True, got)
|
||||
if m["상태"] != "같음":
|
||||
return default_line(line, got, r)
|
||||
tgt = m["대상"]["로직"]
|
||||
theirs = (line.get("근거") or {}).get("출처") or []
|
||||
if not theirs:
|
||||
return {"상태": "같음", "top": [tgt]}
|
||||
our = hb.ours().get(tgt)
|
||||
sc, _, _ = hb._clause(theirs, our) if our else (0, None, None)
|
||||
if sc >= 0.9:
|
||||
return {"상태": "같음", "top": [tgt]}
|
||||
basis = [c for c in hb.candidates(line, got) if c["근거"]["까닭"].startswith("조항 같음")]
|
||||
if not basis:
|
||||
return {"상태": "같음", "top": [tgt]}
|
||||
top = list(dict.fromkeys([basis[0]["로직"], tgt] + [c["로직"] for c in basis[1:]]))[:5]
|
||||
return {"상태": "후보", "top": top}
|
||||
|
||||
|
||||
def diag_money():
|
||||
got = hm.targets()
|
||||
unit_of = {x["로직"]: x["단위"] for x in got["로직"]}
|
||||
rows = []
|
||||
for r in ITEMS:
|
||||
lab = L[r["id"]]
|
||||
if not lab["정답"]:
|
||||
continue
|
||||
line = {k: r.get(k) for k in ("이름", "규격", "단위", "단가", "갈래", "참조", "근거")}
|
||||
right = {k: money_ratio(line, k, unit_of) for k in lab["정답"]}
|
||||
top = (r["맞춤"].get("후보") or [{}])[0].get("로직") or (r["맞춤"].get("대상") or {}).get("로직")
|
||||
rows.append(
|
||||
{
|
||||
"id": r["id"],
|
||||
"이름": r["이름"],
|
||||
"엑셀": (r.get("단가") or {}).get("계"),
|
||||
"정답 비": right,
|
||||
"1위": top,
|
||||
"1위 비": money_ratio(line, top, unit_of) if top and top not in lab["정답"] else None,
|
||||
}
|
||||
)
|
||||
(HERE / "out_money.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8")
|
||||
best = [min((abs(v - 1) for v in x["정답 비"].values() if v), default=None) for x in rows]
|
||||
have = [b for b in best if b is not None]
|
||||
print("정답 값 셈 됨", len(have), "/", len(rows))
|
||||
for cut in (0.05, 0.15, 0.35, 1.0):
|
||||
print(f" |비-1| ≤ {cut}: {sum(1 for b in have if b <= cut)}")
|
||||
for x in rows:
|
||||
print(f" {x['id']} {x['이름'][:16]:<16} 엑셀 {x['엑셀']} 정답비 { {k: round(v, 2) if v else v for k, v in x['정답 비'].items()} } 1위 {x['1위']} {round(x['1위 비'], 2) if x['1위 비'] else ''}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1:] == ["money"]:
|
||||
diag_money()
|
||||
sys.exit()
|
||||
rows = [table("지금", run()), table("이름만", run(name_only))]
|
||||
with patched(hm, tidy=tidy_keep):
|
||||
rows.append(table("이름만+①다듬기", run(name_only)))
|
||||
with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)):
|
||||
rows.append(table("이름만+②사전", run(name_only)))
|
||||
with patched(hm, tidy=with_syn(tidy_keep, SYN_NORTH)):
|
||||
rows.append(table("이름만+①②", run(name_only)))
|
||||
rows.append(table("이름만+③낱말", run(name_words)))
|
||||
with patched(hm, _grams=grams_123):
|
||||
rows.append(table("이름만+④123그램", run(name_only)))
|
||||
rows.append(table("이름만+④difflib", run(name_difflib)))
|
||||
with patched(hm, candidates=candidates_book):
|
||||
rows.append(table("책 맞춤", run()))
|
||||
rows.append(table("⑤단위 거름망", run(unit_filter_line)))
|
||||
rows.append(table("⑦직종·장비 겹침", run(compose_line)))
|
||||
rows.append(table("⑧금액", run(money_line)))
|
||||
rows.append(table("⑪같음 근거 검사", run(same_check_line)))
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, candidates=candidates_book):
|
||||
rows.append(table("결합 ⑥①책", run()))
|
||||
rows.append(table("결합 ⑥①책⑪", run(same_check_line)))
|
||||
rows.append(table("결합 ⑥①책⑪⑧", run(lambda a, b, c: money_line(a, b, c) if hm.match_line(a, [], {}, True, b)["상태"] != "같음" else same_check_line(a, b, c))))
|
||||
for row in rows:
|
||||
show(row)
|
||||
(HERE / "out_more.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8")
|
||||
@@ -0,0 +1,17 @@
|
||||
from s04b_diff import diff
|
||||
from s05_more import * # noqa: F401,F403
|
||||
|
||||
if __name__ == "__main__":
|
||||
base = run()
|
||||
with patched(hm, candidates=candidates_book):
|
||||
diff(base, run(), "책 맞춤")
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, candidates=candidates_book):
|
||||
diff(base, run(), "결합 ⑥①책")
|
||||
with patched(hb, _clause=clause_prefix):
|
||||
p6 = run()
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep):
|
||||
diff(p6, run(), "⑥ → ⑥①")
|
||||
diff(base, run(compose_line), "⑦")
|
||||
diff(base, run(money_line), "⑧")
|
||||
diff(base, run(same_check_line), "⑪")
|
||||
diff(base, run(unit_filter_line), "⑤")
|
||||
@@ -0,0 +1,103 @@
|
||||
"""⑨ 맞춤 기억 재사용 — 한 파일 정답을 기억으로 넣고 다른 파일에 몇 줄이 바로 들어가는지(열쇠 꼴별).
|
||||
+ 낮은 확신 표시(점수 문턱) 흉내 · ⑥①⑪ 결합(책 빼고) · 사전 덤(글자 묶음에만)."""
|
||||
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
from s04_eval import ( # noqa: F401
|
||||
BY_ID,
|
||||
HERE,
|
||||
ITEMS,
|
||||
L,
|
||||
SYN_NORTH,
|
||||
clause_prefix,
|
||||
hb,
|
||||
hm,
|
||||
patched,
|
||||
run,
|
||||
show,
|
||||
table,
|
||||
tidy_keep,
|
||||
with_syn,
|
||||
)
|
||||
from s05_more import same_check_line
|
||||
|
||||
KEYS = {
|
||||
"지금(품명|규격|단위)": lambda r: hm.line_key(r),
|
||||
"품명|단위": lambda r: f"{hm.mm.plain(r['이름'])}|{hm.mm.plain_unit(r['단위'])}",
|
||||
"정리이름|단위": lambda r: f"{hm.tidy(r['이름'])}|{hm.mm.plain_unit(r['단위'])}",
|
||||
"다듬은이름+규격낱말|단위": lambda r: f"{tidy_keep(str(r['이름']) + ' ' + str(r['규격']))}|{hm.mm.plain_unit(r['단위'])}",
|
||||
}
|
||||
|
||||
|
||||
def reuse(src: str, dst: str) -> None:
|
||||
for name, key in KEYS.items():
|
||||
memo = {}
|
||||
for r in ITEMS:
|
||||
if r["파일"] == src and L[r["id"]]["정답"]:
|
||||
memo.setdefault(key(r), L[r["id"]]["정답"][0])
|
||||
hit = ok = lines = ok_lines = 0
|
||||
for r in ITEMS:
|
||||
if r["파일"] != dst:
|
||||
continue
|
||||
k = memo.get(key(r))
|
||||
if k is None:
|
||||
continue
|
||||
hit += 1
|
||||
lines += r["줄수"]
|
||||
good = k in set(L[r["id"]]["정답"]) | set(L[r["id"]]["허용"])
|
||||
ok += good
|
||||
ok_lines += r["줄수"] if good else 0
|
||||
n = sum(1 for r in ITEMS if r["파일"] == dst)
|
||||
nl = sum(r["줄수"] for r in ITEMS if r["파일"] == dst)
|
||||
print(f" {src}→{dst} {name:<22} 고유 {hit}/{n} 맞음 {ok} · 줄 {lines}/{nl} 맞음 {ok_lines}")
|
||||
|
||||
|
||||
def threshold() -> None:
|
||||
"""1순위 점수 문턱 — 없음 줄 엉뚱 후보 숨김 대 정답 1순위 잃음."""
|
||||
rows = []
|
||||
for r in ITEMS:
|
||||
m = r["맞춤"]
|
||||
if m["상태"] != "후보":
|
||||
continue
|
||||
top = (m.get("후보") or [{}])[0]
|
||||
lab = L[r["id"]]
|
||||
good = top.get("로직") in set(lab["정답"]) | set(lab["허용"])
|
||||
rows.append((top.get("점수", 0), bool(lab["정답"]), good, r["id"]))
|
||||
for cut in (0.5, 0.6, 0.8, 1.0):
|
||||
hidden_none = sum(1 for s, have, g, _ in rows if s < cut and not have)
|
||||
lost = sum(1 for s, have, g, _ in rows if s < cut and have and g)
|
||||
wrong_hidden = sum(1 for s, have, g, _ in rows if s < cut and have and not g)
|
||||
print(f" 문턱 {cut}: 없음 줄 숨김 {hidden_none} · 맞는 1순위 숨김 {lost} · 틀린 1순위 숨김 {wrong_hidden}")
|
||||
print(" 없음 줄 1순위 점수", sorted((round(s, 3), i) for s, have, g, i in rows if not have))
|
||||
|
||||
|
||||
def grams_syn(pairs):
|
||||
base = hm._grams
|
||||
|
||||
def g(text: str) -> set:
|
||||
flat = unicodedata.normalize("NFKC", str(text))
|
||||
for old, new in pairs:
|
||||
flat = flat.replace(old, new)
|
||||
return base(text) | base(flat)
|
||||
|
||||
return g
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("⑨ 기억 재사용(정답 첫 로직을 기억으로)")
|
||||
reuse("북부청임도", "북부청사방")
|
||||
reuse("북부청사방", "북부청임도")
|
||||
print("낮은 확신 문턱(지금 후보 줄)")
|
||||
threshold()
|
||||
rows = [table("지금", run())]
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep):
|
||||
rows.append(table("결합 ⑥①⑪", run(same_check_line)))
|
||||
with patched(hm, _grams=grams_syn(SYN_NORTH)):
|
||||
rows.append(table("②사전(글자 묶음만)", run()))
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, _grams=grams_syn(SYN_NORTH)):
|
||||
rows.append(table("결합 ⑥①⑪②", run(same_check_line)))
|
||||
for row in rows:
|
||||
show(row)
|
||||
(HERE / "out_memory.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""실패 줄 목록 — 이름만 · 지금 · 결합 ⑥①⑪ 의 1순위 엄격 실패(+ 5안 느슨 실패) · 갈래 꼬리표."""
|
||||
import json
|
||||
from s04_eval import BY_ID, HERE, ITEMS, L, clause_prefix, hb, hm, name_only, patched, run, tidy_keep
|
||||
from s05_more import same_check_line
|
||||
|
||||
NAMES = {}
|
||||
|
||||
|
||||
def fails(res, title):
|
||||
print("==", title)
|
||||
out = []
|
||||
for i, r in BY_ID.items():
|
||||
lab = L[i]
|
||||
top = res[i]["top"]
|
||||
s, l = set(lab["정답"]), set(lab["정답"]) | set(lab["허용"])
|
||||
if not s:
|
||||
continue
|
||||
t1 = top[0] if top else None
|
||||
if t1 in s:
|
||||
continue
|
||||
kind = "느슨맞음" if t1 in l else ("5안" if set(top[:5]) & l else "5밖")
|
||||
out.append({"id": i, "줄수": r["줄수"], "이름": r["이름"], "1위": t1, "형": kind, "갈래": lab["갈래"]})
|
||||
print(f" {i} 줄{r['줄수']:>2} {r['이름'][:20]:<20} 1위 {t1} {kind:<5} {lab['갈래']}")
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
got = {}
|
||||
got["이름만"] = fails(run(name_only), "이름만(330 앞)")
|
||||
got["지금"] = fails(run(), "지금")
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep):
|
||||
got["결합"] = fails(run(same_check_line), "결합 ⑥①⑪")
|
||||
(HERE / "out_fail.json").write_text(json.dumps(got, ensure_ascii=False, indent=1), "utf-8")
|
||||
@@ -0,0 +1,124 @@
|
||||
"""셋째 파일(다른 꼴 · STmate · 동부청 영월 2024) — 지금 읽기는 「일위대가 시트 없음」 400 이라
|
||||
여기서 따로 읽어(일위대가표 D 코드 줄 · X 기계 줄 + 단가산출근거 블록) 같은 맞춤 함수에 넣어 봄.
|
||||
|
||||
python s09_stmate.py view → 고유 줄 · 블록 글 · 지금 후보(판정용) · out_stmate.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import openpyxl
|
||||
|
||||
from s04_eval import HERE, hb, hm, name_only # noqa: F401
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
FILE = ROOT / "resources/knowledge/original/실무문서/05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx"
|
||||
_HOPYO = re.compile(r"^\s*제\s*(\d+)\s*호표")
|
||||
_CODE = re.compile(r"^([DXLMB])\d{5}$")
|
||||
|
||||
|
||||
def _num(v):
|
||||
try:
|
||||
return float(str(v).replace(",", ""))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def read():
|
||||
book = openpyxl.load_workbook(FILE, read_only=True, data_only=True)
|
||||
rows = [list(r) for r in book["일위대가표"].iter_rows(values_only=True)]
|
||||
calc = [list(r) for r in book["단가산출근거"].iter_rows(values_only=True)]
|
||||
book.close()
|
||||
lines, labor, no = [], {}, None
|
||||
for r in rows:
|
||||
head = str(r[0] or "")
|
||||
hit = _HOPYO.match(head)
|
||||
if hit:
|
||||
no = int(hit.group(1))
|
||||
continue
|
||||
code = str(r[12] or "").strip() if len(r) > 12 else ""
|
||||
m = _CODE.match(code)
|
||||
if not m or not r[0]:
|
||||
continue
|
||||
if m.group(1) == "L":
|
||||
labor[code] = str(r[0]).strip()
|
||||
if m.group(1) in "DX":
|
||||
lines.append(
|
||||
{
|
||||
"호표": no,
|
||||
"이름": str(r[0]).strip(),
|
||||
"규격": str(r[1] or "").strip(),
|
||||
"단위": str(r[3] or "").strip(),
|
||||
"단가": {"계": _num(r[4]), "노무비": _num(r[6]), "재료비": _num(r[8]), "경비": _num(r[10])},
|
||||
"갈래": "단가산출" if m.group(1) == "D" else "기계",
|
||||
"코드": code,
|
||||
}
|
||||
)
|
||||
blocks, cur = {}, None
|
||||
for at, r in enumerate(calc, 1):
|
||||
first = str(r[0] or "").strip()
|
||||
second = str(r[1] or "").strip() if len(r) > 1 else ""
|
||||
if _CODE.match(second) and second.startswith("D") and len(r) > 2 and _num(r[2]) is not None:
|
||||
cur = blocks.setdefault(second, {"글": [], "행": [at, at], "직종코드": []})
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
cur["행"][1] = at
|
||||
if second:
|
||||
cur["글"].append(second)
|
||||
lc = re.match(r"^(L\d{5})L$", first)
|
||||
if lc:
|
||||
cur["직종코드"].append(lc.group(1))
|
||||
names = hb.trade_names()
|
||||
seen, out = {}, []
|
||||
for x in lines:
|
||||
key = (x["이름"], x["규격"], x["단위"], x["코드"])
|
||||
if key in seen:
|
||||
seen[key]["줄수"] += 1
|
||||
continue
|
||||
b = blocks.get(x["코드"])
|
||||
if b:
|
||||
text = " ".join(b["글"])
|
||||
trades = []
|
||||
for c in b["직종코드"]:
|
||||
nm = names.get(re.sub(r"\s+", "", labor.get(c, "")))
|
||||
if nm and nm not in trades:
|
||||
trades.append(nm)
|
||||
x["근거"] = {"출처": hb.sources(text), "직종": trades, "행": b["행"]}
|
||||
x["단산글"] = b["글"][:40]
|
||||
x["줄수"] = 1
|
||||
x["id"] = f"Y{len(out):03d}"
|
||||
x["파일"] = "동부청영월"
|
||||
seen[key] = x
|
||||
out.append(x)
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
items = read()
|
||||
got = hm.targets()
|
||||
for x in items:
|
||||
line = {k: x.get(k) for k in ("이름", "규격", "단위", "단가", "갈래", "근거")}
|
||||
m = hm.match_line(line, [], {}, True, got)
|
||||
x["맞춤"] = m
|
||||
x["이름만"] = name_only(line, got, x)
|
||||
(HERE / "out_stmate.json").write_text(json.dumps(items, ensure_ascii=False, indent=1), "utf-8")
|
||||
print("고유", len(items), "줄", sum(x["줄수"] for x in items))
|
||||
if sys.argv[1:] == ["view"]:
|
||||
for x in items:
|
||||
b = x.get("근거") or {}
|
||||
print(f"## {x['id']} 줄{x['줄수']} {x['이름']} | {x['규격']} | {x['단위']} | {x['갈래']} | 단가 {x['단가']['계']}")
|
||||
print(f" 출처 {b.get('출처')} 직종 {b.get('직종')}")
|
||||
for t in (x.get("단산글") or [])[1:9]:
|
||||
print(f" · {' '.join(t.split())[:100]}")
|
||||
m = x["맞춤"]
|
||||
if m["상태"] == "같음":
|
||||
print(f" 같음 {m['대상']}")
|
||||
for c in m.get("후보") or []:
|
||||
why = (c.get("근거") or {}).get("까닭", "")
|
||||
print(f" - {c.get('로직')} {c['이름']} [{c['단위']}] {c['점수']} {why}")
|
||||
print(f" 이름만 {x['이름만']['top'][:3]}")
|
||||
@@ -0,0 +1,201 @@
|
||||
"""req225 C1 — 세 파일(북부청 임도 · 사방 · 동부청 영월 STmate) 함께 지표 + 방법 더
|
||||
(⑫ 블록 글 낱말 · ⑬ 같은 이름 접힘 고침 · ⑭ 부문 · 씨앗 끔). 저장소 안 고침.
|
||||
|
||||
python s10_all.py → 표 · out_all.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import s04_eval as E
|
||||
from s04_eval import clause_prefix, fix_typo, hb, hm, name_only, patched, tidy_keep
|
||||
from s05_more import compose_line, money_line, same_check_line
|
||||
|
||||
from labels import L
|
||||
from labels_y import LY
|
||||
|
||||
Y = json.loads((E.HERE / "out_stmate.json").read_text(encoding="utf-8"))
|
||||
ALL = E.ITEMS + Y
|
||||
LAB = {**L, **LY}
|
||||
BY = {r["id"]: r for r in ALL}
|
||||
GROUPS = {
|
||||
"북부청임도": [r["id"] for r in ALL if r["파일"] == "북부청임도"],
|
||||
"북부청사방": [r["id"] for r in ALL if r["파일"] == "북부청사방"],
|
||||
"동부청영월": [r["id"] for r in ALL if r["파일"] == "동부청영월"],
|
||||
}
|
||||
|
||||
|
||||
def score(results, ids):
|
||||
have = [i for i in ids if LAB[i]["정답"]]
|
||||
none = [i for i in ids if not LAB[i]["정답"]]
|
||||
|
||||
def rate(pred, w=lambda i: 1):
|
||||
tot = sum(w(i) for i in have)
|
||||
return round(100 * sum(w(i) for i in have if pred(i)) / tot, 1) if tot else 0
|
||||
|
||||
s = {i: set(LAB[i]["정답"]) for i in ids}
|
||||
lo = {i: set(LAB[i]["정답"]) | set(LAB[i]["허용"]) for i in ids}
|
||||
top = {i: results[i]["top"] for i in ids}
|
||||
lines = lambda i: BY[i]["줄수"] # noqa: E731
|
||||
return {
|
||||
"고유": len(ids),
|
||||
"있음": len(have),
|
||||
"없음": len(none),
|
||||
"1위 엄격": rate(lambda i: bool(top[i]) and top[i][0] in s[i]),
|
||||
"1위 느슨": rate(lambda i: bool(top[i]) and top[i][0] in lo[i]),
|
||||
"5안 엄격": rate(lambda i: bool(set(top[i][:5]) & s[i])),
|
||||
"5안 느슨": rate(lambda i: bool(set(top[i][:5]) & lo[i])),
|
||||
"줄 1위 느슨": rate(lambda i: bool(top[i]) and top[i][0] in lo[i], lines),
|
||||
"줄 5안 느슨": rate(lambda i: bool(set(top[i][:5]) & lo[i]), lines),
|
||||
"없음인데 후보": sum(1 for i in none if top[i]),
|
||||
}
|
||||
|
||||
|
||||
def table(name, results):
|
||||
row = {"이름": name, "전체": score(results, list(BY))}
|
||||
row.update({g: score(results, ids) for g, ids in GROUPS.items()})
|
||||
return row
|
||||
|
||||
|
||||
def show(row):
|
||||
def c(s):
|
||||
return f"{s['1위 엄격']:>5}/{s['1위 느슨']:>5} {s['5안 엄격']:>5}/{s['5안 느슨']:>5}"
|
||||
|
||||
print(
|
||||
f"{row['이름'][:24]:<24}| 전체 {c(row['전체'])} | 임도 {c(row['북부청임도'])} | 사방 {c(row['북부청사방'])} | 영월 {c(row['동부청영월'])} | 없음후보 {row['전체']['없음인데 후보']}"
|
||||
)
|
||||
|
||||
|
||||
def run(fn=None):
|
||||
hm._targets.cache_clear()
|
||||
hb._ours.cache_clear()
|
||||
got = hm.targets()
|
||||
out = {}
|
||||
for r in ALL:
|
||||
line = {k: r.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")}
|
||||
CURRENT["r"] = r
|
||||
out[r["id"]] = (fn or E.default_line)(line, got, r)
|
||||
return out
|
||||
|
||||
|
||||
CURRENT = {"r": None}
|
||||
|
||||
|
||||
# ── ⑫ 블록 글 낱말 — 우리 로직 이름 낱말이 엑셀 단가산출 블록 글에 나오면 덤 · 후보에 들임 ──────
|
||||
_GENERIC = frozenset(
|
||||
"인력 기계 장비 소계 합계 노무비 재료비 경비 적용 기준 별도계상 별도 인부 보통인부 특별인부 설치 해체 "
|
||||
"운반 작업 시공 사용 포함 공구손료 경장비 손료 기계경비 재료 인건비 품셈 표준품셈 건설표준품셈 이하 이상".split()
|
||||
)
|
||||
|
||||
|
||||
def block_words(r) -> str:
|
||||
text = " ".join((r.get("단산글") or [])[:30])
|
||||
return re.sub(r"\s+", "", text)
|
||||
|
||||
|
||||
def our_words(name):
|
||||
return {w for w in hb._loose_words(name) if w not in _GENERIC and len(w) >= 2}
|
||||
|
||||
|
||||
def block_line(line, got, r, pool_n=40):
|
||||
m = hm.match_line(line, [], {}, True, got)
|
||||
if m["상태"] == "같음":
|
||||
return {"상태": "같음", "top": [m["대상"]["로직"]]}
|
||||
text = block_words(r)
|
||||
basis = hb.candidates(line, got)
|
||||
named = hm.candidates(line, "로직", got, None)
|
||||
ordered = hb.merge(basis, named)[:pool_n]
|
||||
have = {c.get("로직") for c in ordered}
|
||||
if text:
|
||||
for one in got["로직"]:
|
||||
ws = our_words(one["이름"])
|
||||
if one["로직"] not in have and ws and all(w in text for w in ws):
|
||||
ordered.append({"로직": one["로직"], "점수": 0.3})
|
||||
have.add(one["로직"])
|
||||
rescored = []
|
||||
for at, c in enumerate(ordered):
|
||||
k = c.get("로직")
|
||||
nm = next((x["이름"] for x in got["로직"] if x["로직"] == k), "")
|
||||
ws = our_words(nm)
|
||||
hit = sum(1 for w in ws if w in text) / len(ws) if ws and text else 0
|
||||
rescored.append((c["점수"] + 0.4 * hit, -at, k))
|
||||
rescored.sort(reverse=True)
|
||||
top = [k for _, _, k in rescored]
|
||||
first = next((c.get("로직") for c in named), None)
|
||||
five = top[:5] if first in top[:5] or first is None else top[:4] + [first]
|
||||
return {"상태": "후보" if five else "없음", "top": five}
|
||||
|
||||
|
||||
# ── ⑬ 같은 이름 접힘 고침 — 이름 후보가 (이름 · 규격 · 단위) 같으면 하나만 남기던 것 → 로직마다 ──────
|
||||
def candidates_nofold(line, pool, got=None, limit=5):
|
||||
got = got or hm.targets()
|
||||
me = hm.tidy(line.get("이름"))
|
||||
grams, unit = hm._grams(me), line.get("단위")
|
||||
text = hm.mm.plain_spec(f"{line.get('이름')}{line.get('규격')}")
|
||||
mine = hm.words(line.get("이름"))
|
||||
scored = []
|
||||
for one in got[pool]:
|
||||
if not grams & one["grams"] or not hm.near(mine, one["words"]):
|
||||
continue
|
||||
s = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"]))
|
||||
s += 1 if one["tidy"] == me else 0
|
||||
s += 0.15 if hm.fits(unit, one["단위"]) else 0
|
||||
s += 0.05 if one.get("산림") else 0
|
||||
s += 0.5 if one.get("spec") and one["spec"] in text else 0
|
||||
scored.append((s, one))
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
return [{**hm._public(o), "점수": round(s, 3)} for s, o in scored[: limit or None]]
|
||||
|
||||
|
||||
# ── ⑭ 부문 — 엑셀 글 「공통품셈 · 토목품셈 · (건축)」 이 있으면 그 부문 조항만 「조항 같음」 ────────
|
||||
_PART = re.compile(r"(공통|토목|건축|기계설비|유지관리)")
|
||||
|
||||
|
||||
def clause_part(theirs, our):
|
||||
got = clause_prefix(theirs, our)
|
||||
r = CURRENT["r"] or {}
|
||||
parts = set(_PART.findall(" ".join((r.get("단산글") or [])[:6])))
|
||||
if got[0] >= 0.9 and parts and our["책"] == "건설품셈" and our["부문"] not in parts:
|
||||
return 0.5, got[1], "앞 마디 같음"
|
||||
return got
|
||||
|
||||
|
||||
def typo_same(line, got, r):
|
||||
b = dict(line.get("근거") or {})
|
||||
if b.get("출처"):
|
||||
b["출처"] = fix_typo(b["출처"])
|
||||
line = {**line, "근거": b}
|
||||
return same_check_line(line, got, r)
|
||||
|
||||
|
||||
def no_seeds():
|
||||
return patched(hm, _seeds=lambda: {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rows = []
|
||||
rows.append(table("지금", run()))
|
||||
with no_seeds():
|
||||
rows.append(table("지금(씨앗 끔)", run()))
|
||||
rows.append(table("이름만(씨앗 끔)", run(name_only)))
|
||||
rows.append(table("이름만(씨앗 켬)", run(name_only)))
|
||||
with patched(hm, tidy=tidy_keep):
|
||||
rows.append(table("①이름 다듬기", run()))
|
||||
with patched(hb, _clause=clause_prefix):
|
||||
rows.append(table("⑥조항 윗마디", run()))
|
||||
rows.append(table("⑦직종·장비", run(compose_line)))
|
||||
rows.append(table("⑧금액", run(money_line)))
|
||||
rows.append(table("⑪같음 근거 검사", run(same_check_line)))
|
||||
rows.append(table("⑫블록 글 낱말", run(block_line)))
|
||||
with patched(hm, candidates=candidates_nofold):
|
||||
rows.append(table("⑬같은 이름 접힘 고침", run()))
|
||||
with patched(hb, _clause=clause_part):
|
||||
rows.append(table("⑭부문(+⑥)", run()))
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, candidates=candidates_nofold):
|
||||
rows.append(table("결합 ⑥①⑪⑬+오타", run(typo_same)))
|
||||
with patched(hb, _clause=clause_part), patched(hm, tidy=tidy_keep, candidates=candidates_nofold):
|
||||
rows.append(table("결합 ⑥①⑪⑬⑭+오타", run(typo_same)))
|
||||
rows.append(table("결합 …+⑫", run(lambda a, b, c: typo_same(a, b, c) if hm.match_line(a, [], {}, True, b)["상태"] == "같음" else block_line(a, b, c))))
|
||||
for row in rows:
|
||||
show(row)
|
||||
(E.HERE / "out_all.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8")
|
||||
@@ -0,0 +1,29 @@
|
||||
from s10_all import * # noqa: F401,F403
|
||||
|
||||
|
||||
def mark(i, top):
|
||||
s, l = set(LAB[i]["정답"]), set(LAB[i]["정답"]) | set(LAB[i]["허용"])
|
||||
t1 = top[0] if top else None
|
||||
return ("O" if t1 in s else "o" if t1 in l else "x") + ("5" if set(top[:5]) & l else "-")
|
||||
|
||||
|
||||
def diff(a, b, title, only="Y"):
|
||||
print("==", title)
|
||||
for i in BY:
|
||||
if not LAB[i]["정답"] or not i.startswith(only):
|
||||
continue
|
||||
ma, mb = mark(i, a[i]["top"]), mark(i, b[i]["top"])
|
||||
if ma != mb:
|
||||
print(f" {i} {BY[i]['이름'][:18]:<18} {ma}->{mb} {a[i]['top'][:2]} -> {b[i]['top'][:2]} 정답{LAB[i]['정답']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
base = run()
|
||||
with no_seeds():
|
||||
diff(base, run(), "씨앗 끔")
|
||||
diff(base, run(same_check_line), "⑪")
|
||||
with patched(hm, candidates=candidates_nofold):
|
||||
diff(base, run(), "⑬")
|
||||
diff(base, run(block_line), "⑫ 영월")
|
||||
diff(base, run(block_line), "⑫ 임도", "N")
|
||||
diff(base, run(block_line), "⑫ 사방", "S")
|
||||
@@ -0,0 +1,27 @@
|
||||
"""화면 단추 정확도 — [같음 모두 확정] · [근거 맞음 모두 확정](TS basisOk · basisPick 그대로 옮김)."""
|
||||
from s10_all import ALL, LAB
|
||||
|
||||
|
||||
def basis_ok(b, line_basis):
|
||||
if not b:
|
||||
return False
|
||||
trades = (line_basis or {}).get("직종") or []
|
||||
return b["까닭"].startswith("조항 같음") and bool(b["단위"]) and (bool(b["직종"]) or not any(x != "보통인부" for x in trades))
|
||||
|
||||
|
||||
for f in ("북부청임도", "북부청사방", "동부청영월"):
|
||||
same = [r for r in ALL if r["파일"] == f and r["맞춤"]["상태"] == "같음" and (r["맞춤"].get("환산") or {}).get("배") is not None]
|
||||
s_ok = sum(1 for r in same if r["맞춤"]["대상"]["로직"] in LAB[r["id"]]["정답"])
|
||||
l_ok = sum(1 for r in same if r["맞춤"]["대상"]["로직"] in set(LAB[r["id"]]["정답"]) | set(LAB[r["id"]]["허용"]))
|
||||
picks = []
|
||||
for r in ALL:
|
||||
if r["파일"] != f or r["맞춤"]["상태"] != "후보":
|
||||
continue
|
||||
c = next((c for c in r["맞춤"].get("후보") or [] if basis_ok(c.get("근거"), r.get("근거"))), None)
|
||||
if c:
|
||||
picks.append((r, c["로직"]))
|
||||
p_s = sum(1 for r, k in picks if k in LAB[r["id"]]["정답"])
|
||||
p_l = sum(1 for r, k in picks if k in set(LAB[r["id"]]["정답"]) | set(LAB[r["id"]]["허용"]))
|
||||
wrong = [(r["id"], r["이름"], k) for r, k in picks if k not in set(LAB[r["id"]]["정답"]) | set(LAB[r["id"]]["허용"])]
|
||||
cand = sum(1 for r in ALL if r["파일"] == f and r["맞춤"]["상태"] == "후보")
|
||||
print(f"{f}: 같음 확정 {len(same)} 줄(고유) 엄격 맞음 {s_ok} 느슨 {l_ok} | 근거 맞음 확정 {len(picks)}/{cand} 엄격 {p_s} 느슨 {p_l} 틀림 {wrong}")
|
||||
@@ -0,0 +1,26 @@
|
||||
"""정답 로직과 엑셀 줄 단위 — 환산이 「입력」(사람이 넣어야)인 줄 수 · 단위 별칭(hr=시간 · 공/㎥=공㎥ · 개소=개=본) 뒤."""
|
||||
from s10_all import ALL, LAB, hm
|
||||
|
||||
units = {x["로직"]: x["단위"] for x in hm.targets()["로직"]}
|
||||
ALIAS = {"hr": "시간", "h": "시간", "공/㎥": "공㎥", "개소": "개", "본": "개", "ea": "개", "EA": "개"}
|
||||
|
||||
|
||||
def norm(u):
|
||||
u = str(u or "").strip()
|
||||
return ALIAS.get(u, ALIAS.get(u.lower(), u))
|
||||
|
||||
|
||||
rows = []
|
||||
for r in ALL:
|
||||
lab = LAB[r["id"]]
|
||||
if not lab["정답"]:
|
||||
continue
|
||||
k = lab["정답"][0]
|
||||
want = units.get(k)
|
||||
a = hm.factor(r["단위"], want, r.get("규격"))
|
||||
b = hm.factor(norm(r["단위"]), " · ".join(norm(x.strip()) for x in str(want).split("·")), r.get("규격"))
|
||||
rows.append((r["id"], r["이름"], r["단위"], want, a["갈래"], b["갈래"]))
|
||||
print("정답 줄", len(rows), "· 환산 입력(지금)", sum(1 for x in rows if x[4] == "입력"), "· 별칭 뒤", sum(1 for x in rows if x[5] == "입력"))
|
||||
for x in rows:
|
||||
if x[4] == "입력":
|
||||
print(" ", x)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""금액을 「확인 표시」로 쓸 때 — 지금 1순위(또는 같음 대상)의 우리 값 ÷ 엑셀 단가가 ±5% · ±15% 안이면 맞는 비율."""
|
||||
from s05_more import money_ratio
|
||||
from s10_all import ALL, LAB, hm
|
||||
|
||||
units = {x["로직"]: x["단위"] for x in hm.targets()["로직"]}
|
||||
buckets = {0.05: [0, 0], 0.15: [0, 0], "밖": [0, 0], "못셈": [0, 0]}
|
||||
for r in ALL:
|
||||
m = r["맞춤"]
|
||||
top = (m.get("대상") or {}).get("로직") if m["상태"] == "같음" else ((m.get("후보") or [{}])[0].get("로직"))
|
||||
if not top:
|
||||
continue
|
||||
lab = LAB[r["id"]]
|
||||
good = top in set(lab["정답"]) | set(lab["허용"])
|
||||
line = {k: r.get(k) for k in ("이름", "규격", "단위", "단가", "갈래", "근거")}
|
||||
q = money_ratio(line, top, units)
|
||||
key = "못셈" if q is None else 0.05 if abs(q - 1) <= 0.05 else 0.15 if abs(q - 1) <= 0.15 else "밖"
|
||||
buckets[key][0] += 1
|
||||
buckets[key][1] += good
|
||||
for k, (n, g) in buckets.items():
|
||||
print(f" |비-1| {k}: 1순위 {n} 개 중 맞음 {g}")
|
||||
@@ -0,0 +1,26 @@
|
||||
import json
|
||||
from s10_all import BY, LAB, E, no_seeds, run, name_only
|
||||
|
||||
|
||||
def fails(res):
|
||||
out = {}
|
||||
for i in BY:
|
||||
s = set(LAB[i]["정답"])
|
||||
if not s:
|
||||
continue
|
||||
top = res[i]["top"]
|
||||
if not top or top[0] not in s:
|
||||
lo = s | set(LAB[i]["허용"])
|
||||
out[i] = {"1위": top[0] if top else None, "형": "느슨맞음" if top and top[0] in lo else ("5안" if set(top[:5]) & lo else "5밖")}
|
||||
return out
|
||||
|
||||
|
||||
with no_seeds():
|
||||
a = fails(run(name_only))
|
||||
b = fails(run())
|
||||
(E.HERE / "out_fails_all.json").write_text(json.dumps({"이름만": a, "지금": b}, ensure_ascii=False, indent=1), "utf-8")
|
||||
for name, f in (("이름만(씨앗 끔)", a), ("지금", b)):
|
||||
print("==", name, len(f))
|
||||
for i, v in f.items():
|
||||
if i.startswith("Y"):
|
||||
print(f" {i} {BY[i]['이름'][:22]:<22} {v}")
|
||||
@@ -0,0 +1,19 @@
|
||||
"""실패 갈래 표 — out_fails_all.json(이름만 · 지금 실패 목록) × cats.py(주 갈래)."""
|
||||
import json
|
||||
from cats import NAME, NOW, NOTE, ORDER
|
||||
from s10_all import BY
|
||||
|
||||
f = json.loads(open("out_fails_all.json", encoding="utf-8").read())
|
||||
miss = {k: [i for i in f[k] if i not in (NAME if k == "이름만" else NOW)] for k in f}
|
||||
print("갈래 빠짐", miss)
|
||||
rows = []
|
||||
for cat in ORDER:
|
||||
def cnt(k, table):
|
||||
ids = [i for i in f[k] if (table.get(i) or [None])[0] == cat]
|
||||
side = [i for i in f[k] if cat in (table.get(i) or [])[1:]]
|
||||
return len(ids), sum(BY[i]["줄수"] for i in ids), len(side)
|
||||
a, b = cnt("이름만", NAME), cnt("지금", NOW)
|
||||
rows.append((cat, a, b))
|
||||
print(f"{cat:<22} 이름만 주 {a[0]:>2}(줄 {a[1]:>3}) 곁 {a[2]:>2} | 지금 주 {b[0]:>2}(줄 {b[1]:>3}) 곁 {b[2]:>2}")
|
||||
print("합", len(f["이름만"]), sum(BY[i]["줄수"] for i in f["이름만"]), "|", len(f["지금"]), sum(BY[i]["줄수"] for i in f["지금"]))
|
||||
json.dump(rows, open("out_cats.json", "w", encoding="utf-8"), ensure_ascii=False)
|
||||
@@ -0,0 +1,57 @@
|
||||
"""작은 고침 묶음 · ⑪ 판 지킴(옛 판 글이면 같음 안 내림 · 근거 후보가 직종 또는 뜻 낱말을 나눠야)."""
|
||||
import json
|
||||
import re
|
||||
|
||||
from s04_eval import clause_prefix, fix_typo, hb, hm, patched, tidy_keep
|
||||
from s05_more import same_check_line
|
||||
from s10_all import CURRENT, E, _GENERIC, candidates_nofold, run, show, table, typo_same
|
||||
|
||||
_OLD = re.compile(r"(20(1\d|2[0-4])|1\d년|2[0-4]년|임도품셈|임도표준품셈|사방품셈)")
|
||||
|
||||
|
||||
def guarded_same(line, got, r):
|
||||
text = " ".join((r.get("단산글") or [])[:6])
|
||||
m = hm.match_line(line, [], {}, True, got)
|
||||
if m["상태"] != "같음" or _OLD.search(text):
|
||||
return same_check_line(line, got, r) if m["상태"] != "같음" else {"상태": "같음", "top": [m["대상"]["로직"]]}
|
||||
theirs = (line.get("근거") or {}).get("출처") or []
|
||||
if not theirs:
|
||||
return {"상태": "같음", "top": [m["대상"]["로직"]]}
|
||||
tgt = m["대상"]["로직"]
|
||||
our = hb.ours().get(tgt)
|
||||
if our and hb._clause(theirs, our)[0] >= 0.9:
|
||||
return {"상태": "같음", "top": [tgt]}
|
||||
mine = {w for w in hb._loose_words(f"{line.get('이름')}") if w not in _GENERIC}
|
||||
|
||||
def strong(c):
|
||||
if c["근거"]["직종"]:
|
||||
return True
|
||||
theirs_w = {w for w in hb._loose_words(c["이름"]) if w not in _GENERIC}
|
||||
return any(a in b or b in a for a in mine for b in theirs_w)
|
||||
|
||||
basis = [c for c in hb.candidates(line, got) if c["근거"]["까닭"].startswith("조항 같음") and strong(c)]
|
||||
if not basis:
|
||||
return {"상태": "같음", "top": [tgt]}
|
||||
top = list(dict.fromkeys([basis[0]["로직"], tgt] + [c["로직"] for c in basis[1:]]))[:5]
|
||||
return {"상태": "후보", "top": top}
|
||||
|
||||
|
||||
def typo_guard(line, got, r):
|
||||
b = dict(line.get("근거") or {})
|
||||
if b.get("출처"):
|
||||
b["출처"] = fix_typo(b["출처"])
|
||||
line = {**line, "근거": b}
|
||||
return guarded_same(line, got, r)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rows = [table("지금", run())]
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold):
|
||||
rows.append(table("작은 고침 ⑥⑬+오타", run(lambda a, b, c: E.default_line({**a, "근거": {**(a.get("근거") or {}), "출처": fix_typo((a.get("근거") or {}).get("출처") or [])}} if a.get("근거") else a, b, c))))
|
||||
rows.append(table("⑥⑬+오타+⑪(판 지킴)", run(typo_guard)))
|
||||
rows.append(table("⑪(판 지킴)만", run(guarded_same)))
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, candidates=candidates_nofold):
|
||||
rows.append(table("⑥①⑪(판 지킴)⑬+오타", run(typo_guard)))
|
||||
for row in rows:
|
||||
show(row)
|
||||
(E.HERE / "out_guard.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8")
|
||||
@@ -0,0 +1,265 @@
|
||||
"""최종 보고 — req225/C1_이름맞춤_실측.md 를 결과 파일(out_*.json) · 정답 표시(labels*.py) · 갈래(cats.py)로 만듦."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from cats import NOTE, ORDER
|
||||
from s10_all import ALL, LAB
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
OUT = HERE.parent / "C1_이름맞춤_실측.md"
|
||||
|
||||
|
||||
def load(name):
|
||||
return {r["이름"]: r for r in json.loads((HERE / name).read_text(encoding="utf-8"))}
|
||||
|
||||
|
||||
ALLR = load("out_all.json")
|
||||
GUARD = load("out_guard.json")
|
||||
NAME = load("out_name_all.json")
|
||||
CATS = json.loads((HERE / "out_cats.json").read_text(encoding="utf-8"))
|
||||
CAT = {c: (a, b) for c, a, b in CATS}
|
||||
|
||||
|
||||
def pair(s, a, b):
|
||||
return f"{s[a]:.1f} / {s[b]:.1f}"
|
||||
|
||||
|
||||
def t1(s):
|
||||
return pair(s, "1위 엄격", "1위 느슨")
|
||||
|
||||
|
||||
def t5(s):
|
||||
return pair(s, "5안 엄격", "5안 느슨")
|
||||
|
||||
|
||||
# ── 지표 표 ─────────────────────────────────────────────────────────────
|
||||
name0 = ALLR["이름만(씨앗 끔)"]
|
||||
now = ALLR["지금"]
|
||||
best = GUARD["⑥⑬+오타+⑪(판 지킴)"]
|
||||
lines_of = {g: sum(r["줄수"] for r in ALL if r["파일"] == g) for g in ("북부청임도", "북부청사방", "동부청영월")}
|
||||
none_lines = {g: sum(r["줄수"] for r in ALL if r["파일"] == g and not LAB[r["id"]]["정답"]) for g in lines_of}
|
||||
metric_rows = []
|
||||
for g, label in (("북부청임도", "북부청 임도"), ("북부청사방", "북부청 사방"), ("동부청영월", "동부청 영월*"), ("전체", "세 파일")):
|
||||
s0, s1, s2 = name0[g], now[g], best[g]
|
||||
nl = sum(lines_of.values()) if g == "전체" else lines_of[g]
|
||||
nn = sum(none_lines.values()) if g == "전체" else none_lines[g]
|
||||
metric_rows.append(
|
||||
f"| {label} | {s1['고유']}({nl}) | {s1['있음']} | {s1['없음']}({nn}) · {100 * s1['없음'] / s1['고유']:.1f}% "
|
||||
f"| {t1(s0)} | {t5(s0)} | {t1(s1)} | {t5(s1)} | {t1(s2)} | {t5(s2)} |"
|
||||
)
|
||||
|
||||
# ── 실패 갈래 표 ───────────────────────────────────────────────────────
|
||||
cat_rows = []
|
||||
for cat, a, b in CATS:
|
||||
ex = " · ".join(NOTE.get(cat, [])[:3])
|
||||
cat_rows.append(f"| {cat} | {a[0]}({a[1]}) · 곁 {a[2]} | {b[0]}({b[1]}) · 곁 {b[2]} | {ex} |")
|
||||
tot_a = sum(x[1][0] for x in CATS)
|
||||
tot_b = sum(x[2][0] for x in CATS)
|
||||
tot_al = sum(x[1][1] for x in CATS)
|
||||
tot_bl = sum(x[2][1] for x in CATS)
|
||||
|
||||
|
||||
# ── 부록 C — 정답 표시 한 줄씩 ──────────────────────────────────────────
|
||||
def top_of(r):
|
||||
m = r["맞춤"]
|
||||
if m["상태"] == "같음":
|
||||
return [m["대상"]["로직"]], "같음"
|
||||
return [c.get("로직") for c in m.get("후보") or [] if c.get("로직")], m["상태"]
|
||||
|
||||
|
||||
def mark(i, top):
|
||||
s, lo = set(LAB[i]["정답"]), set(LAB[i]["정답"]) | set(LAB[i]["허용"])
|
||||
if not s:
|
||||
return "없음 줄 · " + ("엉뚱 후보 보임" if top else "후보 없음")
|
||||
t = top[0] if top else None
|
||||
if t in s:
|
||||
return "✓"
|
||||
if t in lo:
|
||||
return "△(허용)"
|
||||
return "5안" if set(top[:5]) & lo else "✗"
|
||||
|
||||
|
||||
app = []
|
||||
for r in ALL:
|
||||
i = r["id"]
|
||||
lab = LAB[i]
|
||||
top, state = top_of(r)
|
||||
right = " · ".join(lab["정답"]) or "우리에 없음"
|
||||
extra = f" · 허용 {' · '.join(lab['허용'])}" if lab.get("허용") else ""
|
||||
part = f" · 부분 {' · '.join(lab['부분'])}" if lab.get("부분") else ""
|
||||
guess = " · 짐작" if lab.get("짐작") else ""
|
||||
spec = f" · {r['규격']}" if r.get("규격") else ""
|
||||
app.append(
|
||||
f"| {i} | {r['이름']}{spec} [{r['단위']}] ×{r['줄수']} | {right}{extra}{part}{guess} | {lab['근거']} | {state} {top[0] if top else '—'} {mark(i, top)} |"
|
||||
)
|
||||
|
||||
# ── 방법별 표(숫자) ────────────────────────────────────────────────────
|
||||
A = ALLR
|
||||
N = NAME
|
||||
G = GUARD
|
||||
|
||||
|
||||
def cell(row):
|
||||
s = row["전체"]
|
||||
return f"{t1(s)} | {t5(s)}"
|
||||
|
||||
|
||||
meth = [
|
||||
("기준 ① 이름만(330 앞 · 씨앗 끔)", cell(A["이름만(씨앗 끔)"]), "—", "—"),
|
||||
("기준 ② 지금(330 뒤)", cell(A["지금"]), "—", "—"),
|
||||
("① 이름 다듬기(괄호 낱말 살림 · 수 · 단위 · 규격 조각 뺌) — 이름만에", cell(N["이름만+①다듬기"]), "작음", "괄호 속 규격이 엉뚱 후보를 끌어옴 · 느슨 1위 −7.1"),
|
||||
("① 이름 다듬기 — 지금에", cell(A["①이름 다듬기"]), "작음", "영월 −2.9 · 사방 터파기(토사)만 나아짐"),
|
||||
("② 같은 뜻 말 사전(임도 파일에서 16쌍) — 이름만에", cell(N["이름만+②사전(임도에서 뽑음)"]), "작음(사전) · 키우기 큼", "사방(처음 봄) 느슨 1위 62.8→69.8 · 영월(다른 꼴) 61.8→52.9"),
|
||||
("② 같은 뜻 말 사전 — 지금에", cell(N["지금+②사전"]), "작음", "레미콘 → 산림 GF000155 로 넘어감(이름 같음 덤 + 산림 덤) · 엄격 −7.1"),
|
||||
("③ 낱말 차례 무시(뜻 낱말 겹침 덤) — 이름만에", cell(N["이름만+③낱말 차례 무시"]), "작음", "다섯 안 느슨 −5.6 · 한국어 붙여 쓴 이름이라 효과 적음"),
|
||||
("④ 글자 1·2·3그램 — 이름만에", cell(N["이름만+④1·2·3그램"]), "작음", "변화 거의 없음"),
|
||||
("④ difflib(가장 긴 같은 조각) — 이름만에", cell(N["이름만+④difflib"]), "작음", "다섯 안 엄격 ↑ · 느슨 ↓ · 없음 줄 엉뚱 후보 4→10"),
|
||||
("⑤ 단위 거름망(단위 안 맞으면 뺌) — 지금에", cell(N["지금+⑤단위 거름망"]), "작음", "해로움 — 맞는 짝 단위가 달라(개소↔M · 개↔㎡ · 조↔㎡ · hr↔시간) 1위 엄격 −7.9"),
|
||||
("⑥ 근거 조항 윗마디(엑셀 6-2 ⊃ 우리 6-2-2) — 지금에", cell(A["⑥조항 윗마디"]), "작음(_clause 몇 줄)", "낮음 — 윗마디 = 0.9 · 직종 · 낱말 거름 그대로"),
|
||||
("⑦ 직종 · 장비 구성 겹침(조항 없이도) — 지금에", cell(A["⑦직종·장비"]), "중간", "효과 작음 — 직종은 이미 근거 점수에 듦"),
|
||||
("⑧ 금액(우리 값 ÷ 엑셀 단가 ±15% 덤) — 지금에", cell(A["⑧금액"]), "중간", "해로움 — 복합 줄 · 입력 기본값 탓 · 확인 표시로는 ±5% 안 1순위 21 중 20 맞음"),
|
||||
("⑪ 같음도 근거 검사(판 지킴 없이) — 지금에", cell(A["⑪같음 근거 검사"]), "중간", "영월 −5.8 — 옛 판 1-9-3 이 지금 낙석방지책으로 걸림"),
|
||||
("⑪ 같음도 근거 검사(판 지킴: 옛 판 글이면 안 내림 · 직종/뜻 낱말 요구)", cell(G["⑪(판 지킴)만"]), "중간", "낮음 — 같음 → 후보로 바뀌어 사람 확정 한 번 더"),
|
||||
("⑫ 블록 글 낱말(우리 이름 낱말이 엑셀 산출 글에 나오면 덤)", cell(A["⑫블록 글 낱말"]), "중간", "섞임 — 느슨 ↑ 엄격 ↓(블록에 단위중량 · 자재 이름이 섞임)"),
|
||||
("⑬ 같은 이름 접힘 고침(산림·건설 같은 이름을 하나로 접던 것)", cell(A["⑬같은 이름 접힘 고침"]), "작음(중복 빼기 열쇠)", "다섯 안에 같은 이름 둘 → 화면에 책 표시 필요"),
|
||||
("⑭ 부문(공통 · 토목 글로 부문 가름)", cell(A["⑭부문(+⑥)"]), "작음", "이 표본에선 변화 0"),
|
||||
("책 맞춤 덤(엑셀 출처 책 = 우리 책 +0.2)", cell(N["지금+책 맞춤 덤"]), "작음", "섞임 — 영월 +11.8 · 임도 −2.0(건설 삭제 조항 · 산림에만 있는 일)"),
|
||||
("결합 ⑥ + ⑬ + 조항 오타", cell(G["작은 고침 ⑥⑬+오타"]), "작음", "낮음"),
|
||||
("결합 ⑥ + ⑬ + 조항 오타 + ⑪(판 지킴) ← 추천", cell(G["⑥⑬+오타+⑪(판 지킴)"]), "작음~중간", "낮음 — 같음 11 줄이 후보로 바뀜(사람 확정 한 번 더)"),
|
||||
("결합 + ① 다듬기", cell(G["⑥①⑪(판 지킴)⑬+오타"]), "중간", "영월 기초잡석 · 큰돌 채집 둘 뒤집힘"),
|
||||
]
|
||||
meth_rows = [f"| {a} | {b} | {c} | {d} |" for a, b, c, d in meth]
|
||||
|
||||
TEXT = f"""# C1 외부 일위대가 이름 맞춤 실측 (요구 225 · 검토)
|
||||
|
||||
M02 「외부 일위대가 가져오기」 맞춤을 실제 엑셀로 재고 실패 까닭 · 개선 방법 효과를 가늠한 기록. 코드 안 고침(읽기 · 스크래치패드 흉내만).
|
||||
기준 = main_laptop_1 31e6adf7d · 우리 돈 로직 1,438(산림 300 · 건설 1,138) · 기억 없음(층 기억 빈 것)으로 잼.
|
||||
|
||||
## 1. 결론 요약
|
||||
|
||||
1. 잰 자료 = 북부청 임도 · 사방(지금 올라가는 꼴) + 동부청 영월 2024(STmate 꼴 · 지금은 「일위대가 시트 없음」 400 → 스크래치패드에서 따로 읽어 같은 맞춤 함수에 넣음). 로직 줄 고유 136(줄 504) 전수 정답 표시 · 정답 있음 126 · 우리에 없음 10(7.4%).
|
||||
2. 이름만(330 앞) 1순위 엄격 {t1(name0['전체']).split(' / ')[0]}% · 느슨 {t1(name0['전체']).split(' / ')[1]}% · 다섯 안 {t5(name0['전체'])}% — 이름 문제 실재(같은 뜻 다른 말 · 산림/건설 책 갈래 · 레미콘 같은 줄임말이 주 까닭).
|
||||
3. 지금(330 근거 맞춤 뒤) 1순위 {t1(now['전체'])}% · 다섯 안 {t5(now['전체'])}% — 엑셀 단가산출 조항 · 직종이 이름 문제 대부분을 덮음(+37pt). 줄 무게로는 1순위 느슨 78.1% · 다섯 안 느슨 97.9%.
|
||||
4. 지금 남은 1순위 실패 36 줄(고유)의 주 까닭 = 책 갈래 11(「같음」 자동 대상이 엑셀 출처와 다른 책 10 · 이름 같음 덤이 근거를 이김 1) · 같은 뜻 다른 말 11(고임돌 · 조약돌 · 소운반 · 철골설치 · 측구수로관) · 우리 로직이 더 잘게 5(철근 = 가공 + 조립) · 그 밖 5(같은 이름 접힘 · 이름 같음 덤) · 장비 줄 2 · 복합 1 · 괄호 1.
|
||||
5. 이름 쪽 손질(다듬기 · n-그램 · 낱말 차례 · 단위 거름망)은 효과 없거나 해로움. 사전(임도에서 16쌍)은 사방 +7.0 · 영월 −8.9(느슨 1위) → 한 파일로 만든 사전은 다른 꼴에 안 통함.
|
||||
6. 효과 큰 것 = 근거 쪽 작은 고침 — 조항 윗마디(6-2 ⊃ 6-2-2) · 같은 이름 접힘 고침 · 조항 오타 → 1순위 {t1(G['작은 고침 ⑥⑬+오타']['전체'])}% · 여기에 「같음도 근거 검사(판 지킴)」 더하면 {t1(best['전체'])}% · 다섯 안 {t5(best['전체'])}%.
|
||||
7. 금액(우리 값 ÷ 엑셀 단가)은 순위 점수로 넣으면 엄격 −4.7pt(복합 줄 · 입력 기본값) · 확인 표시로는 ±5% 안 1순위 21 중 20 맞음(95%).
|
||||
8. 기억 — 지금 열쇠(품명|규격|단위)로는 같은 회사 다른 파일 재사용 고유 4/48 · 둘째 열쇠(품명|단위)면 15/48(모두 맞음) · 씨앗(사용자 실무 이름 80)이 영월 1순위 엄격 44.1 → 67.6.
|
||||
9. 단추 정밀 — [근거 맞음 모두 확정] 고른 55 줄(고유) 모두 맞음(엄격 54) · [같음 모두 확정] 25 중 엄격 20(다른 책 5 · 느슨은 25).
|
||||
10. 「다른 외부문서도 같음」의 실제 걸림돌 = 저장소 실무 내역 xlsx 6 개를 열어 보니 5 개가 STmate 꼴(일위대가표 · 단가산출근거 · D/M/L/X 코드) → 지금은 올리기부터 400 · 이름 맞춤 앞 단계 문제.
|
||||
|
||||
## 2. 지표 표
|
||||
|
||||
고유 줄 = 같은 (품명 · 규격 · 단위 · 단산 번호) 한 번 셈(확정 한 번이면 같은 열쇠 줄 모두 기억으로 들어감). 칸 = 엄격 / 느슨(%). 엄격 = 엑셀 조항 · 품과 같은 우리 로직 · 느슨 = 같은 일의 다른 책 로직 · 복합의 곁 몫까지. 정답 있는 줄만 분모 · 「우리에 없음」은 따로.
|
||||
\\* 영월 = STmate 꼴을 스크래치패드에서 읽은 가정(지금 화면은 400). 「지금」 · 「추천 결합」은 기억 없음 · 씨앗 켬.
|
||||
|
||||
| 파일 | 고유 줄(줄) | 정답 있음 | 우리에 없음(줄) | 이름만 1위 | 이름만 5안 | 지금 1위 | 지금 5안 | 추천 결합 1위 | 추천 결합 5안 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
{chr(10).join(metric_rows)}
|
||||
|
||||
- 추천 결합 = ⑥ 조항 윗마디 + ⑬ 같은 이름 접힘 고침 + 조항 오타(13-114 → 13-11-4) + ⑪ 같음도 근거 검사(판 지킴).
|
||||
- 우리에 없음 10 줄 중 지금 엉뚱 후보가 보이는 줄 5(고화매트 → 식생매트 · 잔토정리 → 타설후 정리 · 철강재운반 → 덤프 자재 운반 · 철골공 → 철골 공장용접 · 영월 잔토처리 → 터널굴착 1발파당 인력).
|
||||
- 줄 무게(같은 줄 되풀이 셈) — 이름만 1위 느슨 56.1 · 지금 78.1 · 추천 결합 85.6 · 다섯 안 느슨 90.0 · 97.9 · 97.9.
|
||||
- 앞서 잰 「1순위 근거 맞음 123/201」은 근거 꼬리표 기준(정답 대조 아님) — 이번 고유 줄 지금 [근거 맞음] 고름 임도 26/41 · 사방 22/40 과 같은 결.
|
||||
- 자재 · 별도계상 줄(북부청 임도 142 · 사방 56)은 정답 표시 밖 — 상태만: 임도 자재 같음 12 · 후보 50 · 없음 16 / 별도계상 후보 53 · 없음 11.
|
||||
|
||||
## 3. 실패 까닭 갈래 표
|
||||
|
||||
1순위 엄격 실패 줄(고유)마다 주 갈래 하나 + 곁 갈래(겹쳐 셈). 칸 = 주 갈래 고유(줄) · 곁 갈래 수. 이름만 = 330 앞 · 씨앗 끔(실패 {tot_a} · 줄 {tot_al}) / 지금 = 330 뒤(실패 {tot_b} · 줄 {tot_bl}).
|
||||
|
||||
| 갈래 | 이름만 실패 | 지금 실패 | 예 |
|
||||
|---|---|---|---|
|
||||
{chr(10).join(cat_rows)}
|
||||
|
||||
- 이름만 실패의 {round(100 * (CAT['같은 뜻 다른 말'][0][0] + CAT['책 갈래(산림↔건설)'][0][0]) / tot_a)}%({CAT['같은 뜻 다른 말'][0][0] + CAT['책 갈래(산림↔건설)'][0][0]}/{tot_a})가 같은 뜻 다른 말 · 책 갈래 — 330 근거가 이 둘을 대부분 풂(책 갈래 {CAT['책 갈래(산림↔건설)'][0][0]} → {CAT['책 갈래(산림↔건설)'][1][0]} · 같은 뜻 {CAT['같은 뜻 다른 말'][0][0]} → {CAT['같은 뜻 다른 말'][1][0]}).
|
||||
- 지금 책 갈래 11 = 「같음」(이름 같음 · 씨앗) 자동 대상이 엑셀 출처와 다른 책 10(산림 로직 ↔ 엑셀 건설 조항 9 · 반대 1 = 돌망태형 옹벽) + 이름 같음 덤(+1)이 근거 점수를 이김 1(영월 합판거푸집(4회)) — 같음에도 근거 검사를 하면 10 이 풀림(⑪).
|
||||
- 지금 같은 뜻 다른 말 11 = 근거로도 못 품 — 엑셀 출처가 비었거나(사방 철골설치 · 조약돌 9:1) · 단위중량 인용(고임돌 「1-10」) · 옛 판 번호(영월 9-1 · 9-3) · 우리에 그 책 조항 없음(건설 1-2-7 인력운반).
|
||||
|
||||
## 4. 방법별 효과 표
|
||||
|
||||
세 파일 정답 있는 126 줄(고유) · 칸 = 1위 엄격 / 느슨 · 5안 엄격 / 느슨(%). 「이름만에」 = 330 앞 맞춤에 얹음 · 「지금에」 = 330 맞춤에 얹음. 흉내는 스크래치패드에서 함수를 바꿔 끼워 같은 줄을 다시 돌림.
|
||||
|
||||
| 방법 | 1위 | 5안 | 들 일 | 위험 · 비고 |
|
||||
|---|---|---|---|---|
|
||||
{chr(10).join(meth_rows)}
|
||||
| ⑨ 맞춤 기억 재사용(임도 정답 → 사방) | 지금 열쇠 4/48 · 품명\\|단위 15/48(맞음 15) · 정리이름\\|단위 24/48(맞음 21) | 사방 → 임도 4/52 · 18/52(18) · 23/52(22) | 중간 | 정리이름 열쇠는 괄호 낱말을 버려 엉뚱 3(구조물터파기(토사) ↔ (인력100%)) → 품명\\|단위가 알맞음 |
|
||||
| ⑨' 씨앗(사용자 실무 이름 80 = 기억의 한 꼴) | 영월 1위 엄격 44.1 → 67.6 · 느슨 55.9 → 73.5 | 영월 79.4 → 88.2 | — | 임도 · 사방은 씨앗 「면고르기 · 문양거푸집 → 산림」 탓에 엄격 −4.1 · −2.3 |
|
||||
| 단위 별칭(hr=시간 · 공/㎥=공㎥ · 개소=개=본) | 순위 변화 거의 없음 | — | 작음 | 정답 짝 환산 「입력」 20 → 14 줄 |
|
||||
| 장비 줄 길(갈래 기계 · 단위 시간 → 「기계 시간당 기계경비」 + 기계 입력) | 흉내 안 함 · 남은 실패 2 줄(펌프차 · 크레인) 풂 예상 | — | 작음 | 낮음 |
|
||||
| 임베딩(한국어 문장 벡터) 재순위 | 기대 — 같은 뜻 다른 말 일부(터파기/굴착 · 레미콘/레디믹스트) | — | 중간~큼(새 패키지 · 모델 · 미리 셈) | 휠라 · 고임돌 · 야면석 같은 현장말은 모를 가능성 · 산림/건설 같은 이름은 벡터도 같아 책 가름 못 함 → 근거와 함께만 |
|
||||
| 큰 언어 모델 재순위(후보 20 + 엑셀 산출 글 + 우리 호표 · 출처) | 기대 — 남은 실패(복합 · 판 차이 · 같은 뜻)의 대부분 | — | 중간(길 · 열쇠) · 비용 줄당 수 원 | 외부 전송(실무 자료) · 매번 다른 답 · 없는 키 지어냄 → 후보 목록 안에서만 고르게 · 근거 실패 줄(약 15%)에만 · 사람 확정 그대로 |
|
||||
|
||||
## 5. 추천 차례 — 먼저 할 것 셋
|
||||
|
||||
1. **근거를 이름보다 앞세우는 서버 고침 넷**(들 일 작음~중간 · 위험 낮음) — `M02_Hopyo_Basis._clause` 윗마디(엑셀 6-2 → 우리 6-2-2 · 6-2-3) · `M02_Hopyo_Match.candidates` 접힘 열쇠에 로직 키 · 조항 오타(13-114) · 같음일 때도 근거 검사(엑셀 글이 옛 판 「24년 · 임도품셈 · 사방품셈」이면 안 내림 · 근거 후보가 직종 또는 뜻 낱말을 나눌 때만) → 1순위 엄격 {t1(now['전체']).split(' / ')[0]} → {t1(best['전체']).split(' / ')[0]}% · 다섯 안 엄격 {t5(now['전체']).split(' / ')[0]} → {t5(best['전체']).split(' / ')[0]}%. 덤으로 장비 줄 길(기계 갈래 → 기계 시간당 기계경비 + 기계 입력)과 단위 별칭(hr · 공/㎥ · 개소).
|
||||
2. **고르기 창 손질**(중간 · 부록 A) — 후보마다 책 · 조항 · 우리 값 ÷ 엑셀 단가(±5% 표시) · 엑셀 산출 글의 직종 · 품과 우리 호표를 나란히 · 같음 줄에도 근거 후보 · [같음 모두 확정]에서 근거 어긋남 빼기 · [다른 로직 찾기]에 엑셀 조항 미리 넣기. 다섯 안 정답이 94%라 남은 일은 「사람이 빨리 고르기」.
|
||||
3. **기억 넓히기**(중간) — 확정 때 둘째 열쇠(품명|단위)를 「기억 후보」로 · 확정한 엑셀 이름을 층 별칭으로 남겨 이름 후보 · 찾기에 씀(손 사전 대신 사용자 확정이 사전이 됨) → 같은 회사 다음 파일 4/48 → 15/48 · 씨앗 효과(영월 +23.5pt)와 같은 결.
|
||||
|
||||
- 별도로 알아 둘 것 — 다른 외부문서 대부분(STmate 꼴)은 지금 올리기부터 400. 「다른 문서에서도 이름이 다름」을 풀려면 STmate 읽기(일위대가표 · 단가산출근거 · D/L/X 코드로 줄 ↔ 블록 · 노임 · 장비 바로 이음)가 먼저 — 코드가 있어 이름 · 블록 잇기는 북부청 꼴보다 쉬움.
|
||||
- 손 사전 · 금액 순위 점수 · 단위 거름망 · n-그램은 하지 않음(위 표에서 해로움 · 효과 없음).
|
||||
|
||||
## 부록 A. 화면 사용 흐름 — 사람이 고르기 어렵게 만드는 것(추천 2 의 근거)
|
||||
|
||||
- 후보 단추 글 = 「이름 [단위] · 키」 뿐(View.ts:446-452 · targetText :86-91) — 이름 후보에는 책 · 조항이 안 보여 같은 이름 산림 · 건설(유로폼 설치 및 해체 GF000210 · GC000214)을 구별 못 함.
|
||||
- 점수 · 까닭은 마우스 올림(title)에만(View.ts:459) · 초록 「근거 맞음」은 조항 같음 + 단위 + 직종 셋 다일 때만(basisOk :122-126) — 윗마디 조항(6-2)이면 늘 회색.
|
||||
- 「같음」 줄은 서버가 후보를 안 만듦(Match.py:395-403) → 고르기 창에 「가까운 것 없음」만(View.ts:499) · 다른 책으로 바꾸려면 [다른 로직 찾기…]로 이름을 다시 쳐야 함.
|
||||
- [같음 모두 확정](View.ts:276-282)은 근거를 안 봄 — 이번 표본 25 중 5 가 엑셀과 다른 책.
|
||||
- 엑셀 근거는 한 줄 요약(출처 · 직종 · 행 · View.ts:464-487) — 계산식(품 · 수량 · 장비)과 우리 로직 호표를 나란히 못 봄 · [엑셀에서 보기]로 창을 하나 더 엶.
|
||||
- 후보마다 우리 값(엑셀 단위로 환산) ÷ 엑셀 단가가 없음 — ±5% 안이면 95% 맞는 신호인데 안 씀.
|
||||
- [다른 로직 찾기…](View.ts:488-496)는 고르기 창을 닫고 빈 찾기 창을 엶 — 찾기 칸은 원문번호(조항)로도 걸러지지만(ui_template_logic_find.ts:44-53) 엑셀 조항을 미리 넣어 주지 않음 · 안내 없음.
|
||||
- 표 「대상」 칸(View.ts:424)도 키 · 책 없이 이름만 — 같은 이름 둘이면 무엇을 골랐는지 안 보임.
|
||||
- 확정 열쇠 = 품명|규격|단위 정확히 같을 때만(Match.py:68-76) · M01 로직 별칭(208-1)은 로직 1,438 중 0 — 확정한 엑셀 이름이 다음 파일 · 찾기 창에 안 쓰임.
|
||||
|
||||
## 부록 B. 맞춤 흐름(파일:줄)
|
||||
|
||||
- 올리기 — 화면 `M02_MasterTemplete_Hopyo_Side.ts` → `Hopyo_Api.ts:219-231 uploadHopyo` → `M02_Labor_Router.py:206 POST /layers/{{층}}/hopyo` → `_upload :174-184` → `M02_Hopyo_Excel.read_book :211-279`(`_sheets :74-102` 「일위대가 · 일위대가총괄표 · 단가산출」 세 시트만 · 없으면 400 `NO_SHEET :32` · 호표 · 줄 · `kind_of :132-149` 갈래 · 단산N참조 줄에 `Basis.read :84-125` 블록 근거 `{{출처, 직종, 행}}` :257-261) → `Grid.read` → `Store.save :78`(맞춤은 저장 안 함 · 열 때마다 셈).
|
||||
- 호표 열기 — `Hopyo_View.ts drawOne :261-344` → `Api readHopyoOne :180-181` → `Router :243-252 GET …/hopyo/{{key}}/{{no}}` → `Match.view :460-474` → `match_hopyo :440-453` → `match_line :367-414`.
|
||||
- `match_line` 차례 — % · 빼기 · 일위N(호표) → 층 기억 `recall :348-350`(열쇠 `line_key :68-76` = 품명|규격|단위) → 이름 같음 · 씨앗 `_exact :286-297`(`_seeds :266-278` 일위대가조합.json 메모) → 하나면 「같음」 끝(:395-403 · 후보 안 만듦) → 아니면 근거 후보 `Basis.candidates :191-235`(`_clause :166-176` 조항 같음 1.0 · 앞 마디 0.5 · 건설 공통/토목 0.1 · 직종 겹침 0.5 · 단위 0.15 · 직종도 뜻 낱말도 안 겹치면 버림 :213-214) + 이름 후보 `Match.candidates :300-329`(정리 이름 `tidy :79-84` 괄호 글 버림 · 글자 두 개 묶음 dice · 정리 이름 같음 +1 · 단위 0.15 · 산림 0.05 · 자재 규격 0.5 · 뜻 낱말 거름 `near :103-105` · (이름 · 규격 · 단위) 같으면 하나로 접음 :320-325) → `Basis.merge :238-256`(로직 키로 점수 합 · 낱말 안 겹친 근거 후보 뒤로) → `Basis.pick :259-266`(이름 1순위 한 자리) → 후보 5.
|
||||
- 출처 읽기 — `Basis.sources :60-71`(「…품셈」 뒤 20 글자 안 절 번호 · 앞 글에 산림 · 임도 = 산림품셈 · 그 밖 건설품셈 — 부문 · 연도 버림) · 우리 쪽 조항 `M02_Labor_Inputs.sources :68-85` · `_NUMBER :20`.
|
||||
- 고르기 · 확정 — `View choose :435-505` → `guessHopyo`(Api :183-193) → `Router :263-281` → `Match.guess :417-428`(입력값 · 환산) → [확정] `saveHopyoMemory` → `Router :283-304` → `Match.confirm :495-526`(층 기억). 단추 [같음 모두 확정] `View :276-282` · [근거 맞음 모두 확정] `View :284-291`(basisOk :122-126 · basisPick :153-158).
|
||||
|
||||
## 부록 C. 실제 자료
|
||||
|
||||
- `resources/knowledge/original/실무문서/2026_실무일위대가 모음/` — `2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls`(호표 68 · 줄 415 · 로직 줄 고유 52) · `2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls`(호표 27 · 줄 201 · 고유 48) — 시험 `resources/tester/test_m02_hopyo_excel.py:26-30` 이 씀. 같은 폴더 `2016_상반_…사방사업표준품셈(안).xls` · `2026_상반_…수목진료.xls` = 일위대가 시트 없음(400).
|
||||
- `resources/knowledge/original/실무문서/05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx` — STmate 꼴(일위대가표 211행 · 단가산출근거 5,604행) · D 코드 줄 고유 36(줄 91). 같은 꼴 = 간선임도 기번3 · 울진 소광 · 계류보전 영덕 · 봉화 현동 내역 xlsx(시트 이름 확인) · 거창은 또 다른 꼴.
|
||||
- 우리 로직 = `resources/master_data/로직_*.json`(M01 `loaded()` 읽기만) · 씨앗 = `resources/master_data/일위대가조합.json` 메모 80.
|
||||
- tmp 안 실무 엑셀 없음.
|
||||
|
||||
## 부록 D. 정답 표시 근거(고유 줄 136 · 전수)
|
||||
|
||||
칸 「지금」 = 지금 맞춤 상태 · 1순위 키 · 표시(✓ 엄격 맞음 · △ 허용 맞음 · 5안 = 다섯 안에만 · ✗). N = 북부청 임도 · S = 사방 · Y = 영월. 「짐작」 = 블록에 출처가 없거나 우리 로직이 일부만 덮어 사람 판단이 섞임. 영월 「씨앗」 줄은 사용자 실무 이름 → 로직(일위대가조합.json)을 정답으로 봄.
|
||||
|
||||
| id | 엑셀 품명 · 규격 [단위] ×줄 | 정답(엄격) · 허용 | 근거 | 지금 |
|
||||
|---|---|---|---|---|
|
||||
{chr(10).join(app)}
|
||||
|
||||
## 부록 E. 스크립트 · 결과 파일
|
||||
|
||||
폴더 = 스크래치패드 `req225/C1/`. 모두 읽기만(서버 · DB · storage · 저장소 안 씀 · 함수 바꿔 끼움은 같은 프로세스 안).
|
||||
|
||||
- `s01_extract.py` — 두 xls 를 `M02_Hopyo_Excel.read_book` 으로 읽어 줄마다 `match_line`(기억 없음) → `out_lines.json`(616 줄 · 후보 · 점수 · 단산 블록 글) · 우리 로직 표 `out_ours.json`.
|
||||
- `s02_view.py` · `s03_find.py` — 고유 로직 줄 보기(`out_items.json`) · 우리 로직 찾기.
|
||||
- `labels.py` · `labels_y.py` — 정답 표시(부록 C 원천) · `cats.py` — 실패 갈래.
|
||||
- `s04_eval.py` · `s04b_diff.py` — 지표 · ①②④⑥ 흉내 · 줄별 바뀜.
|
||||
- `s05_more.py` · `s05b_diff.py` — 책 맞춤 · ⑤⑦⑧⑪ · 금액 진단(`out_money.json`).
|
||||
- `s06_memory.py` — ⑨ 기억 재사용 · 점수 문턱.
|
||||
- `s09_stmate.py` — 영월 STmate 읽기(흉내) → `out_stmate.json`.
|
||||
- `s10_all.py` · `s10b_diff.py` — 세 파일 지표 · ⑫⑬⑭ · 씨앗 끔 → `out_all.json`.
|
||||
- `s11_buttons.py` — 단추 정밀 · `s12_units.py` — 단위 별칭 · `s13_money_flag.py` — 금액 확인 표시 정밀.
|
||||
- `s14_fails_all.py` · `s15_cats.py` — 실패 목록 · 갈래 표(`out_fails_all.json` · `out_cats.json`).
|
||||
- `s16_guard.py` — 작은 고침 묶음 · ⑪ 판 지킴 → `out_guard.json` · `s18_name_all.py` — 이름 쪽 방법 세 파일 → `out_name_all.json`.
|
||||
- `s17_report.py` — 이 글.
|
||||
- 실행 = 저장소 뿌리 `venv/Scripts/python.exe -B <스크립트>`(스크래치패드 폴더에서 · 각 120 초 안).
|
||||
|
||||
## 부록 F. 정의 · 한계
|
||||
|
||||
- 엄격 = 엑셀 블록 출처 조항 · 직종 · 품과 같은 우리 로직(복합이면 주된 몫 · 여럿이면 어느 하나). 느슨 = 엄격 + 같은 일 다른 책 · 복합의 곁 몫. 「우리에 없음」은 분모에서 빼고 따로 셈.
|
||||
- 짐작 표시 31 줄(출처 없음 · 부분 덮음) — 고임돌 · 조약돌 = 산림 13-2-1 인력 채집 몫 · 소운반 = 산림 10-6 인력운반 · 철골설치 = 철골세우기 장비 손료 등은 사람 판단.
|
||||
- 사전(②)은 임도 파일에서만 뽑아 사방 · 영월을 처음 보는 셈으로 잼 — 같은 파일 점수는 부풀어 있음.
|
||||
- 금액(⑧)은 자동값 + 규격에서 읽은 입력값으로 우리 로직을 서버 엔진 그대로 셈(`M02_Labor_Doc.run_line`) · 엑셀 노임(2026)과 우리 노임 차이 몇 % 섞임.
|
||||
- 영월은 지금 화면이 못 올리는 꼴 — 지표는 「읽기를 붙였다고 가정」 · 노임 코드 → 직종 이름이 덜 이어져 근거 직종이 거의 빔.
|
||||
"""
|
||||
|
||||
OUT.write_text(TEXT, encoding="utf-8")
|
||||
print("쓴 곳", OUT, "글자", len(TEXT))
|
||||
@@ -0,0 +1,25 @@
|
||||
"""이름만 쪽 방법(①②③④ · ⑤ · 책 맞춤)을 세 파일 · 씨앗 끔으로 다시 — out_name_all.json."""
|
||||
import json
|
||||
from s04_eval import SYN_NORTH, grams_123, name_only, patched, tidy_keep, with_syn, hm
|
||||
from s05_more import candidates_book, name_difflib, name_words, unit_filter_line
|
||||
from s10_all import E, no_seeds, run, show, table
|
||||
|
||||
rows = []
|
||||
with no_seeds():
|
||||
rows.append(table("이름만(씨앗 끔)", run(name_only)))
|
||||
with patched(hm, tidy=tidy_keep):
|
||||
rows.append(table("이름만+①다듬기", run(name_only)))
|
||||
with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)):
|
||||
rows.append(table("이름만+②사전(임도에서 뽑음)", run(name_only)))
|
||||
rows.append(table("이름만+③낱말 차례 무시", run(name_words)))
|
||||
with patched(hm, _grams=grams_123):
|
||||
rows.append(table("이름만+④1·2·3그램", run(name_only)))
|
||||
rows.append(table("이름만+④difflib", run(name_difflib)))
|
||||
rows.append(table("지금+⑤단위 거름망", run(unit_filter_line)))
|
||||
with patched(hm, candidates=candidates_book):
|
||||
rows.append(table("지금+책 맞춤 덤", run()))
|
||||
with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)):
|
||||
rows.append(table("지금+②사전", run()))
|
||||
for r in rows:
|
||||
show(r)
|
||||
(E.HERE / "out_name_all.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8")
|
||||
@@ -0,0 +1,256 @@
|
||||
"""req226 C4 — 로직 지문 공용: 경로 · 저장소 모듈 · 식 풀기 · 장비 갈래 · 조건 낱말 · 닮음 셈.
|
||||
저장소 안 고침(읽기만 · 같은 프로세스 안에서만 씀)."""
|
||||
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225" / "C1"
|
||||
C3 = HERE.parent.parent / "req225" / "C3"
|
||||
for p in (str(ROOT), str(C1), str(C3)):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
# ── 장비 갈래(엑셀 글 · 우리 EQ 상세구분 공통) ─────────────────────────────
|
||||
MACH = [
|
||||
("굴착기", ("굴착기", "굴삭기", "백호", "b/h", "backhoe", "back-hoe", "포크레인")),
|
||||
("덤프트럭", ("덤프",)),
|
||||
("크레인", ("크레인",)),
|
||||
("브레이커", ("브레이커", "브레이카")),
|
||||
("콘크리트펌프", ("펌프차", "펌프카", "콘크리트펌프", "콘크리트 펌프")),
|
||||
("롤러", ("롤러", "로울러")),
|
||||
("불도저", ("불도저", "불도우저")),
|
||||
("집게", ("집게",)),
|
||||
("살수차", ("살수", "물탱크")),
|
||||
("모터그레이더", ("그레이더",)),
|
||||
("래머", ("래머", "램머", "래 머")),
|
||||
("콤팩터", ("콤팩터", "컴팩터")),
|
||||
("진동기", ("진동기",)),
|
||||
("발전기", ("발전기",)),
|
||||
("공기압축기", ("공기압축기", "컴프레셔", "콤프레샤")),
|
||||
("드릴", ("드릴", "천공기")),
|
||||
("커터", ("커터",)),
|
||||
("화물차", ("화물자동차", "화물차", "카고")),
|
||||
("로더", ("로더",)),
|
||||
("지게차", ("지게차",)),
|
||||
("체인톱", ("체인톱",)),
|
||||
("트랙터", ("트랙터",)),
|
||||
]
|
||||
|
||||
|
||||
def mtypes(text) -> set:
|
||||
t = unicodedata.normalize("NFKC", str(text or "")).lower()
|
||||
return {k for k, ws in MACH if any(w in t for w in ws)}
|
||||
|
||||
|
||||
_SPECNUM = re.compile(r"(\d+(?:\.\d+)?)\s*(㎥|m3|톤|ton|t\b|m\b|㎾|kw|ℓ|mm|hp)", re.I)
|
||||
|
||||
|
||||
def specnums(text) -> set:
|
||||
t = unicodedata.normalize("NFKC", str(text or ""))
|
||||
return {float(a) for a, _ in _SPECNUM.findall(t)}
|
||||
|
||||
|
||||
# ── 조건 낱말(축) ──────────────────────────────────────────────────────
|
||||
SOIL = ("보통토사", "경질토사", "혼합토사", "자갈섞인토사", "사질토", "점질토", "토사")
|
||||
ROCK = ("리핑암", "발파암", "풍화암", "연암", "보통암", "경암", "극경암")
|
||||
AXES = {
|
||||
"토질": SOIL + ROCK,
|
||||
"메찰": ("메쌓기", "찰쌓기", "메붙임", "찰붙임", "메붙이기", "찰붙이기"),
|
||||
"형": ("원형", "사각", "타원형"),
|
||||
"근": ("무근", "철근"),
|
||||
"난이": ("제물치장", "매우복잡", "복잡", "보통", "간단"),
|
||||
"규모": ("소량", "소형", "소규모", "중규모", "대규모", "대형"),
|
||||
"방법": ("인력운반", "인력", "기계", "장비", "펌프"),
|
||||
}
|
||||
CLASH = ("토질", "메찰", "형") # 이름에 박힌 값이 엑셀과 어긋나면 깎는 축(방법 · 난이는 안 깎음 — C3 #8)
|
||||
|
||||
|
||||
def soil_family(w):
|
||||
return "암" if w in ROCK or w == "암" else "토사" if w in SOIL else None
|
||||
|
||||
|
||||
def conds(text) -> dict:
|
||||
"""{축: {낱말}} + 횟수 · 개월 · Type."""
|
||||
t = re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or "")))
|
||||
out = {}
|
||||
for ax, ws in AXES.items():
|
||||
hit = set()
|
||||
rest = t
|
||||
for w in sorted(ws, key=len, reverse=True):
|
||||
if w in rest:
|
||||
hit.add(w)
|
||||
rest = rest.replace(w, " ")
|
||||
if ax == "토질" and re.search(r"(\(암\)|암\)|[^가-힣]암$|^암$|암절취|\b암\b)", t):
|
||||
hit.add("암")
|
||||
if hit:
|
||||
out[ax] = hit
|
||||
n = re.findall(r"(\d)회", t)
|
||||
if n:
|
||||
out["횟수"] = {f"{x}회" for x in n}
|
||||
m = re.findall(r"(\d+)개월", t)
|
||||
if m:
|
||||
out["개월"] = {f"{x}개월" for x in m}
|
||||
ty = re.findall(r"type[-\s]*(ⅰ|ⅱ|ii|i)\b", t.lower())
|
||||
if ty:
|
||||
out["type"] = {"Ⅱ" if x in ("ⅱ", "ii") else "Ⅰ" for x in ty}
|
||||
return out
|
||||
|
||||
|
||||
def cond_score(excel: dict, name_c: dict, opt_c: dict) -> tuple:
|
||||
"""(맞음 비율, 어긋남) — 엑셀 조건 낱말이 우리 이름 · 고르기 글에 있나 · 이름 축 어긋남."""
|
||||
words = [(ax, w) for ax, ws in excel.items() for w in ws]
|
||||
if not words:
|
||||
return 0.0, 0
|
||||
hit = 0
|
||||
for ax, w in words:
|
||||
pool = name_c.get(ax, set()) | opt_c.get(ax, set())
|
||||
if w in pool or (ax == "토질" and soil_family(w) in {soil_family(x) for x in pool}):
|
||||
hit += 1
|
||||
clash = 0
|
||||
for ax in CLASH:
|
||||
if ax in excel and ax in name_c:
|
||||
if ax == "토질":
|
||||
ef = {soil_family(w) for w in excel[ax]}
|
||||
of = {soil_family(w) for w in name_c[ax]}
|
||||
if ef and of and not (ef & of):
|
||||
clash = 1
|
||||
elif not (excel[ax] & name_c[ax]):
|
||||
clash = 1
|
||||
return hit / len(words), clash
|
||||
|
||||
|
||||
# ── 식 풀기 — 「단가 × 식 = 금액」 → 품(단가로 나눈 몫 · 단가는 버림) ──────────────────
|
||||
_UNITFRAC = re.compile(
|
||||
r"(인|개|본|공|EA|ea|hr|ton|Ton|TON|kg|㎥|㎡|m3|m2|m|M|조|회)\s*/\s*(EA|ea|㎥|㎡|m3|m2|m|M|ton|Ton|TON|t|hr|일|개|본|조|공㎥|㎥)"
|
||||
)
|
||||
_UNITTAIL = re.compile(r"(?<=[\d.)])\s*(hr|HR|EA|ea|kg|km|ton|Ton|TON|sec|m3|m2|mm|cm|M|m|t)(?![A-Za-z])")
|
||||
_NOTE = re.compile(r"\(([^()]*[가-힣][^()]*)\)")
|
||||
_LEAD = re.compile(r"^\s*(\d+(?:\.\d+)?)\s*([*/])\s*(.+)$")
|
||||
_SAFE = re.compile(r"^[\d.+\-*/() ]+$")
|
||||
|
||||
|
||||
def _norm(s: str) -> str:
|
||||
s = unicodedata.normalize("NFKC", str(s))
|
||||
s = s.replace("×", "*").replace("✕", "*").replace("÷", "/").replace("x", "*").replace("X", "*")
|
||||
s = re.sub(r"(?<=\d),(?=\d{3})", "", s)
|
||||
return _UNITFRAC.sub("", s)
|
||||
|
||||
|
||||
def _split(rest: str, first_op: str):
|
||||
"""맨 바깥 * / 로 쪼갬 → [(연산, 조각)]."""
|
||||
out, depth, cur, op = [], 0, "", first_op
|
||||
for ch in rest:
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0 and ch in "*/":
|
||||
out.append((op, cur))
|
||||
op, cur = ch, ""
|
||||
continue
|
||||
cur += ch
|
||||
out.append((op, cur))
|
||||
return out
|
||||
|
||||
|
||||
def _factor(text: str):
|
||||
"""조각 → (값, 율 표시, 메모) · 변수(P · Q …)면 None."""
|
||||
notes = _NOTE.findall(text)
|
||||
t = _NOTE.sub("", text)
|
||||
pct = "%" in t
|
||||
if re.search(r"\d\s*회", t): # 「× 2회」 — 횟수 곱(율로 가름)
|
||||
notes.append("회")
|
||||
t = re.sub(r"[가-힣㎥㎡㎝㎜℃˚°'\"]", "", t).replace("%", "")
|
||||
t = _UNITTAIL.sub("", t).strip()
|
||||
if not t or re.search(r"[A-Za-z]", t) or not _SAFE.match(t):
|
||||
return None
|
||||
try:
|
||||
v = eval(t, {"__builtins__": {}}, {}) # noqa: S307 — 숫자 · 연산 글만(_SAFE)
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
v = float(v) / (100 if pct else 1)
|
||||
return v, pct or bool(notes), " ".join(notes)
|
||||
|
||||
|
||||
def _rates_any(parts):
|
||||
"""모든 조각의 율(메모 · %) — 변수 든 조각(P · Q)도 풀 수 있는 조각만."""
|
||||
out = []
|
||||
for seg in parts:
|
||||
m = _LEAD.match(seg)
|
||||
if not m:
|
||||
continue
|
||||
for op, piece in _split(m.group(3), m.group(2)):
|
||||
f = _factor(piece)
|
||||
if f and f[1] and f[0] and all(abs(f[0] - v) > 1e-9 for v, _ in out):
|
||||
out.append((f[0] if op == "*" else 1 / f[0], f[2] or "%"))
|
||||
return out
|
||||
|
||||
|
||||
def parse_rhs(rhs: str):
|
||||
"""「단가 × 식 [= 바꿔 쓴 식] = 금액」 → {단가, 품, 밑품, 율[]} · 못 읽으면 None."""
|
||||
parts = _norm(rhs).split("=")
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
every = _rates_any(parts[:-1])
|
||||
for seg in parts[:-1]:
|
||||
m = _LEAD.match(seg)
|
||||
if not m:
|
||||
continue
|
||||
price = float(m.group(1))
|
||||
facs = []
|
||||
ok = True
|
||||
for op, piece in _split(m.group(3), m.group(2)):
|
||||
f = _factor(piece)
|
||||
if f is None:
|
||||
ok = False
|
||||
break
|
||||
facs.append((op, *f))
|
||||
if not ok or not facs:
|
||||
continue
|
||||
full = base = 1.0
|
||||
rates = []
|
||||
for op, v, rate, note in facs:
|
||||
if v == 0 and op == "/":
|
||||
ok = False
|
||||
break
|
||||
full = full * v if op == "*" else full / v
|
||||
if rate:
|
||||
rates.append([round(v if op == "*" else 1 / v, 6), note or "%"])
|
||||
else:
|
||||
base = base * v if op == "*" else base / v
|
||||
if ok:
|
||||
if not rates and every: # 바꿔 쓴 식엔 메모가 빠짐 — 앞 조각의 율로 밑품
|
||||
for v, note in every:
|
||||
if v:
|
||||
base /= v
|
||||
rates = [[round(v, 6), note] for v, note in every]
|
||||
plain = [v if op == "*" else 1 / v for op, v, rate, _ in facs if not rate and v]
|
||||
return {"단가": price, "품": full, "밑품": base, "율": rates, "인자": plain}
|
||||
return None
|
||||
|
||||
|
||||
# ── 닮음 ───────────────────────────────────────────────────────────────
|
||||
def close(a, b, tol=0.15) -> float:
|
||||
"""두 양수 가까움 0~1 — 비가 tol 안이면 1 쪽 · ln 거리로 떨어짐."""
|
||||
if not a or not b or a <= 0 or b <= 0:
|
||||
return 0.0
|
||||
d = abs(math.log(a / b))
|
||||
return 1.0 if d <= 0.03 else max(0.0, 1 - (d - 0.03) / (tol * 4))
|
||||
|
||||
|
||||
def cosine(a: dict, b: dict) -> float:
|
||||
keys = set(a) | set(b)
|
||||
num = sum(a.get(k, 0) * b.get(k, 0) for k in keys)
|
||||
da = math.sqrt(sum(v * v for v in a.values()))
|
||||
db = math.sqrt(sum(v * v for v in b.values()))
|
||||
return num / (da * db) if da and db else 0.0
|
||||
|
||||
|
||||
def jacc(a, b) -> float:
|
||||
a, b = set(a), set(b)
|
||||
return len(a & b) / len(a | b) if a | b else 0.0
|
||||
@@ -0,0 +1,10 @@
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
files, whole = m01.loaded()
|
||||
for code in sys.argv[1:]:
|
||||
row = whole.logic(code)
|
||||
print("=====", code)
|
||||
print(json.dumps(row, ensure_ascii=False, indent=1, default=str)[:6000])
|
||||
@@ -0,0 +1,17 @@
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
files, whole = m01.loaded()
|
||||
mf = m01.cm.mf
|
||||
code = sys.argv[1]
|
||||
given = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {}
|
||||
logic = whole.logic(code)
|
||||
auto = {k: v for k, v in m01.mau.values(whole, logic).items() if v is not None}
|
||||
print("auto", auto)
|
||||
got = mf.run(whole, code, {**auto, **m01._dec(given)})
|
||||
print("keys", list(got.keys()))
|
||||
for ln in got.get("줄") or []:
|
||||
print(json.dumps(ln, ensure_ascii=False, default=str)[:400])
|
||||
print("중간", json.dumps(got.get("중간"), ensure_ascii=False, default=str)[:1500])
|
||||
@@ -0,0 +1,33 @@
|
||||
import json, sys, time, math
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
files, whole = m01.loaded()
|
||||
mf = m01.cm.mf
|
||||
got = hm.targets()
|
||||
kinds = Counter(); ok = 0; bad = Counter(); combos = []
|
||||
t0 = time.time()
|
||||
eq = whole.numbers.get("EQ", {})
|
||||
print("EQ sample", list(eq.items())[:2])
|
||||
for one in got["로직"]:
|
||||
code = one["로직"]
|
||||
row = whole.logic(code)
|
||||
for h in row.get("호표") or []:
|
||||
kinds[h.get("종류")] += 1
|
||||
picks = [len(s.get("고르기") or []) for s in row.get("입력") or [] if s.get("고르기")]
|
||||
combos.append(math.prod(picks) if picks else 1)
|
||||
try:
|
||||
auto = {k: v for k, v in m01.mau.values(whole, row).items() if v is not None}
|
||||
r = mf.run(whole, code, auto)
|
||||
ok += 1
|
||||
except Exception as e:
|
||||
bad[type(e).__name__] += 1
|
||||
print("logics", len(got["로직"]), "kinds", kinds)
|
||||
print("default ok", ok, "bad", bad, "sec", round(time.time()-t0, 1))
|
||||
c = Counter()
|
||||
for n in combos:
|
||||
c["1" if n == 1 else "<=10" if n <= 10 else "<=100" if n <= 100 else "<=1000" if n <= 1000 else ">1000"] += 1
|
||||
print("combo sizes", c)
|
||||
@@ -0,0 +1,17 @@
|
||||
import sys, warnings, re
|
||||
from pathlib import Path
|
||||
import openpyxl
|
||||
warnings.filterwarnings("ignore")
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
F = ROOT / "resources/knowledge/original/실무문서/05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx"
|
||||
book = openpyxl.load_workbook(F, read_only=True, data_only=True)
|
||||
print(book.sheetnames)
|
||||
want = sys.argv[1] if len(sys.argv) > 1 else "단가산출근거"
|
||||
lo, hi = (int(sys.argv[2]), int(sys.argv[3])) if len(sys.argv) > 3 else (1, 60)
|
||||
for at, r in enumerate(book[want].iter_rows(values_only=True), 1):
|
||||
if at < lo: continue
|
||||
if at > hi: break
|
||||
cells = [str(c) for c in r[:14]]
|
||||
while cells and cells[-1] in ("None", ""): cells.pop()
|
||||
print(at, " | ".join(cells))
|
||||
book.close()
|
||||
@@ -0,0 +1,16 @@
|
||||
"""지문 실패 줄 뜯어 보기 — 엑셀 지문 · 1~3 위 채널 · 정답 로직의 차례 · 채널."""
|
||||
import json, sys
|
||||
import s3_match as S
|
||||
ids = sys.argv[1:]
|
||||
for i in ids:
|
||||
e = S.EXP[i]
|
||||
lab = S.A.LAB[i]
|
||||
print(f"## {i} {e['이름']} | {e['규격']} | {e['단위']} 출처{e['출처']} 옛판{e['옛판']} 조건{ {k: sorted(v) for k, v in e['조건'].items()} }")
|
||||
print(" 인력", {t: (round(o['품'], 4), round(o['밑품'], 4)) for t, o in e['인력'].items()}, "장비", [(m['갈래'], m['규격'], m['시간'] and round(m['시간'], 4)) for m in e['장비']], "자재", [m['이름'][:14] for m in e['자재']])
|
||||
allr = S.rank(e, None, 1438)
|
||||
pos = {k: n for n, (_, k, _, _) in enumerate(allr)}
|
||||
for s, k, ch, v in allr[:3]:
|
||||
print(f" {k} {S.TG[k]['이름'][:22]} [{S.TG[k]['단위']}] {s:.2f}", {a: (round(b, 2) if isinstance(b, float) else b) for a, b in ch.items()}, (v or {}).get('in'))
|
||||
for k in lab['정답'][:2]:
|
||||
s, ch, v = S.score_pair(e, next(o for o in S.P if o['로직'] == k))
|
||||
print(f" 정답 {k} {S.TG[k]['이름'][:22]} [{S.TG[k]['단위']}] 차례 {pos.get(k)} {s:.2f}", {a: (round(b, 2) if isinstance(b, float) else b) for a, b in ch.items()}, (v or {}).get('in'), {t: round(q, 4) for t, q in ((v or {}).get('L') or {}).items() if q})
|
||||
@@ -0,0 +1,339 @@
|
||||
"""req226 C4 ① 바깥 단가산출 블록 → 로직 지문(직종 품 · 장비 · 자재 · 할증 · 계수 · 조건 · 조항).
|
||||
|
||||
북부청 = req225 C1 out_items.json 의 단산 글(산출근거 칸)을 줄마다 풂.
|
||||
영월 = STmate 「단가산출근거」 코드(L · X · M · S) + QTY 칸 + 노무비 · 중기 · 재료비 · 경비 목록표로 이름 이음.
|
||||
출력 = excel_fp.json (고유 줄 136 · id 는 앞 조사와 같음)
|
||||
python s1_excel_fp.py [id …] → 지문 찍어 보기
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import fp_common as F
|
||||
from fp_common import HERE, conds, mtypes, parse_rhs, specnums
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
NORTH = json.loads((F.C1 / "out_items.json").read_text(encoding="utf-8"))
|
||||
YW_ITEMS = json.loads((F.C1 / "out_stmate.json").read_text(encoding="utf-8"))
|
||||
YW_FILE = F.ROOT / "resources/knowledge/original/실무문서/05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx"
|
||||
TRADES = hb.trade_names()
|
||||
|
||||
COST = {"노무비", "재료비", "경비"}
|
||||
SKIPW = {
|
||||
"소계", "계", "합계", "인부소계", "자재소계", "주자재소계", "운반", "상차", "기계", "다짐", "적사", "살수", "집토",
|
||||
"암절취", "채집", "인력", "장비", "작업조건", "1일시공량", "작업면적", "1시간작업량", "기계채집", "구조물터파기",
|
||||
}
|
||||
_COLON = re.compile(r"^\s*(?:\d+\)|[0-9가-힣]\.)?\s*(?P<name>[^::=]{1,40}?)\s*[::]\s*(?P<rhs>.*)$")
|
||||
_MAT = re.compile(r"^\s*(?P<name>[^\d\s::=][^::=]*?[A-Za-z가-힣)\]])\s+(?P<rhs>[\d,]+(?:\.\d+)?\s*[×xX*÷/].*=.*)$")
|
||||
_PCT = re.compile(r"([가-힣]+)의\s*([\d.]+)\s*%")
|
||||
_COEF = re.compile(r"(?<![A-Za-z가-힣])(q|qs|q1|K|k|f|f1|f2|E|Es|Cm|L|V1|V2|T|rt|n|t1|t2|t3|t4)\s*=\s*(\d+(?:\.\d+)?)(?![\d.]*\s*[×x*÷/])")
|
||||
_HEAD = re.compile(r"^\s*(\d+)\.\s*(.*)$")
|
||||
_SUB = re.compile(r"^\s*([가-하])\.\s*(.*)$")
|
||||
_OLD = re.compile(r"((?<![\d,.])20(0\d|1\d|20)(?![\d,.])|(?<!\d)(1\d|20)년|임도품셈|임도표준품셈|사방품셈|사방사업표준품셈)")
|
||||
_LASTNUM = re.compile(r"=\s*([\d,]+(?:\.\d+)?)\s*$")
|
||||
|
||||
|
||||
def _flat(s):
|
||||
return re.sub(r"\s+", "", str(s or ""))
|
||||
|
||||
|
||||
def _ratio(rhs):
|
||||
"""식을 못 풀 때만 — 금액 ÷ 단가(품 되찾기용 · 순위엔 안 씀)."""
|
||||
m = re.match(r"^\s*([\d,]+(?:\.\d+)?)", rhs)
|
||||
n = _LASTNUM.search(rhs)
|
||||
if not m or not n:
|
||||
return None
|
||||
p, a = float(m.group(1).replace(",", "")), float(n.group(1).replace(",", ""))
|
||||
return a / p if p else None
|
||||
|
||||
|
||||
_HEAD_Y = re.compile(r"^\s*(\d+)[.)]\s*(.*)$")
|
||||
_SUB_Y = re.compile(r"^\s*[①-⑩]")
|
||||
|
||||
|
||||
def rates_in(text):
|
||||
"""글 속 섞음 비율 후보 — (N%) · 8:2 · 9:1."""
|
||||
t = str(text or "")
|
||||
out = {round(float(n) / 100, 4) for n in re.findall(r"(\d+(?:\.\d+)?)\s*%", t) if 0 < float(n) < 100}
|
||||
for a, b in re.findall(r"(\d+)\s*:\s*(\d+)", t):
|
||||
a, b = float(a), float(b)
|
||||
if a + b and a and b and a + b <= 20:
|
||||
out |= {round(a / (a + b), 4), round(b / (a + b), 4)}
|
||||
return out
|
||||
|
||||
|
||||
def rate_factor(text, cands):
|
||||
"""식 글에서 섞음 비율로 보이는 곱 조각 하나(없으면 None)."""
|
||||
if not cands:
|
||||
return None
|
||||
nums = re.findall(r"[*×]\s*(\d*\.\d+|\d+)(?![\d,])", str(text or ""))
|
||||
for n in nums:
|
||||
v = round(float(n), 4)
|
||||
if v in cands:
|
||||
return v
|
||||
return None
|
||||
|
||||
|
||||
def empty(item):
|
||||
return {
|
||||
"id": item["id"],
|
||||
"파일": item["파일"],
|
||||
"이름": item["이름"],
|
||||
"규격": item.get("규격") or "",
|
||||
"단위": item.get("단위") or "",
|
||||
"갈래": item.get("갈래"),
|
||||
"출처": ((item.get("근거") or {}).get("출처")) or [],
|
||||
"직종근거": ((item.get("근거") or {}).get("직종")) or [],
|
||||
"인력": {}, # 직종: {품, 밑품, 율, 어떻게}
|
||||
"인력절": [], # [절, 직종, 품, 밑품]
|
||||
"장비": [], # {갈래, 규격, 시간, 밑시간, 절, 글}
|
||||
"자재": [], # {이름, 품, 밑품}
|
||||
"율": [], # [바탕, %]
|
||||
"계수": {},
|
||||
"조건": {},
|
||||
"옛판": False,
|
||||
"글": [],
|
||||
"절": {}, # 절 번호: {이름, 출처}
|
||||
}
|
||||
|
||||
|
||||
def add_labor(fp, trade, q, base, rates, how, sec):
|
||||
one = fp["인력"].setdefault(trade, {"품": 0.0, "밑품": 0.0, "율": [], "어떻게": []})
|
||||
one["품"] += q
|
||||
one["밑품"] += base
|
||||
one["율"] += rates
|
||||
one["어떻게"].append(how)
|
||||
fp["인력절"].append([sec, trade, round(q, 6), round(base, 6)])
|
||||
|
||||
|
||||
def north(item):
|
||||
fp = empty(item)
|
||||
text = item.get("단산글") or []
|
||||
fp["글"] = text[:60]
|
||||
head_text = " ".join([item["이름"], fp["규격"]] + [t for t in text[:5]] + [t for t in text if "※" in t or "작업조건" in t])
|
||||
fp["조건"] = {k: sorted(v) for k, v in conds(head_text).items()}
|
||||
fp["이름조건"] = {k: sorted(v) for k, v in conds(f"{item['이름']} {fp['규격']}").items()}
|
||||
fp["옛판"] = bool(_OLD.search(" ".join(text[:6])))
|
||||
if not text: # 기계 줄 · 블록 없음
|
||||
fp["장비"] = [
|
||||
{"갈래": sorted(mtypes(f"{item['이름']} {fp['규격']}")), "규격": sorted(specnums(f"{item['이름']} {fp['규격']}")),
|
||||
"시간": 1.0 if F.re.search(r"(hr|시간)", fp["단위"], F.re.I) else None, "밑시간": None, "절": 0, "글": item["이름"]}
|
||||
]
|
||||
fp["장비"] = [m for m in fp["장비"] if m["갈래"]]
|
||||
return fp
|
||||
sec, sub, mach, mach_label = 0, "", None, ""
|
||||
got_cost = {}
|
||||
item_rates = rates_in(f"{item['이름']} {fp['규격']}")
|
||||
sec_rates = set()
|
||||
|
||||
def unmix(got):
|
||||
"""율 메모가 없는데 맨 인자가 섞음 비율 후보와 같으면 율로 가름."""
|
||||
if got and not got["율"]:
|
||||
for v in got.get("인자") or []:
|
||||
if round(v, 4) in item_rates | sec_rates and 0 < v < 1:
|
||||
return {**got, "밑품": got["밑품"] / v, "율": [[round(v, 4), "섞음"]]}
|
||||
return got
|
||||
for raw in text[1:]:
|
||||
line = raw.rstrip()
|
||||
h = _HEAD.match(line)
|
||||
s = _SUB.match(line)
|
||||
if h or s:
|
||||
if h:
|
||||
sec, sub = int(h.group(1)), ""
|
||||
fp["절"][str(sec)] = {"이름": h.group(2).strip(), "출처": hb.sources(h.group(2))}
|
||||
sec_rates = rates_in(h.group(2))
|
||||
else:
|
||||
sub = s.group(1)
|
||||
sec_rates = sec_rates | rates_in(s.group(2))
|
||||
top = fp["절"].get(str(sec)) or {"이름": "", "출처": []}
|
||||
fp["절"][f"{sec}.{sub}"] = {"이름": f"{top['이름']} · {s.group(2).strip()}", "출처": hb.sources(s.group(2)) or top["출처"]}
|
||||
title = (h or s).group(2)
|
||||
ts = mtypes(title)
|
||||
mach = (ts, specnums(title)) if ts else None
|
||||
mach_label = title.strip() if ts else ""
|
||||
# 머리 줄 자체가 「기 계 : 굴착기 …」 꼴이면 아래 비용 줄이 그 장비
|
||||
for k, v in _PCT.findall(line):
|
||||
fp["율"].append([k, float(v)])
|
||||
cm = _COLON.match(line)
|
||||
name = _flat(cm.group("name")) if cm else ""
|
||||
name = re.sub(r"^[0-9가-힣]\.", "", name)
|
||||
if cm and name in TRADES:
|
||||
rhs = cm.group("rhs")
|
||||
got = unmix(parse_rhs(rhs))
|
||||
key = f"{sec}.{sub}" if sub else str(sec)
|
||||
if got:
|
||||
add_labor(fp, TRADES[name], got["품"], got["밑품"], got["율"], "식", key)
|
||||
else:
|
||||
r = _ratio(rhs)
|
||||
if r:
|
||||
add_labor(fp, TRADES[name], r, r, [], "역산", key)
|
||||
continue
|
||||
if cm and name in COST:
|
||||
got = unmix(parse_rhs(cm.group("rhs")))
|
||||
if got and mach:
|
||||
key = (sec, sub, mach_label)
|
||||
prev = got_cost.get(key)
|
||||
prio = 0 if (name == "노무비" and got["단가"] > 0) else 1 if name == "경비" else 2
|
||||
if prev is None or prio < prev[0]:
|
||||
got_cost[key] = (prio, mach, got)
|
||||
continue
|
||||
if cm and name not in SKIPW and re.search(r"(별도|별산)", cm.group("rhs")):
|
||||
fp["자재"].append({"이름": cm.group("name").strip(), "품": None, "밑품": None})
|
||||
continue
|
||||
mm_ = _MAT.match(line)
|
||||
if mm_ and not cm:
|
||||
got = parse_rhs(mm_.group("rhs"))
|
||||
if got:
|
||||
fp["자재"].append({"이름": mm_.group("name").strip(), "품": got["품"], "밑품": got["밑품"]})
|
||||
continue
|
||||
if not (h or s):
|
||||
ts = mtypes(line)
|
||||
if ts and "%" not in line and not cm:
|
||||
mach, mach_label = (ts, specnums(line)), line.strip()
|
||||
for k, v in _COEF.findall(line):
|
||||
fp["계수"].setdefault(k, float(v))
|
||||
for (sec_, sub_, label), (_, (ts, sp), got) in got_cost.items():
|
||||
fp["장비"].append(
|
||||
{"갈래": sorted(ts), "규격": sorted(sp), "시간": got["품"], "밑시간": got["밑품"], "율": got["율"], "절": f"{sec_}.{sub_}" if sub_ else str(sec_), "글": label}
|
||||
)
|
||||
return fp
|
||||
|
||||
|
||||
# ── 영월(STmate) ───────────────────────────────────────────────────────
|
||||
def yw_tables():
|
||||
import openpyxl
|
||||
|
||||
book = openpyxl.load_workbook(YW_FILE, read_only=True, data_only=True)
|
||||
|
||||
def table(name, cols):
|
||||
out = {}
|
||||
for r in book[name].iter_rows(values_only=True):
|
||||
code = str(r[0] or "").strip()
|
||||
if re.match(r"^[A-Z]\d{5}$", code):
|
||||
out[code] = [str(r[c] or "").strip() for c in cols]
|
||||
return out
|
||||
|
||||
labor = table("노무비목록표", (1,))
|
||||
mach = table("중기목록표", (1, 2))
|
||||
mats = table("재료비목록표", (1, 2, 3))
|
||||
cost = table("경비목록표", (1, 2))
|
||||
calc = [list(r) for r in book["단가산출근거"].iter_rows(values_only=True)]
|
||||
book.close()
|
||||
blocks, cur = {}, None
|
||||
for r in calc:
|
||||
first = str(r[0] or "").strip()
|
||||
second = str(r[1] or "").strip() if len(r) > 1 else ""
|
||||
if re.match(r"^D\d{5}$", second) and len(r) > 2 and isinstance(r[2], (int, float)):
|
||||
cur = blocks.setdefault(second, [])
|
||||
continue
|
||||
if cur is not None:
|
||||
qty = r[7] if len(r) > 7 and isinstance(r[7], (int, float)) else None
|
||||
cur.append((first, str(r[1] or ""), qty))
|
||||
return labor, mach, mats, cost, blocks
|
||||
|
||||
|
||||
def yeongwol(item, T):
|
||||
labor, mach, mats, cost, blocks = T
|
||||
fp = empty({**item, "파일": "동부청영월"})
|
||||
rows = blocks.get(item.get("코드")) or []
|
||||
text = [t for _, t, _ in rows if t.strip()]
|
||||
fp["글"] = [t.strip() for t in text[:60]]
|
||||
head_text = " ".join([item["이름"], fp["규격"]] + text[:4])
|
||||
fp["조건"] = {k: sorted(v) for k, v in conds(head_text).items()}
|
||||
fp["이름조건"] = {k: sorted(v) for k, v in conds(f"{item['이름']} {fp['규격']}").items()}
|
||||
fp["옛판"] = bool(_OLD.search(" ".join(text[:4])))
|
||||
if not rows:
|
||||
ts = mtypes(f"{item['이름']} {fp['규격']}")
|
||||
if ts:
|
||||
fp["장비"] = [{"갈래": sorted(ts), "규격": sorted(specnums(f"{item['이름']} {fp['규격']}")), "시간": 1.0, "밑시간": None, "절": 0, "글": item["이름"]}]
|
||||
return fp
|
||||
sec, sub, seen_x = 0, "", {}
|
||||
item_rates = rates_in(f"{item['이름']} {fp['규격']}")
|
||||
sec_rates = set()
|
||||
for code, t, qty in rows:
|
||||
h = _HEAD_Y.match(t)
|
||||
if h:
|
||||
sec, sub = int(h.group(1)), ""
|
||||
sec_rates = rates_in(h.group(2))
|
||||
fp["절"][str(sec)] = {"이름": h.group(2).strip(), "출처": hb.sources(h.group(2))}
|
||||
elif _SUB_Y.match(t):
|
||||
sub = str("①②③④⑤⑥⑦⑧⑨⑩".index(t.strip()[0]) + 1)
|
||||
top = fp["절"].get(str(sec)) or {"이름": "", "출처": []}
|
||||
fp["절"][f"{sec}.{sub}"] = {"이름": f"{top['이름']} · {t.strip()[1:]}", "출처": hb.sources(t) or top["출처"]}
|
||||
elif _SUB_Y.match(t):
|
||||
sec_rates = sec_rates | rates_in(t)
|
||||
mix = rate_factor(t, item_rates | sec_rates) if code else None
|
||||
for k, v in _PCT.findall(t):
|
||||
fp["율"].append([k, float(v)])
|
||||
if not code:
|
||||
if not h:
|
||||
for k, v in _COEF.findall(t):
|
||||
fp["계수"].setdefault(k, float(v))
|
||||
continue
|
||||
base = code[:6]
|
||||
if code.startswith("L") and qty is not None:
|
||||
nm = TRADES.get(_flat((labor.get(base) or [""])[0]))
|
||||
if nm:
|
||||
b = qty / mix if mix else qty
|
||||
rates = [[mix, "섞음"]] if mix else []
|
||||
add_labor(fp, nm, float(qty), float(b), rates, "QTY", f"{sec}.{sub}" if sub else str(sec))
|
||||
elif code.startswith("X") and qty is not None:
|
||||
nm, sp = mach.get(base) or ["", ""]
|
||||
key = f"{sec}.{sub}" if sub else str(sec)
|
||||
prev = seen_x.get((key, base))
|
||||
prio = {"L": 0, "S": 1, "M": 2}.get(code[-1], 3)
|
||||
if prev is None or prio < prev[0]:
|
||||
seen_x[(key, base)] = (prio, nm, sp, float(qty), mix)
|
||||
elif code.startswith("M") and qty is not None:
|
||||
nm, sp, un = mats.get(base) or ["", "", ""]
|
||||
fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": float(qty), "밑품": float(qty)})
|
||||
elif code.startswith("S") and qty is not None:
|
||||
nm, sp = cost.get(base) or ["", ""]
|
||||
ts = mtypes(nm)
|
||||
if ts:
|
||||
seen_x.setdefault((f"{sec}.{sub}" if sub else str(sec), base), (5, nm, sp, float(qty), None))
|
||||
else:
|
||||
fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": float(qty), "밑품": float(qty)})
|
||||
for (sec_, base), (_, nm, sp, q, mx) in seen_x.items():
|
||||
fp["장비"].append(
|
||||
{"갈래": sorted(mtypes(nm)), "규격": sorted(specnums(sp) | specnums(nm)), "시간": q, "밑시간": q / mx if mx else q,
|
||||
"율": [[mx, "섞음"]] if mx else [], "절": sec_, "글": f"{nm} {sp}"}
|
||||
)
|
||||
return fp
|
||||
|
||||
|
||||
def main():
|
||||
out = [north(r) for r in NORTH]
|
||||
T = yw_tables()
|
||||
out += [yeongwol(r, T) for r in YW_ITEMS]
|
||||
for fp in out:
|
||||
for one in fp["인력"].values():
|
||||
one["품"] = round(one["품"], 6)
|
||||
one["밑품"] = round(one["밑품"], 6)
|
||||
(HERE / "excel_fp.json").write_text(json.dumps(out, ensure_ascii=False, indent=1), "utf-8")
|
||||
stat = {
|
||||
"고유": len(out),
|
||||
"인력 있음": sum(1 for f in out if f["인력"]),
|
||||
"장비 있음": sum(1 for f in out if f["장비"]),
|
||||
"자재 있음": sum(1 for f in out if f["자재"]),
|
||||
"율 있음": sum(1 for f in out if f["율"]),
|
||||
"계수 있음": sum(1 for f in out if f["계수"]),
|
||||
"조건 있음": sum(1 for f in out if f["조건"]),
|
||||
"조항 있음": sum(1 for f in out if f["출처"]),
|
||||
"역산 직종": sum(1 for f in out for o in f["인력"].values() if "역산" in o["어떻게"]),
|
||||
"아무것도 없음": [f["id"] for f in out if not (f["인력"] or f["장비"] or f["자재"])],
|
||||
}
|
||||
print(json.dumps(stat, ensure_ascii=False))
|
||||
want = sys.argv[1:]
|
||||
for fp in out:
|
||||
if fp["id"] in want:
|
||||
show = {k: fp[k] for k in ("id", "이름", "규격", "단위", "출처", "인력", "장비", "자재", "율", "계수", "조건", "옛판")}
|
||||
print(json.dumps(show, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,253 @@
|
||||
"""req226 C4 ② 우리 돈 로직 1,438 지문 — 정적(직종 · 장비 · 자재 · 덧줄 % · 조항 · 조건 낱말) +
|
||||
입력 조합마다 M01 엔진으로 단위당 품(인 · 장비 시간 · 자재 수량)을 셈(읽기만 · 원본 안 고침).
|
||||
|
||||
조합 = 기본값(자동값) + 고르기 입력(지역 · 조종원직종 · 보정작업 · 기종 목록 뺌) 곱이 64 이하면 모두 ·
|
||||
넘으면 하나씩 바꿈. 기종 목록 입력은 고를 수 있는 장비 갈래 · 규격만 적음.
|
||||
python s2_ours_fp.py <시작> <끝> → ours_fp_<시작>.json
|
||||
python s2_ours_fp.py merge → ours_fp.json + 덮는 범위 찍음
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from decimal import Decimal
|
||||
|
||||
import fp_common as F
|
||||
from fp_common import HERE, conds, mtypes, specnums
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
|
||||
FILES, WHOLE = m01.loaded()
|
||||
MF = m01.cm.mf
|
||||
LB = {str(r.get("키")): str(r.get("이름")) for r in WHOLE.numbers.get("LB", {}).values()}
|
||||
EQ = WHOLE.numbers.get("EQ", {})
|
||||
_PER = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?")
|
||||
_SUB = re.compile(r"^\s*로직\(\s*(\w+)\s*(?:,(.*))?\)\s*$")
|
||||
_EQCODE = re.compile(r"^\d{4}-\d{4}$")
|
||||
_PCT = re.compile(r"\*\s*([\d.]+)\s*/\s*100")
|
||||
SKIP_IN = ("지역", "조종원직종", "보정작업")
|
||||
FIXED_MACH = {"GC000267": {"굴착기"}, "GC000270": {"굴착기", "집게"}, "GC000271": {"굴착기", "브레이커"}, "GF000145": {"덤프트럭"}}
|
||||
|
||||
|
||||
def is_mach_logic(row) -> bool:
|
||||
return "시간당" in str(row.get("이름") or "")
|
||||
|
||||
|
||||
def eq_of(code):
|
||||
one = EQ.get(str(code)) or {}
|
||||
if not one:
|
||||
return set(), set()
|
||||
return mtypes(f"{one.get('상세구분')} {one.get('이름')}"), specnums(one.get("규격"))
|
||||
|
||||
|
||||
def times_of(row) -> float:
|
||||
per = _PER.findall(str(row.get("결과단위") or ""))
|
||||
return float(per[0].replace(",", "")) if per and per[0] else 1.0
|
||||
|
||||
|
||||
def _args(text):
|
||||
out = {}
|
||||
for part in re.split(r",(?![^()]*\))", text or ""):
|
||||
if "=" in part:
|
||||
k, v = part.split("=", 1)
|
||||
out[k.strip()] = v.strip()
|
||||
return out
|
||||
|
||||
|
||||
def static(code, row, unit):
|
||||
our = hb.ours().get(code, {})
|
||||
picks = [s for s in row.get("입력") or [] if s.get("고르기")]
|
||||
labels = []
|
||||
opt_mach = []
|
||||
for s in picks:
|
||||
names = s.get("고르기이름") or {}
|
||||
for v in s.get("고르기") or []:
|
||||
if isinstance(v, str) and _EQCODE.match(v):
|
||||
ts, sp = eq_of(v)
|
||||
if ts:
|
||||
opt_mach.append([sorted(ts), sorted(sp)])
|
||||
else:
|
||||
labels.append(str(names.get(v) or v))
|
||||
trades, mach, mats = set(), [], []
|
||||
for h in row.get("호표") or []:
|
||||
kind = h.get("종류")
|
||||
if kind == "인력":
|
||||
trades.add(LB.get(str(h.get("요소")), str(h.get("이름"))))
|
||||
elif kind == "로직":
|
||||
m = _SUB.match(str(h.get("요소") or ""))
|
||||
sub = m.group(1) if m else ""
|
||||
ts = set(FIXED_MACH.get(sub, set())) | mtypes(h.get("이름"))
|
||||
for v in _args(m.group(2) if m else "").values():
|
||||
lit = v.strip("'\"")
|
||||
if _EQCODE.match(lit):
|
||||
a, b = eq_of(lit)
|
||||
ts |= a
|
||||
if ts:
|
||||
mach.append([sorted(ts), sorted(specnums(h.get("이름")))])
|
||||
elif kind == "재료":
|
||||
el = h.get("요소")
|
||||
words = el.get("검색어") or el.get("이름") or el.get("상세구분") if isinstance(el, dict) else ""
|
||||
mats.append(f"{h.get('이름')} {words or ''}".strip())
|
||||
pct = []
|
||||
for d in row.get("덧줄") or []:
|
||||
for v in _PCT.findall(str(d.get("식") or "")):
|
||||
pct.append([str(d.get("이름")), float(v)])
|
||||
if is_mach_logic(row):
|
||||
mach.append([sorted(FIXED_MACH.get(code, set())), []])
|
||||
return {
|
||||
"로직": code,
|
||||
"이름": str(row.get("이름") or ""),
|
||||
"단위": unit,
|
||||
"결과단위": str(row.get("결과단위") or ""),
|
||||
"책": our.get("책"),
|
||||
"부문": our.get("부문"),
|
||||
"조항": sorted(our.get("조항") or []),
|
||||
"원문번호": str(row.get("원문번호") or ""),
|
||||
"상세구분": str(row.get("상세구분") or ""),
|
||||
"직종": sorted(trades),
|
||||
"장비": mach,
|
||||
"장비고름": opt_mach,
|
||||
"자재": mats,
|
||||
"덧줄율": pct,
|
||||
"이름조건": {k: sorted(v) for k, v in conds(row.get("이름")).items()},
|
||||
"고름조건": {k: sorted(v) for k, v in conds(" ".join(labels + [str(s.get("이름")) for s in picks])).items()},
|
||||
"기계경비": is_mach_logic(row),
|
||||
}
|
||||
|
||||
|
||||
def combos(row):
|
||||
picks = []
|
||||
for s in row.get("입력") or []:
|
||||
opts = s.get("고르기") or []
|
||||
if not opts or any(w in str(s.get("이름")) for w in SKIP_IN):
|
||||
continue
|
||||
if any(isinstance(v, str) and _EQCODE.match(v) for v in opts):
|
||||
continue
|
||||
picks.append((str(s["이름"]), list(opts)))
|
||||
if not picks:
|
||||
return [{}]
|
||||
if math.prod(len(o) for _, o in picks) <= 64:
|
||||
return [dict(zip([n for n, _ in picks], vals)) for vals in itertools.product(*[o for _, o in picks])]
|
||||
out = [{}]
|
||||
for n, opts in picks:
|
||||
out += [{n: v} for v in opts]
|
||||
return out
|
||||
|
||||
|
||||
def run_one(code, row, given, times, by_name):
|
||||
auto = {k: v for k, v in m01.mau.values(WHOLE, row).items() if v is not None}
|
||||
got = MF.run(WHOLE, code, {**auto, **m01._dec(given)})
|
||||
mid = got.get("중간") or {}
|
||||
L, X, M, S = {}, [], {}, {}
|
||||
for line in got.get("줄") or []:
|
||||
h = by_name.get(line.get("이름"))
|
||||
if h is None:
|
||||
continue
|
||||
try:
|
||||
q = float(Decimal(str(line.get("수량") or 0))) / times
|
||||
except ArithmeticError:
|
||||
continue
|
||||
kind = h.get("종류")
|
||||
if kind == "인력":
|
||||
t = LB.get(str(h.get("요소")), str(h.get("이름")))
|
||||
L[t] = L.get(t, 0.0) + q
|
||||
elif kind == "로직":
|
||||
m = _SUB.match(str(h.get("요소") or ""))
|
||||
sub = m.group(1) if m else ""
|
||||
sub_row = WHOLE.logic(sub) if sub else {}
|
||||
if sub_row and is_mach_logic(sub_row):
|
||||
ts, sp = set(FIXED_MACH.get(sub, set())) | mtypes(h.get("이름")), specnums(h.get("이름"))
|
||||
for v in _args(m.group(2)).values():
|
||||
lit = v.strip("'\"")
|
||||
if not _EQCODE.match(lit):
|
||||
lit = str(mid.get(lit) or given.get(lit) or auto.get(lit) or "")
|
||||
if _EQCODE.match(lit):
|
||||
a, b = eq_of(lit)
|
||||
ts |= a
|
||||
sp |= b
|
||||
if q:
|
||||
X.append([sorted(ts), sorted(sp), q])
|
||||
elif q:
|
||||
S[sub] = S.get(sub, 0.0) + q
|
||||
elif kind == "재료" and q:
|
||||
M[str(h.get("이름"))] = M.get(str(h.get("이름")), 0.0) + q
|
||||
mix = {}
|
||||
for k in ("인력비율", "기계비율"):
|
||||
try:
|
||||
if mid.get(k) not in (None, ""):
|
||||
mix[k] = float(Decimal(str(mid.get(k))))
|
||||
except ArithmeticError:
|
||||
pass
|
||||
return {"L": L, "X": X, "M": M, "S": S, "mix": mix}
|
||||
|
||||
|
||||
def _key(v):
|
||||
r = lambda x: float(f"{x:.4g}") # noqa: E731
|
||||
return json.dumps(
|
||||
[sorted((k, r(q)) for k, q in v["L"].items()), sorted((tuple(a), tuple(b), r(q)) for a, b, q in v["X"]), sorted((k, r(q)) for k, q in v["M"].items())],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
def fingerprint(one):
|
||||
code = one["로직"]
|
||||
row = WHOLE.logic(code)
|
||||
fp = static(code, row, one["단위"])
|
||||
times = times_of(row)
|
||||
by_name = {}
|
||||
for h in row.get("호표") or []:
|
||||
by_name.setdefault(h.get("이름"), h)
|
||||
seen, variants, errors = set(), [], 0
|
||||
for given in combos(row) if not fp["기계경비"] else [{}]:
|
||||
try:
|
||||
v = run_one(code, row, given, times, by_name)
|
||||
except Exception: # noqa: BLE001 — 못 도는 조합은 셈만
|
||||
errors += 1
|
||||
continue
|
||||
k = _key(v)
|
||||
if k in seen:
|
||||
continue
|
||||
seen.add(k)
|
||||
variants.append({"in": given, **v})
|
||||
fp["갈래수"] = len(variants)
|
||||
fp["오류"] = errors
|
||||
fp["변형"] = variants
|
||||
return fp
|
||||
|
||||
|
||||
def main():
|
||||
got = hm.targets()
|
||||
if sys.argv[1:] == ["merge"]:
|
||||
out = []
|
||||
for p in sorted(HERE.glob("ours_fp_*.json"), key=lambda p: int(p.stem.split("_")[-1])):
|
||||
out += json.loads(p.read_text(encoding="utf-8"))
|
||||
(HERE / "ours_fp.json").write_text(json.dumps(out, ensure_ascii=False, default=str), "utf-8")
|
||||
ran = [f for f in out if f["변형"]]
|
||||
stat = {
|
||||
"로직": len(out),
|
||||
"셈 됨(변형 1+)": len(ran),
|
||||
"못 셈": [f["로직"] for f in out if not f["변형"]][:40],
|
||||
"인력 있음": sum(1 for f in ran if any(v["L"] for v in f["변형"])),
|
||||
"장비 있음": sum(1 for f in out if f["장비"] or any(v["X"] for v in f["변형"])),
|
||||
"자재 있음": sum(1 for f in out if f["자재"]),
|
||||
"기계경비 로직": sum(1 for f in out if f["기계경비"]),
|
||||
"변형 합": sum(f["갈래수"] for f in out),
|
||||
"변형 2+": sum(1 for f in out if f["갈래수"] > 1),
|
||||
"조항 있음": sum(1 for f in out if f["조항"]),
|
||||
}
|
||||
print(json.dumps(stat, ensure_ascii=False))
|
||||
return
|
||||
a, b = int(sys.argv[1]), int(sys.argv[2])
|
||||
t0 = time.time()
|
||||
out = [fingerprint(one) for one in got["로직"][a:b]]
|
||||
(HERE / f"ours_fp_{a}.json").write_text(json.dumps(out, ensure_ascii=False, default=str), "utf-8")
|
||||
print(a, b, len(out), "변형", sum(f["갈래수"] for f in out), "오류", sum(f["오류"] for f in out), round(time.time() - t0, 1), "s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,355 @@
|
||||
"""req226 C4 ③ 지문 비교 맞춤 흉내 — 직종 · 품 벡터 · 장비 · 자재 · 단위 · 조건 · 조항(한 신호) · 이름(약한 덤).
|
||||
금액 안 씀. 앞 조사와 같은 정답(labels · labels_y) · 같은 셈(s10_all.score)으로 지금 · 묶음 ① 과 맞댐.
|
||||
|
||||
python s3_match.py → 표 · out_match.json(줄마다 1~5 위 · 채널 · 고른 입력)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import fp_common as F
|
||||
from fp_common import HERE, close, cond_score, cosine, jacc
|
||||
|
||||
import s10_all as A # noqa: E402 (C1 — 정답 · 셈 · 지금 돌리기)
|
||||
from cats import NOW, ORDER # noqa: E402
|
||||
from s04_eval import clause_prefix # noqa: E402
|
||||
from s20_c3_extra import combo # noqa: E402
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
|
||||
EX = {e["id"]: e for e in json.loads((HERE / "excel_fp.json").read_text(encoding="utf-8"))}
|
||||
OURS = json.loads((HERE / "ours_fp.json").read_text(encoding="utf-8"))
|
||||
TG = {x["로직"]: x for x in hm.targets()["로직"]}
|
||||
OURC = hb.ours()
|
||||
W = {"인력": 3.0, "장비": 2.0, "자재": 1.0, "율": 0.3, "단위": 0.5, "조건": 0.6, "조항": 1.5, "이름": 0.6}
|
||||
WHOLE = m01.loaded()[1]
|
||||
|
||||
|
||||
def prep(o):
|
||||
"""맞춤용 정리 — 기계경비 로직은 조종원 뺌 · 모두 0 인 변형 뺌 · 고를 수 있는 장비."""
|
||||
vs = []
|
||||
for v in o["변형"]:
|
||||
L = {} if o["기계경비"] else {t: q for t, q in v["L"].items() if q > 0}
|
||||
X = [(set(a), set(b), q) for a, b, q in v["X"] if q > 0]
|
||||
M = {k: q for k, q in v["M"].items() if q > 0}
|
||||
if L or X or M or not (v["L"] or v["X"] or v["M"]):
|
||||
vs.append({"in": v["in"], "L": L, "X": X, "M": M})
|
||||
mix = v.get("mix") or {}
|
||||
ri, rm = mix.get("인력비율"), mix.get("기계비율")
|
||||
if (ri and 0 < ri < 100) or (rm and 0 < rm < 100): # 섞기 전 품(인력 · 기계 100%)
|
||||
Lu = {t: q * 100 / ri for t, q in L.items()} if ri and 0 < ri < 100 else L
|
||||
Xu = [(a, b, q * 100 / rm) for a, b, q in X] if rm and 0 < rm < 100 else X
|
||||
vs.append({"in": {**v["in"], "_섞기전": f"인력 {ri:g}% · 기계 {rm:g}% → 100%"}, "L": Lu, "X": Xu, "M": M})
|
||||
if not vs: # 엔진이 못 돈 로직 — 정적 지문(직종 이름만 · 품 모름)
|
||||
vs = [{"in": None, "L": {t: None for t in o["직종"]}, "X": [(set(a), set(b), None) for a, b in o["장비"] if a], "M": {}}]
|
||||
opt = []
|
||||
if o["기계경비"]:
|
||||
ts = set().union(*[set(a) for a, _ in o["장비고름"]], *[set(a) for a, _ in o["장비"]]) if (o["장비고름"] or o["장비"]) else set()
|
||||
sp = set().union(*[set(b) for _, b in o["장비고름"]]) if o["장비고름"] else set()
|
||||
opt = [(ts, sp)]
|
||||
mats = set()
|
||||
for name in o["자재"]:
|
||||
mats |= mat_words(name)
|
||||
for v in vs:
|
||||
for name in v["M"]:
|
||||
mats |= mat_words(name)
|
||||
return {
|
||||
**o,
|
||||
"vs": vs,
|
||||
"opt": opt,
|
||||
"matw": mats,
|
||||
"pctv": {round(p, 3) for _, p in o["덧줄율"]},
|
||||
"nameg": TG[o["로직"]]["grams"],
|
||||
"nc": {k: set(v) for k, v in o["이름조건"].items()},
|
||||
"oc": {k: set(v) for k, v in o["고름조건"].items()},
|
||||
"has_L": any(v["L"] for v in vs),
|
||||
"has_X": any(v["X"] for v in vs) or bool(opt),
|
||||
}
|
||||
|
||||
|
||||
def mat_words(name):
|
||||
t = F.unicodedata.normalize("NFKC", str(name or "")).lower().replace(".", "").replace(" ", "")
|
||||
out = set(hm.words(name))
|
||||
if "pvc" in t:
|
||||
out.add("pvc")
|
||||
return {w for w in out if len(w) >= 2}
|
||||
|
||||
|
||||
P = [prep(o) for o in OURS]
|
||||
|
||||
|
||||
def lab_sim(E, Eb, O, unit_ok):
|
||||
if not O:
|
||||
return 0.0, None
|
||||
keys = set(E) | set(O)
|
||||
w = lambda t: 0.5 if t == "보통인부" else 1.0 # noqa: E731
|
||||
st = sum(w(t) for t in set(E) & set(O)) / sum(w(t) for t in keys)
|
||||
if any(q is None for q in O.values()): # 품 모름(정적) — 꼴 = 직종 겹침으로 갈음 · 크기 덤 없음
|
||||
return 0.6 * st, None
|
||||
shape = max(cosine(E, O), cosine(Eb, O))
|
||||
if not unit_ok: # 단위가 달라 크기를 못 견줌 — 크기 덤 없음
|
||||
return 0.35 * st + 0.25 * shape, None
|
||||
mag = sum(max(close(E.get(t), O.get(t)), close(Eb.get(t), O.get(t))) for t in keys) / len(keys)
|
||||
return 0.35 * st + 0.25 * shape + 0.40 * mag, mag
|
||||
|
||||
|
||||
def mach_sim(EM, OX, opt, unit_ok):
|
||||
if not OX and not opt:
|
||||
return 0.0
|
||||
got = []
|
||||
for m in EM:
|
||||
mt, ms = set(m["갈래"]), set(m["규격"])
|
||||
best = 0.0
|
||||
for ot, osp, oh in OX:
|
||||
j = jacc(mt, ot)
|
||||
if not j:
|
||||
continue
|
||||
sp = (1.0 if ms & osp else 0.0) if ms and osp else 0.5
|
||||
hc = max(close(m.get("시간"), oh), close(m.get("밑시간"), oh)) if unit_ok and oh else 0.0
|
||||
best = max(best, 0.6 * j + 0.2 * sp + 0.2 * hc)
|
||||
for ot, osp in opt:
|
||||
if mt and mt <= ot:
|
||||
sp = (1.0 if ms & osp else 0.0) if ms and osp else 0.5
|
||||
best = max(best, 0.6 + 0.2 * sp + 0.2 * (1.0 if unit_ok else 0.0))
|
||||
got.append(best)
|
||||
s = sum(got) / len(got)
|
||||
et = set().union(*[set(m["갈래"]) for m in EM])
|
||||
ot_all = set().union(*[t for t, _, _ in OX]) if OX else set()
|
||||
extra = len(ot_all - et) / max(1, len(ot_all | et)) if ot_all else 0.0
|
||||
return s * (1 - 0.5 * extra)
|
||||
|
||||
|
||||
def ex_prep(e):
|
||||
E = {t: o["품"] for t, o in e["인력"].items() if o["품"]}
|
||||
Eb = {t: o["밑품"] for t, o in e["인력"].items() if o["밑품"]}
|
||||
mats = [mat_words(m["이름"]) for m in e["자재"]]
|
||||
return {
|
||||
**e,
|
||||
"E": E,
|
||||
"Eb": Eb,
|
||||
"matl": [m for m in mats if m],
|
||||
"pctv": {round(v, 3) for _, v in e["율"]},
|
||||
"nameg": hm._grams(hm.tidy(e["이름"])),
|
||||
"theirs": [list(x) for x in e["출처"]],
|
||||
"조건": {k: set(v) for k, v in e["조건"].items()},
|
||||
"이름조건": {k: set(v) for k, v in e["이름조건"].items()},
|
||||
}
|
||||
|
||||
|
||||
COUNT = {"개소", "개", "본", "ea", "조", "매", "대", "기", "공"}
|
||||
HOUR = {"hr", "시간", "h"}
|
||||
|
||||
|
||||
def unit_fit(a, b):
|
||||
"""단위 맞음 + 별칭(개소 = 개 = 본 · hr = 시간 · 공/㎥ = 공㎥) — 품 크기 견줌에만."""
|
||||
if hm.fits(a, b):
|
||||
return True
|
||||
na = F.re.sub(r"[\s/]", "", str(a or "")).lower()
|
||||
nbs = [F.re.sub(r"[\s/]", "", x).lower() for x in str(b or "").split("·")]
|
||||
for nb in nbs:
|
||||
if na == nb or (na in COUNT and nb in COUNT) or (na in HOUR and nb in HOUR):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def score_pair(e, o, use=None):
|
||||
"""(점수, 채널, 고른 변형) — use = 쓸 채널(None = 모두)."""
|
||||
use = use or W
|
||||
unit_ok = unit_fit(e["단위"], o["단위"])
|
||||
ch = {"단위": 1.0 if unit_ok else 0.0}
|
||||
g1, g2 = e["nameg"], o["nameg"]
|
||||
ch["이름"] = 2 * len(g1 & g2) / (len(g1) + len(g2)) if g1 and g2 else 0.0
|
||||
our = OURC.get(o["로직"])
|
||||
c = clause_prefix(e["theirs"], our)[0] if (our and e["theirs"]) else 0.0
|
||||
ch["조항"] = c * (0.5 if e["옛판"] else 1.0)
|
||||
hit, _ = cond_score(e["조건"], o["nc"], o["oc"])
|
||||
_, clash = cond_score(e["이름조건"], o["nc"], {})
|
||||
ch["조건"] = hit - clash
|
||||
if e["matl"]:
|
||||
ch["자재"] = sum(1 for ws in e["matl"] if any(a in b or b in a for a in ws for b in o["matw"])) / len(e["matl"])
|
||||
ch["율"] = 1.0 if (e["pctv"] and o["pctv"] and e["pctv"] & o["pctv"]) else 0.0
|
||||
base = sum(use.get(k, 0) * v for k, v in ch.items())
|
||||
best, bestv, bestch = None, None, None
|
||||
for v in o["vs"]:
|
||||
s, vch = base, {}
|
||||
if e["E"]:
|
||||
ls, mag = lab_sim(e["E"], e["Eb"], v["L"], unit_ok)
|
||||
vch["인력"], vch["품"] = ls, mag
|
||||
s += use.get("인력", 0) * ls
|
||||
elif v["L"] and "인력" in use:
|
||||
s -= 0.6
|
||||
if e["장비"]:
|
||||
ms = mach_sim(e["장비"], v["X"], o["opt"], unit_ok)
|
||||
vch["장비"] = ms
|
||||
s += use.get("장비", 0) * ms
|
||||
elif (v["X"] or o["opt"]) and "장비" in use:
|
||||
s -= 0.4
|
||||
if best is None or s > best:
|
||||
best, bestv, bestch = s, v, vch
|
||||
return best, {**ch, **bestch}, bestv
|
||||
|
||||
|
||||
def rank(e, use=None, n=5):
|
||||
scored = []
|
||||
for o in P:
|
||||
s, ch, v = score_pair(e, o, use)
|
||||
scored.append((s, o["로직"], ch, v))
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
return scored[:n]
|
||||
|
||||
|
||||
RB = {0: 1.0, 1: 0.6, 2: 0.45, 3: 0.35, 4: 0.25}
|
||||
|
||||
|
||||
def sure(e, pool):
|
||||
"""숫자가 딱 맞음 — 1위의 직종 · 품(3% 안)이 다 맞거나(인력 줄) 장비 갈래 · 시간이 맞음(장비만 줄) ·
|
||||
2위와 점수 차 0.3 넘음."""
|
||||
if not pool:
|
||||
return False
|
||||
s1, _, ch, _ = pool[0]
|
||||
gap = s1 - pool[1][0] if len(pool) > 1 else 9
|
||||
if e["E"]:
|
||||
rich = len(e["E"]) >= 2 or set(e["E"]) != {"보통인부"} or bool(e["장비"]) # 보통인부 하나뿐이면 우연 같음
|
||||
ok = rich and (ch.get("품") or 0) >= 0.97 and ch.get("인력", 0) >= 0.97
|
||||
elif e["장비"]:
|
||||
ok = ch.get("장비", 0) >= 0.95
|
||||
else:
|
||||
ok = False
|
||||
return ok and gap > 0.3
|
||||
|
||||
|
||||
def fuse(fp_top, base, beta=2.0, pool=None):
|
||||
"""지문 점수 + β × 묶음 ① 차례 덤 → 다시 다섯."""
|
||||
bonus = {k: RB.get(i, 0) for i, k in enumerate(base["top"][:5])}
|
||||
cand = {k: s for s, k, _, _ in (pool or fp_top)}
|
||||
for k in bonus:
|
||||
if k not in cand:
|
||||
o = next(x for x in P if x["로직"] == k)
|
||||
cand[k] = score_pair(EXP[CUR["id"]], o)[0]
|
||||
out = sorted(cand, key=lambda k: -(cand[k] + beta * bonus.get(k, 0)))
|
||||
return out[:5]
|
||||
|
||||
|
||||
CUR = {"id": None}
|
||||
EXP = {i: ex_prep(e) for i, e in EX.items()}
|
||||
|
||||
|
||||
def as_results(tops):
|
||||
return {i: {"상태": "후보", "top": t} for i, t in tops.items()}
|
||||
|
||||
|
||||
def fails_by_cat(res, base_fail):
|
||||
"""지금 실패 36 의 갈래별 — 고쳐짐 수 / 갈래 수 · 새 실패."""
|
||||
out = {}
|
||||
for i in base_fail:
|
||||
cat = (NOW.get(i) or ["(새)"])[0]
|
||||
ok = bool(res[i]["top"]) and res[i]["top"][0] in set(A.LAB[i]["정답"])
|
||||
a = out.setdefault(cat, [0, 0, []])
|
||||
a[1] += 1
|
||||
a[0] += ok
|
||||
if not ok:
|
||||
a[2].append(i)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ids = list(A.BY)
|
||||
have = [i for i in ids if A.LAB[i]["정답"]]
|
||||
now = A.run()
|
||||
rec = combo()
|
||||
tops, detail, pools = {}, {}, {}
|
||||
variants = {
|
||||
"지문 전부": None,
|
||||
"지문 − 조항": {k: v for k, v in W.items() if k != "조항"},
|
||||
"지문 − 이름": {k: v for k, v in W.items() if k != "이름"},
|
||||
"지문 − 조항 − 이름(속만)": {k: v for k, v in W.items() if k not in ("조항", "이름")},
|
||||
"지문 − 품(직종 · 장비 갈래만)": "noqty",
|
||||
}
|
||||
rows, by_var = [], {}
|
||||
for name, use in variants.items():
|
||||
t = {}
|
||||
for i in ids:
|
||||
e = EXP[i]
|
||||
if use == "noqty":
|
||||
e = {**e, "E": {k: 1.0 for k in e["E"]}, "Eb": {k: 1.0 for k in e["E"]}, "장비": [{**m, "시간": None, "밑시간": None} for m in e["장비"]]}
|
||||
top = rank(e, None if use == "noqty" else use, 40)
|
||||
t[i] = [k for _, k, _, _ in top[:5]]
|
||||
if name == "지문 전부":
|
||||
pools[i] = top
|
||||
detail[i] = [{"로직": k, "점수": round(s, 3), "채널": {a: (round(b, 3) if isinstance(b, float) else b) for a, b in ch.items()}, "입력": v["in"] if v else None} for s, k, ch, v in top[:5]]
|
||||
by_var[name] = t
|
||||
rows.append(A.table(name, as_results(t)))
|
||||
base_rows = [A.table("지금", now), A.table("묶음 ①", rec)]
|
||||
fused = {}
|
||||
for beta in (1.0, 2.0, 3.0):
|
||||
t = {}
|
||||
for i in ids:
|
||||
CUR["id"] = i
|
||||
t[i] = fuse(None, rec[i], beta, pools[i])
|
||||
fused[beta] = t
|
||||
rows.append(A.table(f"지문 + 묶음 ① (β={beta:g})", as_results(t)))
|
||||
t = {}
|
||||
for i in ids:
|
||||
CUR["id"] = i
|
||||
t[i] = fuse(None, now[i], 2.0, pools[i])
|
||||
rows.append(A.table("지문 + 지금 (β=2)", as_results(t)))
|
||||
keep_same = {i: (rec[i]["top"] if rec[i]["상태"] == "같음" else by_var["지문 전부"][i]) for i in ids}
|
||||
rows.append(A.table("같음은 묶음 ① · 나머지 지문", as_results(keep_same)))
|
||||
keep_same2 = {i: (rec[i]["top"] if rec[i]["상태"] == "같음" else fused[1.0][i]) for i in ids}
|
||||
rows.append(A.table("같음은 묶음 ① · 나머지 지문+① β=1", as_results(keep_same2)))
|
||||
print("묶음 ① 같음 줄", sum(1 for i in ids if rec[i]["상태"] == "같음"))
|
||||
gate = {}
|
||||
for i in ids:
|
||||
gate[i] = by_var["지문 전부"][i] if sure(EXP[i], pools[i]) else fused[2.0][i]
|
||||
rows.append(A.table("지문 우선(딱 맞음) + 묶음 ①", as_results(gate)))
|
||||
with A.no_seeds():
|
||||
rows.append(A.table("지금 (씨앗 끔)", A.run()))
|
||||
rec_off = combo()
|
||||
rows.append(A.table("묶음 ① (씨앗 끔)", rec_off))
|
||||
t2, g2 = {}, {}
|
||||
for i in ids:
|
||||
CUR["id"] = i
|
||||
t2[i] = fuse(None, rec_off[i], 2.0, pools[i])
|
||||
g2[i] = by_var["지문 전부"][i] if sure(EXP[i], pools[i]) else t2[i]
|
||||
rows.append(A.table("지문 + 묶음 ① (씨앗 끔 β=2)", as_results(t2)))
|
||||
rows.append(A.table("지문 우선 + 묶음 ① (씨앗 끔)", as_results(g2)))
|
||||
k2 = {}
|
||||
for i in ids:
|
||||
CUR["id"] = i
|
||||
k2[i] = rec_off[i]["top"] if rec_off[i]["상태"] == "같음" else fuse(None, rec_off[i], 1.0, pools[i])
|
||||
rows.append(A.table("같음은 묶음 ① · 나머지 지문+① β=1 (씨앗 끔)", as_results(k2)))
|
||||
print("딱 맞음 줄", sum(1 for i in ids if sure(EXP[i], pools[i])), "/", len(ids),
|
||||
"· 그중 지문 1위 엄격 맞음", sum(1 for i in have if sure(EXP[i], pools[i]) and by_var["지문 전부"][i][:1] and by_var["지문 전부"][i][0] in set(A.LAB[i]["정답"])),
|
||||
"/", sum(1 for i in have if sure(EXP[i], pools[i])))
|
||||
for r in base_rows + rows:
|
||||
A.show(r)
|
||||
# 실패 갈래별
|
||||
base_fail = [i for i in have if not now[i]["top"] or now[i]["top"][0] not in set(A.LAB[i]["정답"])]
|
||||
cats = {
|
||||
"지금": fails_by_cat(now, base_fail),
|
||||
"묶음 ①": fails_by_cat(rec, base_fail),
|
||||
"지문": fails_by_cat(as_results(by_var["지문 전부"]), base_fail),
|
||||
"지문 + 묶음 ①": fails_by_cat(as_results(fused[2.0]), base_fail),
|
||||
}
|
||||
for k, v in cats.items():
|
||||
print(k, {c: f"{a}/{b}" for c, (a, b, _) in v.items()})
|
||||
newfail = {}
|
||||
for k, res in (("지문", as_results(by_var["지문 전부"])), ("지문 + 묶음 ①", as_results(fused[2.0])), ("묶음 ①", rec)):
|
||||
newfail[k] = [i for i in have if i not in base_fail and (not res[i]["top"] or res[i]["top"][0] not in set(A.LAB[i]["정답"]))]
|
||||
print("새 실패", k, newfail[k])
|
||||
out = {
|
||||
"표": base_rows + rows,
|
||||
"갈래": {k: {c: v for c, v in d.items()} for k, d in cats.items()},
|
||||
"새 실패": newfail,
|
||||
"추천": keep_same2,
|
||||
"딱 맞음": [i for i in ids if sure(EXP[i], pools[i])],
|
||||
"줄": {i: {"1~5": detail[i], "지문만": by_var["지문 전부"][i], "지문+묶음①": fused[2.0][i], "묶음①": rec[i]["top"][:5], "지금": now[i]["top"][:5], "정답": A.LAB[i]["정답"], "허용": A.LAB[i]["허용"]} for i in ids},
|
||||
}
|
||||
(HERE / "out_match.json").write_text(json.dumps(out, ensure_ascii=False, indent=1, default=str), "utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.setrecursionlimit(10000)
|
||||
main()
|
||||
@@ -0,0 +1,330 @@
|
||||
"""req226 C4 ④⑤ — 절(몫)마다 맞춤 → 로직 조합 · 「편의대로 바꿈」 꼴 나눔 · 「로직 X · 계수 바뀜」 판정.
|
||||
|
||||
정답 있는 줄 전부(고유 126)에서 엑셀 지문 ↔ 정답 로직(가장 닮은 변형)을 견줌. 금액 안 씀.
|
||||
python s4_changes.py → 표 찍음 · out_changes.json
|
||||
"""
|
||||
|
||||
import json
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
import s3_match as S
|
||||
from fp_common import HERE, close
|
||||
|
||||
A, P, EXP, TG, OURC = S.A, S.P, S.EXP, S.TG, S.OURC
|
||||
BYCODE = {o["로직"]: o for o in P}
|
||||
COMPOSITE = {
|
||||
**{i: {"GC000206", "GC000207"} for i in ("N003", "N019", "N045", "S055", "S059", "Y010")},
|
||||
"N050": {"GC000705", "GC000714", "GC000712"},
|
||||
"S062": {"GC000559", "GC000560"},
|
||||
}
|
||||
ALL_CLAUSES = defaultdict(set)
|
||||
for o in P:
|
||||
for n in o["조항"]:
|
||||
ALL_CLAUSES[o["책"]].add(n)
|
||||
TOL = 0.03
|
||||
|
||||
|
||||
def work_sections(e):
|
||||
secs = {}
|
||||
for sec, t, q, b in e["인력절"]:
|
||||
one = secs.setdefault(str(sec), {"인력": {}, "장비": []})
|
||||
a = one["인력"].setdefault(t, {"품": 0.0, "밑품": 0.0, "율": [], "어떻게": []})
|
||||
a["품"] += q
|
||||
a["밑품"] += b
|
||||
for m in e["장비"]:
|
||||
secs.setdefault(str(m["절"]), {"인력": {}, "장비": []})["장비"].append(m)
|
||||
return {k: v for k, v in secs.items() if v["인력"] or v["장비"]}
|
||||
|
||||
|
||||
def _order(key):
|
||||
a, _, b = str(key).partition(".")
|
||||
return (int(a) if a.isdigit() else 0, b)
|
||||
|
||||
|
||||
def section_fp(e, sec, part):
|
||||
head = e["절"].get(sec) or e["절"].get(str(sec).split(".")[0]) or {}
|
||||
sub = {**{k: v for k, v in e.items() if k in S.EX[e["id"]]}, "인력": part["인력"], "장비": part["장비"], "자재": [], "율": []}
|
||||
sub["이름"] = f"{e['이름']} {head.get('이름') or ''}".strip()
|
||||
sub["출처"] = head.get("출처") or e["출처"]
|
||||
sub["조건"] = {k: sorted(v) for k, v in e["조건"].items()}
|
||||
sub["이름조건"] = {}
|
||||
return S.ex_prep(sub)
|
||||
|
||||
|
||||
def combination(i):
|
||||
e = EXP[i]
|
||||
secs = work_sections(e)
|
||||
if len(secs) < 2:
|
||||
return None
|
||||
out = []
|
||||
for sec, part in sorted(secs.items(), key=lambda x: _order(x[0])):
|
||||
top = S.rank(section_fp(e, sec, part), None, 3)
|
||||
out.append({"절": sec, "이름": (e["절"].get(sec) or {}).get("이름", ""), "1위": top[0][1], "점수": round(top[0][0], 2), "2위": top[1][1]})
|
||||
return out
|
||||
|
||||
|
||||
def compare(e, code):
|
||||
"""엑셀 ↔ 우리 로직(가장 닮은 변형) — 직종마다 (엑셀 품, 밑품, 우리 품) · 판정."""
|
||||
o = BYCODE[code]
|
||||
s, ch, v = S.score_pair(e, o)
|
||||
O = {t: q for t, q in ((v or {}).get("L") or {}).items() if q}
|
||||
E = {t: x["품"] for t, x in e["인력"].items()}
|
||||
B = {t: x["밑품"] for t, x in e["인력"].items()}
|
||||
unit_ok = S.unit_fit(e["단위"], o["단위"])
|
||||
rows, same, base_only, changed = [], True, False, []
|
||||
for t in sorted(set(E) | set(O)):
|
||||
a, b, c = E.get(t), B.get(t), O.get(t)
|
||||
rows.append([t, a and round(a, 4), b and round(b, 4), c and round(c, 4)])
|
||||
if a is None or c is None or not c:
|
||||
same = False
|
||||
continue
|
||||
if abs(a / c - 1) <= TOL:
|
||||
continue
|
||||
if b and abs(b / c - 1) <= TOL:
|
||||
base_only = True
|
||||
continue
|
||||
same = False
|
||||
changed.append(f"{t} {c:.3g} → {a:.3g}")
|
||||
trades_same = set(E) == set(O) and bool(E)
|
||||
if not E and not O:
|
||||
verdict = mach_verdict(e, v, o, unit_ok)
|
||||
elif not unit_ok:
|
||||
verdict = "단위 달라 크기 못 견줌"
|
||||
elif any(q is None for q in ((v or {}).get("L") or {}).values()) or (v or {}).get("in") is None:
|
||||
verdict = "우리 품 모름(엔진 못 돎)"
|
||||
elif not trades_same:
|
||||
verdict = "직종 다름"
|
||||
elif same and not base_only:
|
||||
verdict = "같음(3% 안)"
|
||||
elif same and base_only:
|
||||
verdict = "섞음 · 할증 붙임(밑품 같음)"
|
||||
else:
|
||||
verdict = "계수 바뀜"
|
||||
return {"판정": verdict, "직종": rows, "바뀜": changed, "변형": (v or {}).get("in"), "점수": round(s, 2), "채널": {k: (round(x, 2) if isinstance(x, float) else x) for k, x in ch.items()}}
|
||||
|
||||
|
||||
def mach_verdict(e, v, o, unit_ok):
|
||||
"""인력 없는 줄 — 장비 갈래 · 시간으로."""
|
||||
EM = e["장비"]
|
||||
OX = list((v or {}).get("X") or [])
|
||||
if not EM:
|
||||
return "자재 · 그 밖(품 없음)"
|
||||
if o["기계경비"]:
|
||||
return "장비 시간 줄(기계경비 로직)"
|
||||
et = set().union(*[set(m["갈래"]) for m in EM])
|
||||
ot = set().union(*[set(a) for a, _, _ in OX]) if OX else set()
|
||||
if et != ot:
|
||||
return "장비 갈래 다름"
|
||||
if not unit_ok:
|
||||
return "단위 달라 크기 못 견줌"
|
||||
ok = all(any(set(m["갈래"]) & a and (close(m.get("시간"), q) >= 0.99 or close(m.get("밑시간"), q) >= 0.99) for a, _, q in OX) for m in EM)
|
||||
return "같음(3% 안)" if ok else "계수 바뀜"
|
||||
|
||||
|
||||
def extra_work(e, code):
|
||||
"""엑셀에 있는데 정답 로직(모든 변형 · 고를 수 있는 장비)엔 없는 장비 갈래 · 직종."""
|
||||
o = BYCODE[code]
|
||||
ot = set()
|
||||
tr = set(o["직종"])
|
||||
for v in o["vs"]:
|
||||
for a, _, _ in v["X"]:
|
||||
ot |= set(a)
|
||||
tr |= set(v["L"])
|
||||
for a, _ in o["opt"]:
|
||||
ot |= set(a)
|
||||
for a, _ in o["장비"]:
|
||||
ot |= set(a)
|
||||
et = set().union(*[set(m["갈래"]) for m in e["장비"]]) if e["장비"] else set()
|
||||
return sorted(et - ot), sorted(set(e["인력"]) - tr)
|
||||
|
||||
|
||||
def section_compare(i):
|
||||
"""복합 정답 줄 — 절마다 가장 닮은 정답 몫과 견줌."""
|
||||
e = EXP[i]
|
||||
out = []
|
||||
for sec, part in sorted(work_sections(e).items(), key=lambda x: _order(x[0])):
|
||||
sub = section_fp(e, sec, part)
|
||||
code = max(COMPOSITE[i], key=lambda k: S.score_pair(sub, BYCODE[k])[0])
|
||||
c = compare(sub, code)
|
||||
out.append({"절": sec, "로직": code, "판정": c["판정"], "바뀜": c["바뀜"]})
|
||||
return out
|
||||
|
||||
|
||||
def clause_kind(e, code):
|
||||
if not e["출처"]:
|
||||
return "출처 없음"
|
||||
our = OURC.get(code)
|
||||
c = S.clause_prefix(e["theirs"], our)[0] if our else 0
|
||||
if c >= 0.9:
|
||||
return "조항 같음"
|
||||
books = {b for b, _ in e["출처"]}
|
||||
if our and our["책"] not in books:
|
||||
return "다른 책 조항"
|
||||
missing = [n for b, n in e["출처"] if n not in ALL_CLAUSES.get(b, set()) and not any(x.startswith(n + "-") for x in ALL_CLAUSES.get(b, set()))]
|
||||
if e["옛판"] or missing:
|
||||
return "옛 판 · 없는 번호"
|
||||
return "다른 번호(같은 책)"
|
||||
|
||||
|
||||
def pick_answer(i):
|
||||
e = EXP[i]
|
||||
lab = A.LAB[i]["정답"]
|
||||
return max(lab, key=lambda k: S.score_pair(e, BYCODE[k])[0])
|
||||
|
||||
|
||||
def splits(ids):
|
||||
"""쪼갬 — 같은 파일 · 같은 정답 로직 줄 둘의 품을 더하면 우리 한 변형의 꼴(비율)과 같음(코사인 0.98+) ·
|
||||
한 줄만으로는 그 꼴이 아님."""
|
||||
from fp_common import cosine
|
||||
|
||||
found = set()
|
||||
by = defaultdict(list)
|
||||
for i in ids:
|
||||
if A.LAB[i]["정답"] and i not in COMPOSITE:
|
||||
by[(A.BY[i]["파일"], pick_answer(i))].append(i)
|
||||
for (f, code), group in by.items():
|
||||
vs = [v["L"] for v in BYCODE[code]["vs"] if v["L"] and all(q for q in v["L"].values())]
|
||||
for a in group:
|
||||
for b in group:
|
||||
if a >= b or not EXP[a]["E"] or not EXP[b]["E"]:
|
||||
continue
|
||||
if set(EXP[a]["E"]) == set(EXP[b]["E"]):
|
||||
continue # 같은 일 되풀이(규격만 다름)
|
||||
both = {t: EXP[a]["E"].get(t, 0) + EXP[b]["E"].get(t, 0) for t in set(EXP[a]["E"]) | set(EXP[b]["E"])}
|
||||
whole = max((cosine(both, O) for O in vs), default=0)
|
||||
alone = max((max(cosine(EXP[a]["E"], O), cosine(EXP[b]["E"], O)) for O in vs), default=0)
|
||||
if whole >= 0.98 and alone < 0.95:
|
||||
found |= {a, b}
|
||||
return found
|
||||
|
||||
|
||||
def main():
|
||||
ids = [i for i in A.BY if A.LAB[i]["정답"]]
|
||||
res = json.loads((HERE / "out_match.json").read_text(encoding="utf-8"))["줄"]
|
||||
# ── 로직 조합(절마다) ──
|
||||
combos = {i: combination(i) for i in A.BY}
|
||||
multi = [i for i in A.BY if combos[i]]
|
||||
comp_rows = []
|
||||
for i, truth in COMPOSITE.items():
|
||||
got = {c["1위"] for c in combos[i] or []}
|
||||
top1 = res[i]["지문만"][:1]
|
||||
comp_rows.append({"id": i, "이름": A.BY[i]["이름"], "정답 조합": sorted(truth), "절 1위": [c["1위"] for c in combos[i] or []], "조합 맞음": truth <= got, "줄 1위": top1})
|
||||
print("절 둘 이상 줄", len(multi), "· 복합 정답 줄", len(COMPOSITE), "· 조합 맞음", sum(r["조합 맞음"] for r in comp_rows))
|
||||
for r in comp_rows:
|
||||
print(" ", r)
|
||||
# 조합 맞음 지표 = 복합 줄은 절 1위 묶음 ⊇ 정답 조합 · 나머지는 줄 1위 엄격
|
||||
def combo_ok(i, top1):
|
||||
if i in COMPOSITE:
|
||||
return COMPOSITE[i] <= {c["1위"] for c in combos[i] or []}
|
||||
return bool(top1) and top1[0] in set(A.LAB[i]["정답"])
|
||||
|
||||
inside = sum(1 for i in ids if combos[i] and set(A.LAB[i]["정답"]) & {c["1위"] for c in combos[i]})
|
||||
print("절 둘 이상 줄 중 정답 로직이 절 1위 묶음 안에 있음", inside, "/", sum(1 for i in ids if combos[i]))
|
||||
for key in ("지금", "묶음①", "지문만", "지문+묶음①"):
|
||||
ok = sum(combo_ok(i, res[i][key]) for i in ids)
|
||||
comp_only = sum(1 for i in COMPOSITE if res[i][key][:1] and res[i][key][0] in COMPOSITE[i])
|
||||
print(f"조합 맞음 {key}: {ok}/{len(ids)} = {100 * ok / len(ids):.1f}% (복합 8 줄 1위가 한 몫이라도 맞음 {comp_only})")
|
||||
# ── 바꿈 꼴 ──
|
||||
sp = splits(ids)
|
||||
rows, kinds = [], Counter()
|
||||
for i in ids:
|
||||
e = EXP[i]
|
||||
code = sorted(COMPOSITE[i])[0] if i in COMPOSITE else pick_answer(i)
|
||||
if i in COMPOSITE:
|
||||
secs = section_compare(i)
|
||||
bad = [x for x in secs if x["판정"] not in ("같음(3% 안)", "섞음 · 할증 붙임(밑품 같음)")]
|
||||
cmp_ = {"판정": "조합 · " + ("같음(3% 안)" if not bad else "계수 바뀜"), "바뀜": [f"{x['로직']} {b}" for x in bad for b in x["바뀜"]], "직종": [], "변형": None, "절": secs}
|
||||
else:
|
||||
cmp_ = compare(e, code)
|
||||
ck = clause_kind(e, code)
|
||||
rates = [r for x in e["인력"].values() for r in x["율"]] + [r for m in e["장비"] for r in (m.get("율") or [])]
|
||||
mix = any(n in ("%", "섞음") and 0 < v < 1 for v, n in rates) and bool(e["장비"]) and bool(e["인력"])
|
||||
bonus = any(n not in ("%", "섞음") for v, n in rates)
|
||||
xm, xt = extra_work(e, code)
|
||||
bundle = i in COMPOSITE or bool(xm)
|
||||
tags = []
|
||||
if S.hm.tidy(e["이름"]) != TG[code]["tidy"]:
|
||||
tags.append("이름 다름")
|
||||
if ck != "조항 같음":
|
||||
tags.append(ck)
|
||||
if bundle:
|
||||
tags.append("묶음")
|
||||
if i in sp:
|
||||
tags.append("쪼갬")
|
||||
if mix:
|
||||
tags.append("섞음")
|
||||
if bonus:
|
||||
tags.append("할증 · 횟수 · 보정 붙임")
|
||||
v = cmp_["판정"]
|
||||
if v.endswith("계수 바뀜"):
|
||||
tags.append("계수 바뀜")
|
||||
if v == "직종 다름":
|
||||
tags.append("직종 다름")
|
||||
if v in ("단위 달라 크기 못 견줌",):
|
||||
tags.append("단위 다름")
|
||||
same = v in ("같음(3% 안)", "섞음 · 할증 붙임(밑품 같음)", "장비 시간 줄(기계경비 로직)")
|
||||
if same and not bundle and not mix and not bonus and i not in sp:
|
||||
tags.append("이름만" if ck == "조항 같음" else "인용 조항만" if ck != "출처 없음" else "출처 없음 · 품 같음")
|
||||
for t in tags:
|
||||
kinds[t] += 1
|
||||
rows.append({"id": i, "이름": e["이름"], "규격": e["규격"], "정답": code, "정답 이름": TG[code]["이름"], "조항": ck, "판정": v, "바뀜": cmp_["바뀜"], "직종": cmp_["직종"], "변형": cmp_["변형"], "더 있는 일": xm + xt, "꼴": tags, "지문 1위": res[i]["지문만"][:1], "1위 맞음": bool(res[i]["지문만"]) and res[i]["지문만"][0] in set(A.LAB[i]["정답"]), "줄수": A.BY[i]["줄수"]})
|
||||
print("바꿈 꼴(겹쳐 셈 · 고유 126)", dict(kinds.most_common()))
|
||||
verdicts = Counter(r["판정"] for r in rows)
|
||||
print("정답 로직 대 엑셀 품 판정", dict(verdicts))
|
||||
hit = [r for r in rows if r["1위 맞음"]]
|
||||
print("지문 1위 맞은 줄의 판정", dict(Counter(r["판정"] for r in hit)), "/", len(hit))
|
||||
# ── 숫자는 어느 책 — 정답 · 허용에 두 책이 다 있는 줄 ──
|
||||
nb = []
|
||||
for i in ids:
|
||||
e = EXP[i]
|
||||
cands = [k for k in dict.fromkeys(A.LAB[i]["정답"] + A.LAB[i]["허용"]) if k in BYCODE]
|
||||
books = {BYCODE[k]["책"] for k in cands}
|
||||
if len(books) < 2 or i in COMPOSITE:
|
||||
continue
|
||||
good = {}
|
||||
for k in cands:
|
||||
c = compare(e, k)
|
||||
if c["판정"] in ("같음(3% 안)", "섞음 · 할증 붙임(밑품 같음)"):
|
||||
good.setdefault(BYCODE[k]["책"], []).append(k)
|
||||
cited = sorted({b for b, _ in e["출처"]})
|
||||
nb.append({"id": i, "이름": e["이름"], "인용 책": cited, "숫자 맞는 책": sorted(good), "로직": good})
|
||||
tally = Counter()
|
||||
for x in nb:
|
||||
g, c = x["숫자 맞는 책"], x["인용 책"]
|
||||
if not g:
|
||||
tally["어느 책과도 3% 넘게 다름"] += 1
|
||||
elif len(g) == 2:
|
||||
tally["두 책 품 같음(숫자로 못 가름)"] += 1
|
||||
elif not c:
|
||||
tally[f"출처 없음 · 숫자 {g[0]}"] += 1
|
||||
elif g[0] in c:
|
||||
tally["숫자 = 인용 책"] += 1
|
||||
else:
|
||||
tally[f"인용 {'·'.join(c)} · 숫자 {g[0]}"] += 1
|
||||
print("두 책 후보 줄", len(nb), dict(tally))
|
||||
# ── 책 갈래 11 ──
|
||||
book = []
|
||||
for i in [k for k, v in S.NOW.items() if v[0] == "책 갈래(산림↔건설)"]:
|
||||
e = EXP[i]
|
||||
cands = list(dict.fromkeys(A.LAB[i]["정답"] + A.LAB[i]["허용"]))
|
||||
line = {"id": i, "이름": e["이름"], "엑셀 출처": e["출처"], "엑셀 품": {t: round(x["품"], 4) for t, x in e["인력"].items()}}
|
||||
for k in cands:
|
||||
c = compare(e, k)
|
||||
line[f"{BYCODE[k]['책']} {k}"] = {"판정": c["판정"], "품": {r[0]: r[3] for r in c["직종"]}, "변형": c["변형"]}
|
||||
book.append(line)
|
||||
for b in book:
|
||||
print(" 책", json.dumps(b, ensure_ascii=False, default=str)[:400])
|
||||
# ── 없음 줄 — 지문 1위 점수 나눔 ──
|
||||
none = [i for i in A.BY if not A.LAB[i]["정답"]]
|
||||
sc = {i: res[i]["1~5"][0]["점수"] if res[i]["1~5"] else 0 for i in A.BY}
|
||||
ok_sc = sorted(sc[i] for i in ids if res[i]["지문만"][:1] and res[i]["지문만"][0] in set(A.LAB[i]["정답"]))
|
||||
print("없음 줄 1위 점수", sorted((round(sc[i], 2), i) for i in none))
|
||||
print("맞은 1위 점수 아래 10", [round(x, 2) for x in ok_sc[:10]], "· 중앙", round(ok_sc[len(ok_sc) // 2], 2))
|
||||
for cut in (2.5, 3.0, 3.5):
|
||||
print(f" 문턱 {cut}: 없음 줄 숨김 {sum(1 for i in none if sc[i] < cut)}/{len(none)} · 맞은 1위 잃음 {sum(1 for x in ok_sc if x < cut)}/{len(ok_sc)}")
|
||||
out = {"조합": {i: combos[i] for i in multi}, "복합": comp_rows, "바꿈": rows, "꼴수": dict(kinds), "판정수": dict(verdicts), "책갈래": book, "두책": nb, "두책셈": dict(tally)}
|
||||
(HERE / "out_changes.json").write_text(json.dumps(out, ensure_ascii=False, indent=1, default=str), "utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
"""req226 C4 — 보고서용 표 · 예 뽑기(out_match · out_changes · 지문 파일 읽기만)."""
|
||||
import json
|
||||
import sys
|
||||
from fp_common import HERE
|
||||
|
||||
M = json.loads((HERE / "out_match.json").read_text(encoding="utf-8"))
|
||||
C = json.loads((HERE / "out_changes.json").read_text(encoding="utf-8"))
|
||||
TG = {x["로직"]: x for x in json.loads((HERE / "ours_fp.json").read_text(encoding="utf-8"))}
|
||||
part = sys.argv[1] if len(sys.argv) > 1 else "table"
|
||||
if part == "table":
|
||||
for r in M["표"]:
|
||||
a, n, s, y = r["전체"], r["북부청임도"], r["북부청사방"], r["동부청영월"]
|
||||
print(f"| {r['이름']} | {a['1위 엄격']} / {a['1위 느슨']} | {a['5안 엄격']} / {a['5안 느슨']} | {n['1위 엄격']} | {s['1위 엄격']} | {y['1위 엄격']} | {a['줄 1위 느슨']} | {a['없음인데 후보']} |")
|
||||
elif part == "combo":
|
||||
for i in sys.argv[2:]:
|
||||
print(i, [(c["절"], c["이름"][:18], c["1위"], TG[c["1위"]]["이름"][:22]) for c in C["조합"].get(i) or []])
|
||||
elif part == "rows":
|
||||
want = set(sys.argv[2:])
|
||||
for r in C["바꿈"]:
|
||||
if not want or r["id"] in want or r["판정"] in want or any(t in want for t in r["꼴"]):
|
||||
print(r["id"], r["이름"][:16], "|", r["규격"][:18], "→", r["정답"], r["정답 이름"][:18], "|", r["판정"], r["바뀜"], "| 조항", r["조항"], "| 꼴", r["꼴"], "| 더", r["더 있는 일"], "| 1위맞음" if r["1위 맞음"] else "")
|
||||
elif part == "twobook":
|
||||
for x in C["두책"]:
|
||||
print(x["id"], x["이름"][:16], "인용", x["인용 책"], "숫자", x["숫자 맞는 책"], x["로직"])
|
||||
@@ -0,0 +1,41 @@
|
||||
"""req226 C4 — 엑셀이 쓴 조건값(지금 M02 규격 읽기 `spec_inputs`)으로 정답 로직을 돌린 품 ↔ 엑셀 품.
|
||||
가장 닮은 변형(s4 판정)과 견줘 「규격 읽기만으로 같은 품이 나오나」를 셈. 읽기만."""
|
||||
import json
|
||||
from collections import Counter
|
||||
|
||||
import s2_ours_fp as O
|
||||
import s4_changes as C
|
||||
from fp_common import HERE, close
|
||||
|
||||
rows = json.loads((HERE / "out_changes.json").read_text(encoding="utf-8"))["바꿈"]
|
||||
cnt, ex = Counter(), []
|
||||
for r in rows:
|
||||
if r["판정"] not in ("같음(3% 안)", "섞음 · 할증 붙임(밑품 같음)"):
|
||||
continue
|
||||
i, code = r["id"], r["정답"]
|
||||
line = {k: C.A.BY[i].get(k) for k in ("이름", "규격", "단위")}
|
||||
row = O.WHOLE.logic(code)
|
||||
try:
|
||||
values, unread = O.hm.spec_inputs(line, O.hm._logic_specs(code))
|
||||
by_name = {}
|
||||
for h in row.get("호표") or []:
|
||||
by_name.setdefault(h.get("이름"), h)
|
||||
v = O.run_one(code, row, values, O.times_of(row), by_name)
|
||||
except Exception as e: # noqa: BLE001
|
||||
cnt["못 돎"] += 1
|
||||
continue
|
||||
E = C.EXP[i]
|
||||
L = v["L"]
|
||||
mix = v.get("mix") or {}
|
||||
if mix.get("인력비율") and 0 < mix["인력비율"] < 100:
|
||||
L = {t: q * 100 / mix["인력비율"] for t, q in L.items()}
|
||||
if not E["E"]:
|
||||
cnt["인력 없음(건넘)"] += 1
|
||||
continue
|
||||
ok = set(E["E"]) == {t for t, q in L.items() if q} and all(max(close(E["E"][t], L.get(t)), close(E["Eb"].get(t), L.get(t))) >= 0.99 for t in E["E"])
|
||||
cnt["규격 읽기로 같은 품" if ok else "규격 읽기로는 다른 품(다른 변형이 맞음)"] += 1
|
||||
if not ok and len(ex) < 8:
|
||||
ex.append((i, r["이름"], r["규격"], code, values, r["변형"]))
|
||||
print(dict(cnt))
|
||||
for x in ex:
|
||||
print(" ", x)
|
||||
@@ -0,0 +1,489 @@
|
||||
"""req226 C5 — 바깥 호표(일위대가) 나무 세우기(읽기만 · 저장소 안 고침).
|
||||
|
||||
호표마다 줄 → 갈래(단산 · 호표 참조 · 노임 · 자재 · 장비 · % · 별도계상 · 근거글 · 기타) +
|
||||
단산 블록 · STmate D 블록의 몫(절)별 벡터(노임 인 · 장비 시간 · 자재 수).
|
||||
꼴 셋 = 북부청 xls(지금 읽기 함수) · 거창 xlsx(지금 읽기 함수 + 블록 행 글 이어 붙임) ·
|
||||
STmate xlsx 다섯(새 흉내 읽기 — 줄 코드 = 14 열 「D00002_1」).
|
||||
|
||||
python c01_trees.py → trees.json · 표
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import operator
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225" / "C1"
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
|
||||
|
||||
DOCS = ROOT / "resources/knowledge/original/실무문서"
|
||||
NORTH = {
|
||||
"북부청임도": DOCS / "2026_실무일위대가 모음/2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls",
|
||||
"북부청사방": DOCS / "2026_실무일위대가 모음/2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls",
|
||||
}
|
||||
GEO = {"거창": DOCS / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx"}
|
||||
STM = {
|
||||
"봉화": DOCS / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx",
|
||||
"동부청영월": DOCS / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
|
||||
"울진대흥": DOCS
|
||||
/ "2024년 간선임도(기번3-울진.대흥)(공통)/2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx",
|
||||
"영덕계류": DOCS
|
||||
/ "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx",
|
||||
"울진소광": DOCS / "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
|
||||
}
|
||||
|
||||
# ── 장비 열쇠 ─────────────────────────────────────────────────────────
|
||||
EQ_TYPES = (
|
||||
("굴착기", ("굴착기", "굴삭기", "백호", "b/h", "backhoe", "back-hoe")),
|
||||
("덤프", ("덤프",)),
|
||||
("크레인", ("크레인",)),
|
||||
("브레이커", ("브레이커", "브레이카")),
|
||||
("롤러", ("롤러", "로울러")),
|
||||
("펌프차", ("펌프차", "펌프카", "콘크리트펌프")),
|
||||
("믹서", ("믹서",)),
|
||||
("불도저", ("불도저", "불도우저")),
|
||||
("살수", ("살수", "물탱크")),
|
||||
("콤팩터", ("콤팩터", "컴팩터", "플레이트")),
|
||||
("화물차", ("화물",)),
|
||||
("공기압축기", ("공기압축기", "컴프레서")),
|
||||
("천공기", ("천공", "드릴")),
|
||||
("로더", ("로더",)),
|
||||
("그레이더", ("그레이더",)),
|
||||
("트레일러", ("트레일러",)),
|
||||
)
|
||||
_SIZE = re.compile(r"(\d+(?:\.\d+)?)\s*(㎥|m3|톤|ton|t\b|kw|hp)", re.I)
|
||||
|
||||
|
||||
def eq_key(text: str) -> str | None:
|
||||
"""장비 이름 → 「굴착기|0.6」 꼴(크기 없으면 「굴착기|」) · 장비 아니면 None."""
|
||||
t = str(text or "").lower().replace("㎥", "㎥")
|
||||
kind = next((k for k, ws in EQ_TYPES if any(w in t for w in ws)), None)
|
||||
if kind is None:
|
||||
return None
|
||||
if "집게" in t:
|
||||
kind += "+집게"
|
||||
m = _SIZE.search(t.replace(",", ""))
|
||||
size = m.group(1).rstrip("0").rstrip(".") if m else ""
|
||||
return f"{kind}|{size}"
|
||||
|
||||
|
||||
def trade_of(name: str) -> str | None:
|
||||
"""노임 이름 → 우리 인력 이름(빈칸 · 옛이름 풀이) · 아니면 None."""
|
||||
flat = re.sub(r"\s+", "", str(name or ""))
|
||||
flat = re.sub(r"\(.*?\)", "", flat) if flat not in hb.trade_names() else flat
|
||||
return hb.trade_names().get(flat)
|
||||
|
||||
|
||||
# ── 식 풀이(품) — 첫 수(노임 · 장비 단가)를 1 로 두고 나머지 식만 셈 ───────────────────────
|
||||
_OPS = {ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Add: operator.add, ast.Sub: operator.sub}
|
||||
|
||||
|
||||
def _eval(node):
|
||||
if isinstance(node, ast.Expression):
|
||||
return _eval(node.body)
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
||||
return float(node.value)
|
||||
if isinstance(node, ast.BinOp) and type(node.op) in _OPS:
|
||||
return _OPS[type(node.op)](_eval(node.left), _eval(node.right))
|
||||
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
|
||||
return -_eval(node.operand)
|
||||
raise ValueError("식 아님")
|
||||
|
||||
|
||||
def qty_of(expr: str) -> float | None:
|
||||
"""「273,540 × 3인 ÷ 55㎥ × 2」 → 3/55×2 · 「59,020 ÷ 23.1 × 90%」 → 1/23.1×0.9."""
|
||||
s = re.sub(r"\([^()0-9]*\)", "", str(expr)) # (보정계수) 같은 글 괄호
|
||||
s = s.replace(",", "")
|
||||
s = re.sub(r"(\d+(?:\.\d+)?)\s*%", r"(\1/100)", s)
|
||||
s = s.replace("×", "*").replace("÷", "/").replace("X", "*").replace("x", "*")
|
||||
s = re.sub(r"/\s*(?=[^\d(\s.])", " ", s) # 「인/㎥」 의 / 는 단위
|
||||
s = re.sub(r"[^\d.+\-*/() ]", " ", s)
|
||||
s = re.sub(r"(?<=\d)\.(?!\d)", "", s) # 「4.」 → 4
|
||||
s = re.sub(r"\(\s*\)", " ", s).strip()
|
||||
m = re.match(r"\s*\d+(?:\.\d+)?", s)
|
||||
if not m:
|
||||
return None
|
||||
s = "1" + s[m.end() :]
|
||||
s = re.sub(r"(?<=[\d)])\s+(?=[\d(])", "*", s) # 빈칸으로 붙은 수는 곱
|
||||
try:
|
||||
v = _eval(ast.parse(s, mode="eval"))
|
||||
except (SyntaxError, ValueError, ZeroDivisionError):
|
||||
return None
|
||||
return v if 0 < v < 1e4 else None
|
||||
|
||||
|
||||
_TRADE = re.compile(r"^[\s*·\-○◇]*([가-힣][가-힣\s]*?)\s*:\s*([\d,]+(?:\.\d+)?)\s*(.*?)(?:=\s*([\d,]+(?:\.\d+)?))?\s*$")
|
||||
_SECTION = re.compile(r"^\s*(\d+)\s*\.\s*(.*)$")
|
||||
_EQ_COST = re.compile(r"^\s*노\s*무\s*비\s*:\s*([\d,]+(?:\.\d+)?)\s*(.*?)(?:=\s*[\d,.]+)?\s*$")
|
||||
_SKIP_SEC = re.compile(r"^(합\s*계|소\s*계|계)")
|
||||
_SUBLABEL = re.compile(r"^\s*[가-하]\s*\.\s*(.*)$")
|
||||
_COSTLINE = re.compile(r"^\s*(노\s*무\s*비|재\s*료\s*비|경\s*비)\s*:")
|
||||
|
||||
|
||||
def sections(texts: list[str]) -> list[dict]:
|
||||
"""블록 글 → 몫(맨 윗 번호 절)마다 {제목, 노임, 장비} — 노임 = 「직종 : 노임 × 식 = 금액」 ·
|
||||
장비 = 장비 이름 줄 뒤 「노무비 : 단가 ÷ Q × 몫」(시간 = 식)."""
|
||||
out, cur, eq, parent = [], None, None, ""
|
||||
for raw in texts:
|
||||
line = str(raw).rstrip()
|
||||
sec = _SECTION.match(line)
|
||||
if sec and not re.match(r"^\s*\d+\.\d", line):
|
||||
title = sec.group(2).strip()
|
||||
if _SKIP_SEC.match(title.replace(" ", "")):
|
||||
cur = None
|
||||
continue
|
||||
parent = title
|
||||
cur = {"제목": title, "절": title, "노임": {}, "장비": {}, "글": []}
|
||||
out.append(cur)
|
||||
eq = eq_key(title)
|
||||
continue
|
||||
sub = _SUBLABEL.match(line) if cur is not None else None
|
||||
if sub and not _SKIP_SEC.match(sub.group(1).replace(" ", "")):
|
||||
cur = {"제목": f"{parent} · {sub.group(1).strip()}", "절": parent, "노임": {}, "장비": {}, "글": []}
|
||||
out.append(cur)
|
||||
if cur is None:
|
||||
if out or not line.strip():
|
||||
continue
|
||||
cur = {"제목": "", "절": "", "노임": {}, "장비": {}, "글": []}
|
||||
out.append(cur)
|
||||
cur["글"].append(line.strip())
|
||||
flat = line.replace(" ", "")
|
||||
if re.search(r"(소계|합계|인부소계)", flat):
|
||||
eq = None
|
||||
continue
|
||||
sub = _SUBLABEL.match(line)
|
||||
if sub:
|
||||
eq = eq_key(sub.group(1))
|
||||
elif not _COSTLINE.match(line) and (k := eq_key(line)):
|
||||
eq = k
|
||||
m = _EQ_COST.match(line)
|
||||
if m and eq:
|
||||
q = qty_of(f"{m.group(1)} {m.group(2)}")
|
||||
if q and q < 50:
|
||||
cur["장비"][eq] = cur["장비"].get(eq, 0) + q
|
||||
continue
|
||||
t = _TRADE.match(line)
|
||||
if t:
|
||||
trade = trade_of(t.group(1))
|
||||
if trade:
|
||||
q = qty_of(f"{t.group(2)} {t.group(3)}")
|
||||
if q is None and t.group(4): # 식을 못 풀면 금액 ÷ 노임(품 되찾기만)
|
||||
q = float(t.group(4).replace(",", "")) / float(t.group(2).replace(",", ""))
|
||||
if q:
|
||||
cur["노임"][trade] = cur["노임"].get(trade, 0) + q
|
||||
return [s for s in out if s["노임"] or s["장비"]]
|
||||
|
||||
|
||||
# ── 북부청 · 거창 — 지금 읽기 함수 ─────────────────────────────────────────
|
||||
def _row_text(row: list, start: int = 1) -> str:
|
||||
cells = []
|
||||
for v in row[start:140]:
|
||||
if v is None or v == "":
|
||||
continue
|
||||
if isinstance(v, float) and v == int(v):
|
||||
v = int(v)
|
||||
cells.append(str(v).strip())
|
||||
return " ".join(cells)
|
||||
|
||||
|
||||
def _sub(line: dict) -> str:
|
||||
kind = line["갈래"]
|
||||
if kind == "단가산출":
|
||||
return "단산"
|
||||
if kind == "호표":
|
||||
return "호표"
|
||||
if kind == "기계":
|
||||
return "장비"
|
||||
if kind in ("%", "별도계상", "자재"):
|
||||
return kind
|
||||
if str(line.get("단위") or "").strip() == "인" and trade_of(line["이름"]):
|
||||
return "노임"
|
||||
return "기타"
|
||||
|
||||
|
||||
def _ids_north() -> dict:
|
||||
items = json.loads((C1 / "out_items.json").read_text(encoding="utf-8"))
|
||||
out = {}
|
||||
for r in items:
|
||||
ref = r.get("참조") or {}
|
||||
out[(r["파일"], r["이름"], r["규격"], r["단위"], ref.get("번") if ref.get("종류") == "단산" else None)] = r["id"]
|
||||
return out
|
||||
|
||||
|
||||
def read_excel(tag: str, path: Path, ids: dict, join_rows: bool) -> dict:
|
||||
data = path.read_bytes()
|
||||
book = hx.read_book(path.name, data)
|
||||
calc = hx._sheets(path.name, data).get(hb.SHEET) or []
|
||||
start, col, _ = hb._columns(calc)
|
||||
heads = sorted(b["행"][0] for b in book["단산"])
|
||||
blocks = {}
|
||||
for b in book["단산"]:
|
||||
a = b["행"][0]
|
||||
end = next((h for h in heads if h > a), len(calc) + 1) - 1
|
||||
texts = []
|
||||
for at in range(a, end + 1):
|
||||
row = calc[at - 1]
|
||||
text = _row_text(row) if join_rows else (row[col] if col < len(row) else "")
|
||||
if isinstance(text, str) and text.strip():
|
||||
texts.append(text.rstrip())
|
||||
joined = " ".join(texts)
|
||||
trades = []
|
||||
for t in texts:
|
||||
m = _TRADE.match(t)
|
||||
if m and (nm := trade_of(m.group(1))) and nm not in trades:
|
||||
trades.append(nm)
|
||||
blocks[b["번"]] = {
|
||||
"이름": b["이름"],
|
||||
"단위": b["단위"],
|
||||
"출처": hb.sources(joined),
|
||||
"직종": trades,
|
||||
"글": texts[:60],
|
||||
"몫": sections(texts[1:]),
|
||||
}
|
||||
hopyos = []
|
||||
for h in book["호표"]:
|
||||
lines = []
|
||||
for one in h["줄"]:
|
||||
ref = one.get("참조") or {}
|
||||
kind = _sub(one)
|
||||
rec = {k: one.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "참조")}
|
||||
rec["갈래"] = kind
|
||||
key = (tag, one["이름"], one["규격"], one["단위"], ref.get("번") if ref.get("종류") == "단산" else None)
|
||||
if key in ids:
|
||||
rec["id"] = ids[key]
|
||||
if kind == "단산" and ref.get("번") in blocks:
|
||||
b = blocks[ref["번"]]
|
||||
rec["근거"] = one.get("근거") if not join_rows else {"출처": b["출처"], "직종": b["직종"]}
|
||||
rec["몫"] = b["몫"]
|
||||
if kind == "노임":
|
||||
rec["노임"] = {trade_of(one["이름"]): one["수량"]}
|
||||
if kind == "장비":
|
||||
rec["장비"] = {eq_key(f"{one['이름']} {one['규격']}"): one["수량"]}
|
||||
lines.append(rec)
|
||||
hopyos.append({"번": h["번"], "이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "줄": lines})
|
||||
return {"파일": tag, "꼴": "거창" if join_rows else "북부청", "호표": hopyos, "단산": len(blocks)}
|
||||
|
||||
|
||||
# ── STmate ───────────────────────────────────────────────────────────
|
||||
_HOPYO = re.compile(r"^\s*제\s*(\d+)\s*호표")
|
||||
_RES = re.compile(r"^([LMXS]\d{5})([LMS])$")
|
||||
|
||||
|
||||
def _num(v):
|
||||
try:
|
||||
return float(str(v).replace(",", ""))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _code(row: list) -> str:
|
||||
raw = str(row[14] if len(row) > 14 and row[14] is not None else "").strip()
|
||||
return raw.rsplit("_", 1)[0] if "_" in raw else raw
|
||||
|
||||
|
||||
def _ids_y() -> dict:
|
||||
y = json.loads((C1 / "out_stmate.json").read_text(encoding="utf-8"))
|
||||
return {(r["이름"], r["규격"], r["단위"], r["코드"]): r["id"] for r in y}
|
||||
|
||||
|
||||
def _d_blocks(rows: list) -> dict:
|
||||
"""단가산출근거 → {D코드: {이름, 단위, 출처, 직종, 글, 몫}} — 자원 줄 = 0 열 「L00002L」 · 7 열 QTY ·
|
||||
23 열 이름."""
|
||||
out, cur, texts = {}, None, []
|
||||
|
||||
def close():
|
||||
if cur is not None:
|
||||
cur["글"] = texts[:60]
|
||||
joined = " ".join(texts)
|
||||
cur["출처"] = hb.sources(joined)
|
||||
secs = sections_stm(cur.pop("_res"), texts)
|
||||
cur["몫"] = secs
|
||||
cur["직종"] = sorted({t for s in secs for t in s["노임"]})
|
||||
|
||||
for r in rows:
|
||||
c0 = str(r[0] or "").strip()
|
||||
c1 = str(r[1] or "").strip() if len(r) > 1 else ""
|
||||
meta = re.search(r"BDCOD\|(D\d{5})", str(r[6] if len(r) > 6 and r[6] else ""))
|
||||
code = meta.group(1) if meta else None
|
||||
if code:
|
||||
close()
|
||||
cur, texts = {"코드": code, "_res": []}, []
|
||||
out[code] = cur
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
if c0.startswith("총") and "계" in c0:
|
||||
continue
|
||||
m = _RES.match(c0) or re.search(r"&([LMXS]\d{5})([LMS])&", str(r[6] if len(r) > 6 and r[6] else ""))
|
||||
if m:
|
||||
full = str(r[23] if len(r) > 23 and r[23] else "")
|
||||
name = full.split("/")[0].strip()
|
||||
cur["_res"].append((m.group(1), m.group(2), _num(r[7]) if len(r) > 7 else None, name, len(texts), full))
|
||||
continue
|
||||
if c1 and not c1.startswith("소계") and c1 != "계":
|
||||
if "이름" not in cur:
|
||||
cur["이름"] = c1
|
||||
texts.append(c1)
|
||||
close()
|
||||
return out
|
||||
|
||||
|
||||
def sections_stm(res: list, texts: list[str]) -> list[dict]:
|
||||
"""STmate 자원 줄 → 몫별 벡터(맨 윗 번호 절 · 없으면 한 몫)."""
|
||||
marks = []
|
||||
for at, t in enumerate(texts):
|
||||
sec = _SECTION.match(t)
|
||||
if sec and not re.match(r"^\s*\d+\.\d", t) and not _SKIP_SEC.match(sec.group(2).replace(" ", "")):
|
||||
marks.append((at, sec.group(2).strip()))
|
||||
def which(pos):
|
||||
got = [m for m in marks if m[0] <= pos]
|
||||
return got[-1][1] if got else ""
|
||||
secs: dict[str, dict] = {}
|
||||
for code, part, q, name, pos, full in res:
|
||||
if q is None:
|
||||
continue
|
||||
title = which(pos)
|
||||
s = secs.setdefault(title, {"제목": title, "노임": {}, "장비": {}, "자재": {}})
|
||||
if code[0] == "L" and part == "L":
|
||||
trade = trade_of(name)
|
||||
if trade:
|
||||
s["노임"][trade] = s["노임"].get(trade, 0) + q
|
||||
elif code[0] == "X" and part == "L":
|
||||
k = eq_key(full) or f"?{code}"
|
||||
s["장비"][k] = s["장비"].get(k, 0) + q
|
||||
elif code[0] == "M":
|
||||
s["자재"][name or code] = s["자재"].get(name or code, 0) + q
|
||||
return [s for s in secs.values() if s["노임"] or s["장비"]]
|
||||
|
||||
|
||||
def read_stmate(tag: str, path: Path, ids_y: dict) -> dict:
|
||||
import openpyxl
|
||||
|
||||
book = openpyxl.load_workbook(path, read_only=True, data_only=True)
|
||||
rows = [list(r) for r in book["일위대가표"].iter_rows(values_only=True)]
|
||||
calc = [list(r) for r in book["단가산출근거"].iter_rows(values_only=True)]
|
||||
book.close()
|
||||
blocks = _d_blocks(calc)
|
||||
hopyos, cur, want_head = [], None, False
|
||||
for r in rows:
|
||||
head = str(r[0] or "")
|
||||
hit = _HOPYO.match(head)
|
||||
if hit:
|
||||
cur = {"번": int(hit.group(1)), "줄": []}
|
||||
hopyos.append(cur)
|
||||
want_head = True
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
if want_head:
|
||||
cur.update(이름=str(r[0] or "").strip(), 규격=str(r[1] or "").strip(), 단위=str(r[3] or "").strip(), 코드=_code(r))
|
||||
want_head = False
|
||||
continue
|
||||
name = str(r[0] or "").strip()
|
||||
if not name or name.replace(" ", "").startswith("합계"):
|
||||
continue
|
||||
code = _code(r)
|
||||
if code[:1] in ("T", "C") and not code[1:2].isalpha():
|
||||
continue # 계 · 계약단가 · 소계 줄
|
||||
note = str(r[12] or "").strip() if len(r) > 12 else ""
|
||||
spec = str(r[1] or "").strip()
|
||||
price = {"계": _num(r[4]), "노무비": _num(r[6]), "재료비": _num(r[8]), "경비": _num(r[10])}
|
||||
rec = {"이름": name, "규격": spec, "수량": _num(r[2]), "단위": str(r[3] or "").strip(), "단가": price, "코드": code}
|
||||
k = code[:1]
|
||||
if not code:
|
||||
rec["갈래"] = "근거글"
|
||||
elif code.startswith("PR") or rec["단위"] == "%":
|
||||
rec["갈래"] = "%"
|
||||
elif k == "B":
|
||||
rec["갈래"], rec["참조"] = "호표", {"종류": "일위", "코드": code}
|
||||
elif k == "D":
|
||||
rec["갈래"], rec["참조"] = "단산", {"종류": "단산", "코드": code}
|
||||
b = blocks.get(code)
|
||||
if b:
|
||||
rec["근거"] = {"출처": b["출처"], "직종": b["직종"]}
|
||||
rec["몫"] = b["몫"]
|
||||
rec["단산글"] = b["글"]
|
||||
elif k == "L":
|
||||
rec["갈래"], rec["노임"] = "노임", {trade_of(name) or name: rec["수량"]}
|
||||
elif k == "X":
|
||||
rec["갈래"], rec["장비"] = "장비", {eq_key(f"{name} {spec}") or f"?{code}": rec["수량"]}
|
||||
elif k == "M":
|
||||
apart = note.startswith("별산") or spec == "별도계상" or not any(price.values())
|
||||
rec["갈래"] = "별도계상" if apart else "자재"
|
||||
elif k in "SW":
|
||||
rec["갈래"] = "별도계상"
|
||||
else:
|
||||
rec["갈래"] = "기타"
|
||||
if tag == "동부청영월" and k in "DX":
|
||||
yk = (name, spec, rec["단위"], str(r[12] or "").strip())
|
||||
if yk in ids_y:
|
||||
rec["id"] = ids_y[yk]
|
||||
cur["줄"].append(rec)
|
||||
by_code = {h.get("코드"): h["번"] for h in hopyos}
|
||||
for h in hopyos:
|
||||
for one in h["줄"]:
|
||||
if one["갈래"] == "호표":
|
||||
one["참조"]["번"] = by_code.get(one["코드"])
|
||||
return {"파일": tag, "꼴": "STmate", "호표": hopyos, "단산": len(blocks)}
|
||||
|
||||
|
||||
KINDS = ("단산", "호표", "노임", "장비", "자재", "%", "별도계상", "근거글", "기타")
|
||||
|
||||
|
||||
def depth(f: dict) -> int:
|
||||
by = {h["번"]: h for h in f["호표"]}
|
||||
|
||||
def d(no, seen=()):
|
||||
h = by.get(no)
|
||||
if not h or no in seen:
|
||||
return 0
|
||||
kids = [one["참조"].get("번") for one in h["줄"] if one["갈래"] == "호표" and one.get("참조")]
|
||||
return 1 + max((d(k, (*seen, no)) for k in kids if k), default=0)
|
||||
|
||||
return max((d(h["번"]) for h in f["호표"]), default=0)
|
||||
|
||||
|
||||
def main():
|
||||
ids_n, ids_y = _ids_north(), _ids_y()
|
||||
files = [read_excel(t, p, ids_n, False) for t, p in NORTH.items()]
|
||||
files += [read_excel(t, p, {}, True) for t, p in GEO.items()]
|
||||
files += [read_stmate(t, p, ids_y) for t, p in STM.items()]
|
||||
(HERE / "trees.json").write_text(json.dumps(files, ensure_ascii=False, indent=1, default=str), "utf-8")
|
||||
print("| 파일 | 꼴 | 호표 | 줄 | " + " | ".join(KINDS) + " | 직접 줄 호표 | 참조만 호표 | 단산 블록 | 몫 벡터 있는 단산 줄 | 깊이 |")
|
||||
tot = {}
|
||||
for f in files:
|
||||
cnt = {k: 0 for k in KINDS}
|
||||
direct = refs_only = with_vec = dline = 0
|
||||
for h in f["호표"]:
|
||||
kinds = [one["갈래"] for one in h["줄"]]
|
||||
for k in kinds:
|
||||
cnt[k] += 1
|
||||
if any(k in ("노임", "장비") for k in kinds):
|
||||
direct += 1
|
||||
elif kinds and all(k in ("단산", "호표", "자재", "별도계상", "%", "근거글") for k in kinds):
|
||||
refs_only += 1
|
||||
for one in h["줄"]:
|
||||
if one["갈래"] == "단산":
|
||||
dline += 1
|
||||
with_vec += bool(one.get("몫"))
|
||||
n = sum(cnt.values())
|
||||
row = [f["파일"], f["꼴"], len(f["호표"]), n, *cnt.values(), direct, refs_only, f["단산"], f"{with_vec}/{dline}", depth(f)]
|
||||
for k, v in zip(["호표", "줄", *KINDS, "직접", "참조만", "단산블록"], row[2:13] + [direct, refs_only, f["단산"]]):
|
||||
tot[k] = tot.get(k, 0) + v
|
||||
print("| " + " | ".join(map(str, row)) + " |")
|
||||
print("합", tot)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,102 @@
|
||||
"""req226 C5 — 우리 로직 단위당 벡터(노임 인 · 장비 시간) — 우리 계산 엔진(`mf.run`)으로 냄(읽기만).
|
||||
|
||||
auto = 자동값 한 벌(모든 돈 로직) · variants(code) = 고르기 입력 갈래(지역 뺌 · 곱이 cap 넘으면 하나씩 바꿈).
|
||||
python c02_ourvec.py → ourvec_auto.json(돈 로직 1,438 자동값 벡터)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
from c01_trees import ROOT, eq_key, trade_of # noqa: E402,F401
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
|
||||
mf = m01.cm.mf
|
||||
WHOLE = m01.loaded()[1]
|
||||
SKIP_INPUTS = {"지역"}
|
||||
|
||||
|
||||
def vec_of(lines: list[dict]) -> dict:
|
||||
"""엔진 줄 → {노임: {직종: 인}, 장비: {열쇠: 시간}}."""
|
||||
out = {"노임": {}, "장비": {}}
|
||||
for one in lines:
|
||||
unit = str(one.get("단위") or "")
|
||||
try:
|
||||
q = float(one.get("수량") or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if q <= 0:
|
||||
continue
|
||||
name = str(one.get("이름") or "")
|
||||
if unit == "인":
|
||||
t = trade_of(name) or name
|
||||
out["노임"][t] = out["노임"].get(t, 0) + q
|
||||
elif unit in ("시간", "hr", "HR", "h"):
|
||||
k = eq_key(name) or f"?{name}"
|
||||
out["장비"][k] = out["장비"].get(k, 0) + q
|
||||
return out
|
||||
|
||||
|
||||
def run(code: str, given: dict | None = None) -> dict | None:
|
||||
logic = WHOLE.logic(code)
|
||||
auto = {k: v for k, v in m01.mau.values(WHOLE, logic).items() if v is not None}
|
||||
try:
|
||||
got = mf.run(WHOLE, code, {**auto, **(given or {})})
|
||||
except Exception: # noqa: BLE001 — 못 도는 갈래는 뺌
|
||||
return None
|
||||
return vec_of(got.get("줄") or [])
|
||||
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def variants(code: str, cap: int = 48) -> tuple:
|
||||
"""((입력 글, 벡터) …) — 자동값 + 고르기 갈래."""
|
||||
logic = WHOLE.logic(code)
|
||||
picks = [
|
||||
(str(s["이름"]), list(s["고르기"]))
|
||||
for s in logic.get("입력") or []
|
||||
if s.get("고르기") and str(s.get("이름")) not in SKIP_INPUTS
|
||||
]
|
||||
combos = [{}]
|
||||
total = 1
|
||||
for _, opts in picks:
|
||||
total *= len(opts)
|
||||
if picks and total <= cap:
|
||||
combos = [dict(zip([n for n, _ in picks], vals)) for vals in itertools.product(*[o for _, o in picks])]
|
||||
elif picks:
|
||||
combos += [{n: v} for n, opts in picks for v in opts]
|
||||
out, seen = [], set()
|
||||
for given in combos:
|
||||
v = run(code, given)
|
||||
if v is None or not (v["노임"] or v["장비"]):
|
||||
continue
|
||||
key = json.dumps(v, sort_keys=True, ensure_ascii=False)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append((json.dumps(given, ensure_ascii=False, default=str), v))
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def main():
|
||||
got = hm.targets()
|
||||
out = {}
|
||||
for one in got["로직"]:
|
||||
v = run(one["로직"])
|
||||
if v is not None:
|
||||
out[one["로직"]] = v
|
||||
(HERE / "ourvec_auto.json").write_text(json.dumps(out, ensure_ascii=False), "utf-8")
|
||||
trades = sum(1 for v in out.values() if v["노임"])
|
||||
eqs = sum(1 for v in out.values() if v["장비"])
|
||||
print("로직", len(got["로직"]), "벡터", len(out), "노임 있음", trades, "장비 있음", eqs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
"""갈래 벡터를 조각으로 미리 셈 → ourvec_var.json 에 더함 · python c02b_varcache.py <시작> <끝> [초 한도]."""
|
||||
import json, sys, time
|
||||
import c03_vec as V
|
||||
import c01_trees # noqa: F401 — 저장소 경로
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
|
||||
a, b = int(sys.argv[1]), int(sys.argv[2])
|
||||
limit = float(sys.argv[3]) if len(sys.argv) > 3 else 95
|
||||
t0 = time.time()
|
||||
codes = [x["로직"] for x in hm.targets()["로직"]][a:b]
|
||||
done = 0
|
||||
for c in codes:
|
||||
if c in V.CACHE:
|
||||
done += 1
|
||||
continue
|
||||
V.variants(c)
|
||||
done += 1
|
||||
if time.time() - t0 > limit:
|
||||
break
|
||||
V.save_cache()
|
||||
print("done", a, a + done, "of", b, "cache", len(V.CACHE), "sec", round(time.time() - t0, 1))
|
||||
@@ -0,0 +1,107 @@
|
||||
"""req226 C5 — 벡터 맞춤 부품(몫 벡터 ↔ 우리 로직 단위당 벡터) · 갈래 벡터 디스크 캐시.
|
||||
|
||||
점수 = 차원(직종 · 장비 종류)마다 가까움(작은/큰) 가중 평균 · 보통인부 무게 0.5 ·
|
||||
장비는 있음 0.5 + 시간 가까움 0.5(작성자가 효율 · 사이클을 바꿔도 종류는 남음) · 크기 다르면 × 0.8 ·
|
||||
몫(10% · 90% 같은 나눔)은 x ∈ {0.1 … 0.9} 로 늘여 봄(× 0.95). 금액 안 씀.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
AUTO = json.loads((HERE / "ourvec_auto.json").read_text(encoding="utf-8"))
|
||||
CACHE_FILE = HERE / "ourvec_var.json"
|
||||
try:
|
||||
CACHE = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
CACHE = {}
|
||||
COMMON = "보통인부"
|
||||
SHARES = (0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9)
|
||||
|
||||
|
||||
def variants(code: str) -> list:
|
||||
"""[(입력 글, 벡터) …] — 디스크 캐시 · 없으면 엔진으로 셈."""
|
||||
if code not in CACHE:
|
||||
import c02_ourvec as ov
|
||||
|
||||
CACHE[code] = [list(x) for x in ov.variants(code)]
|
||||
return CACHE[code] or ([["{}", AUTO[code]]] if code in AUTO else [])
|
||||
|
||||
|
||||
def save_cache():
|
||||
CACHE_FILE.write_text(json.dumps(CACHE, ensure_ascii=False), "utf-8")
|
||||
|
||||
|
||||
def _eq_split(k: str):
|
||||
kind, _, size = k.partition("|")
|
||||
return kind, size
|
||||
|
||||
|
||||
def sim(E: dict, O: dict, x: float = 1.0) -> float:
|
||||
"""E · O = {노임: {..}, 장비: {..}} — 0..1."""
|
||||
num = den = 0.0
|
||||
et, ot = E.get("노임") or {}, O.get("노임") or {}
|
||||
for t in set(et) | set(ot):
|
||||
w = 0.5 if t == COMMON else 1.0
|
||||
a, b = et.get(t, 0.0), ot.get(t, 0.0) * x
|
||||
den += w
|
||||
if a > 0 and b > 0:
|
||||
num += w * min(a, b) / max(a, b)
|
||||
ee, oe = E.get("장비") or {}, O.get("장비") or {}
|
||||
used = set()
|
||||
for k, a in ee.items():
|
||||
kind, size = _eq_split(k)
|
||||
best = 0.0
|
||||
for k2, b in oe.items():
|
||||
kind2, size2 = _eq_split(k2)
|
||||
if kind2 != kind and kind2.split("+")[0] != kind.split("+")[0]:
|
||||
continue
|
||||
f = 1.0 if (not size or not size2 or size == size2) else 0.8
|
||||
f *= 1.0 if kind2 == kind else 0.8 # 굴착기 ↔ 굴착기+집게(같은 기종 · 덧장치만 다름)
|
||||
b *= x
|
||||
s = f * (0.5 + 0.5 * (min(a, b) / max(a, b) if a > 0 and b > 0 else 0))
|
||||
if s > best:
|
||||
best, pick = s, k2
|
||||
den += 1.0
|
||||
num += best
|
||||
if best:
|
||||
used.add(pick)
|
||||
den += sum(1.0 for k in oe if k not in used)
|
||||
return num / den if den else 0.0
|
||||
|
||||
|
||||
def fit(E: dict, code: str) -> tuple[float, str, float]:
|
||||
"""(점수, 입력 글, 몫 x) — 갈래마다 x = 1 과 몫 x 중 가장 좋은 것."""
|
||||
best = (0.0, "{}", 1.0)
|
||||
for given, O in variants(code):
|
||||
s = sim(E, O)
|
||||
if s > best[0]:
|
||||
best = (s, given, 1.0)
|
||||
for x in SHARES:
|
||||
s2 = 0.95 * sim(E, O, x)
|
||||
if s2 > best[0]:
|
||||
best = (s2, given, x)
|
||||
return best
|
||||
|
||||
|
||||
def dims(E: dict) -> set:
|
||||
out = {t for t in (E.get("노임") or {}) if t != COMMON}
|
||||
out |= {_eq_split(k)[0] for k in (E.get("장비") or {})}
|
||||
return out
|
||||
|
||||
|
||||
def index() -> dict:
|
||||
"""{차원: {로직}} — 보통인부 뺀 직종 · 장비 종류(자동값 벡터 기준)."""
|
||||
out: dict[str, set] = {}
|
||||
for code, v in AUTO.items():
|
||||
for d in dims(v):
|
||||
out.setdefault(d, set()).add(code)
|
||||
return out
|
||||
|
||||
|
||||
IDX = index()
|
||||
@@ -0,0 +1,304 @@
|
||||
"""req226 C5 — 닫힌 세계 풀이 (가): 지금 맞춤 후보 다섯(그대로 부름)에 호표 묶음 조건을 걸어 다시 고름.
|
||||
|
||||
조건 = ① 같은 책(줄 근거 책 · 없으면 호표 안 근거 책 다수) ② 같은 조건(토질 — 호표 · 줄 이름) ·
|
||||
③ 우리 조합(일위대가조합.json 37) 통째 같음 · 안에 다 듦 · ④ 아래에서 위로(호표 참조 = 그 호표 풀이) —
|
||||
④는 줄 지표엔 안 듦(참조 줄은 로직 줄 아님) · 조합 지표(c05)에서 씀.
|
||||
지표 = 앞 조사(C1 · C3)와 같은 정답 · 같은 셈(고유 줄 126 · 씨앗 켬 · 기억 없음).
|
||||
|
||||
python c04_closed.py → 표 · closed.json(줄마다 결과 — c05 가 씀)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225" / "C1"
|
||||
C3 = HERE.parent.parent / "req225" / "C3"
|
||||
for p in (HERE, C1, C3):
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import s10_all as S # noqa: E402
|
||||
from s04_eval import clause_prefix, hb, hm, patched # noqa: E402
|
||||
from s10_all import candidates_nofold, no_seeds # noqa: E402
|
||||
from s16_guard import typo_guard # noqa: E402
|
||||
|
||||
from c01_trees import ROOT # noqa: E402
|
||||
|
||||
TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8"))
|
||||
LABELED = ("북부청임도", "북부청사방", "동부청영월")
|
||||
COMBOS = [
|
||||
(r["키"], r["이름"], [x["로직"] for x in r["담은로직"]])
|
||||
for r in json.loads((ROOT / "resources/master_data/일위대가조합.json").read_text(encoding="utf-8"))["줄"]
|
||||
if r["키"] != "UA000001"
|
||||
]
|
||||
|
||||
|
||||
def hopyo_members() -> dict:
|
||||
"""{(파일, 호표 번): [id …](차례 · 겹침 뺌)}."""
|
||||
out = {}
|
||||
for f in TREES:
|
||||
if f["파일"] not in LABELED:
|
||||
continue
|
||||
for h in f["호표"]:
|
||||
ids = list(dict.fromkeys(one["id"] for one in h["줄"] if one.get("id")))
|
||||
out[(f["파일"], h["번"])] = ids
|
||||
return out
|
||||
|
||||
|
||||
MEMBERS = hopyo_members()
|
||||
HOPYO_OF = {}
|
||||
for key, ids in MEMBERS.items():
|
||||
for i in ids:
|
||||
HOPYO_OF.setdefault(i, []).append(key)
|
||||
HEADS = {(f["파일"], h["번"]): f"{h['이름']} {h['규격']}" for f in TREES for h in f["호표"]}
|
||||
|
||||
|
||||
def line_of(r: dict) -> dict:
|
||||
return {k: r.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")}
|
||||
|
||||
|
||||
def book(code: str) -> str | None:
|
||||
one = hb.ours().get(code)
|
||||
return one["책"] if one else None
|
||||
|
||||
|
||||
def own_books(r: dict) -> set:
|
||||
return {b for b, _ in ((r.get("근거") or {}).get("출처") or [])}
|
||||
|
||||
|
||||
def pools() -> dict:
|
||||
"""줄마다 지금 합친 후보(근거 + 이름 · 다섯 자르기 전 · 15)."""
|
||||
got = hm.targets()
|
||||
out = {}
|
||||
for r in S.ALL:
|
||||
line = line_of(r)
|
||||
basis = hb.candidates(line, got)
|
||||
named = hm.candidates(line, "로직", got, None)
|
||||
out[r["id"]] = [c["로직"] for c in hb.merge(basis, named) if c.get("로직")][:15]
|
||||
return out
|
||||
|
||||
|
||||
# ── 같은 일 · 조건 ───────────────────────────────────────────────────
|
||||
_NAMES = {}
|
||||
|
||||
|
||||
def name(code: str) -> str:
|
||||
if not _NAMES:
|
||||
_NAMES.update({x["로직"]: x["이름"] for x in hm.targets()["로직"]})
|
||||
return _NAMES.get(code, "")
|
||||
|
||||
|
||||
def same_work(a: str, b: str) -> bool:
|
||||
"""두 로직이 같은 일(책만 다름) — 정리 이름 글자 두 개 묶음 겹침 0.5 이상 또는 뜻 낱말 겹침."""
|
||||
ta, tb = hm.tidy(name(a)), hm.tidy(name(b))
|
||||
ga, gb = hm._grams(ta), hm._grams(tb)
|
||||
dice = 2 * len(ga & gb) / (len(ga) + len(gb)) if ga and gb else 0
|
||||
return dice >= 0.5 or hm.near(hm.words(name(a)), hm.words(name(b))) and dice >= 0.34
|
||||
|
||||
|
||||
SOIL = re.compile(r"(보통토사|혼합토사|경질토사|토사|사질토|점질토)")
|
||||
ROCK = re.compile(r"(리핑암|발파암|연암|경암|풍화암|보통암|극경암|암절취|\(암\)| 암$|암\))")
|
||||
|
||||
|
||||
def soil(text: str) -> str | None:
|
||||
t = str(text or "")
|
||||
s, r = bool(SOIL.search(t)), bool(ROCK.search(t))
|
||||
return "토사" if s and not r else "암" if r and not s else None
|
||||
|
||||
|
||||
# ── 조건 걸기 ─────────────────────────────────────────────────────────
|
||||
def apply_book(res: dict, pool: dict, mode: str) -> dict:
|
||||
"""mode 'line' = 줄 근거 책 먼저 · 없으면 호표 다수 / 'hopyo' = 근거 없는 줄만 호표 다수."""
|
||||
out = {}
|
||||
for i, got in res.items():
|
||||
r = S.BY[i]
|
||||
own = own_books(r)
|
||||
votes: dict[str, int] = {}
|
||||
for key in HOPYO_OF.get(i, []):
|
||||
for j in MEMBERS[key]:
|
||||
for b in own_books(S.BY[j]):
|
||||
votes[b] = votes.get(b, 0) + 1
|
||||
top_v = sorted(votes.items(), key=lambda x: -x[1])
|
||||
major = top_v[0][0] if top_v and (len(top_v) == 1 or top_v[0][1] > top_v[1][1]) else None
|
||||
if mode == "line":
|
||||
want = next(iter(own)) if len(own) == 1 else (major if not own else None)
|
||||
else:
|
||||
want = major if not own else None
|
||||
top = got["top"]
|
||||
out[i] = got
|
||||
if not top or want is None or book(top[0]) == want:
|
||||
continue
|
||||
alts = top[1:5] + [c for c in pool[i][:10] if c not in top]
|
||||
pick = next((c for c in alts if book(c) == want and same_work(c, top[0])), None)
|
||||
if pick:
|
||||
out[i] = {"상태": "후보", "top": [pick] + [c for c in top if c != pick][:4]}
|
||||
return out
|
||||
|
||||
|
||||
def apply_soil(res: dict) -> dict:
|
||||
out = {}
|
||||
for i, got in res.items():
|
||||
r = S.BY[i]
|
||||
want = soil(f"{r['이름']} {r['규격']}") or next(
|
||||
(s for key in HOPYO_OF.get(i, []) if (s := soil(HEADS.get(key)))), None
|
||||
)
|
||||
top = got["top"]
|
||||
if not want or len(top) < 2 or got["상태"] == "같음":
|
||||
out[i] = got
|
||||
continue
|
||||
clash = lambda c: (s := soil(name(c))) is not None and s != want # noqa: E731
|
||||
out[i] = {**got, "top": sorted(top, key=clash)}
|
||||
return out
|
||||
|
||||
|
||||
def _match(slots: list[list[str]], keys: set) -> dict | None:
|
||||
"""slot 마다 다른 열쇠 하나(slot 후보 안) — 다 맞으면 {slot 차례: 열쇠}."""
|
||||
assign: dict[str, int] = {}
|
||||
|
||||
def try_(s, seen):
|
||||
for k in slots[s]:
|
||||
if k in keys and k not in seen:
|
||||
seen.add(k)
|
||||
if k not in assign or try_(assign[k], seen):
|
||||
assign[k] = s
|
||||
return True
|
||||
return False
|
||||
|
||||
for s in range(len(slots)):
|
||||
if not try_(s, set()):
|
||||
return None
|
||||
return {s: k for k, s in assign.items()}
|
||||
|
||||
|
||||
def backed(i: str, code: str | None) -> bool:
|
||||
"""1순위가 줄 근거 조항과 맞음(같은 책 조항 같음 0.9 이상)."""
|
||||
theirs = (S.BY[i].get("근거") or {}).get("출처") or []
|
||||
our = hb.ours().get(code) if code else None
|
||||
return bool(theirs and our and hb._clause(theirs, our)[0] >= 0.9)
|
||||
|
||||
|
||||
def apply_combo(res: dict, mode: str, guard: bool = False) -> tuple[dict, list]:
|
||||
"""mode 'whole' = 호표 로직 줄 수 = 조합 로직 수 · 'inside' = 호표 줄이 모두 한 조합 안(줄 2 이상) ·
|
||||
guard = 1순위가 줄 근거와 맞는 줄은 안 바꿈."""
|
||||
out = dict(res)
|
||||
hits = []
|
||||
for key, ids in MEMBERS.items():
|
||||
ids = [i for i in ids if res[i]["top"]]
|
||||
if not ids or (mode == "inside" and len(ids) < 2):
|
||||
continue
|
||||
slots = [res[i]["top"][:5] for i in ids]
|
||||
found = []
|
||||
for ck, cname, codes in COMBOS:
|
||||
keys = set(codes)
|
||||
if mode == "whole" and len(keys) != len(ids):
|
||||
continue
|
||||
m = _match(slots, keys)
|
||||
if m is not None:
|
||||
rank = sum(slots[s].index(k) for s, k in m.items())
|
||||
found.append((rank, ck, cname, m))
|
||||
if not found:
|
||||
continue
|
||||
found.sort()
|
||||
rank, ck, cname, m = found[0]
|
||||
hits.append((key, ck, cname, len(found), rank))
|
||||
for s, k in m.items():
|
||||
i = ids[s]
|
||||
top = res[i]["top"]
|
||||
if top[0] != k and not (guard and backed(i, top[0])):
|
||||
out[i] = {"상태": "후보", "top": [k] + [c for c in top if c != k][:4]}
|
||||
return out, hits
|
||||
|
||||
|
||||
def apply_name_vote(res: dict) -> dict:
|
||||
"""한 문서 안 정리 이름이 같은 줄(규격만 다름)은 같은 로직 — 다수 1순위가 다섯 안에 있으면 그것."""
|
||||
out = dict(res)
|
||||
groups: dict = {}
|
||||
for i in res:
|
||||
groups.setdefault((S.BY[i]["파일"], hm.tidy(S.BY[i]["이름"])), []).append(i)
|
||||
for ids in groups.values():
|
||||
if len(ids) < 2:
|
||||
continue
|
||||
votes: dict = {}
|
||||
for i in ids:
|
||||
t = (res[i]["top"] or [None])[0]
|
||||
if t:
|
||||
votes[t] = votes.get(t, 0) + S.BY[i]["줄수"]
|
||||
ranked = sorted(votes.items(), key=lambda x: -x[1])
|
||||
if not ranked or (len(ranked) > 1 and ranked[0][1] == ranked[1][1]):
|
||||
continue
|
||||
major = ranked[0][0]
|
||||
for i in ids:
|
||||
top = res[i]["top"]
|
||||
if top and top[0] != major and major in top[:5] and not backed(i, top[0]):
|
||||
out[i] = {"상태": "후보", "top": [major] + [c for c in top if c != major][:4]}
|
||||
return out
|
||||
|
||||
|
||||
def base_runs() -> dict:
|
||||
"""{이름: (결과, 후보 풀)} — 지금 · 묶음 ①(C3 추천 결합) · 씨앗 끔 둘."""
|
||||
out = {}
|
||||
out["지금"] = (S.run(), pools())
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold):
|
||||
out["묶음①"] = (S.run(typo_guard), pools())
|
||||
with no_seeds():
|
||||
out["지금(씨앗 끔)"] = (S.run(), pools())
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold):
|
||||
out["묶음①(씨앗 끔)"] = (S.run(typo_guard), pools())
|
||||
return out
|
||||
|
||||
|
||||
def changed(a: dict, b: dict) -> list:
|
||||
rows = []
|
||||
for i in a:
|
||||
if not S.LAB[i]["정답"]:
|
||||
continue
|
||||
t1, t2 = (a[i]["top"] or [None])[0], (b[i]["top"] or [None])[0]
|
||||
if t1 != t2:
|
||||
s, lo = set(S.LAB[i]["정답"]), set(S.LAB[i]["정답"]) | set(S.LAB[i]["허용"])
|
||||
mark = lambda t: "O" if t in s else "o" if t in lo else "x" # noqa: E731
|
||||
rows.append((i, S.BY[i]["이름"][:18], f"{mark(t1)}→{mark(t2)}", name(t1)[:14], name(t2)[:14]))
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
runs = base_runs()
|
||||
rows, keep = [], {}
|
||||
for base, (res, pool) in runs.items():
|
||||
steps = {"": res}
|
||||
steps["+책(줄→호표)"] = apply_book(res, pool, "line")
|
||||
steps["+책(호표만)"] = apply_book(res, pool, "hopyo")
|
||||
steps["+토질"] = apply_soil(res)
|
||||
steps["+조합 통째"], hits_w = apply_combo(res, "whole")
|
||||
steps["+조합 안"], hits_i = apply_combo(res, "inside")
|
||||
steps["+조합 통째(근거 지킴)"], _ = apply_combo(res, "whole", True)
|
||||
steps["+조합 안(근거 지킴)"], _ = apply_combo(res, "inside", True)
|
||||
steps["+같은 이름 다수(문서)"] = apply_name_vote(res)
|
||||
good, _ = apply_combo(apply_name_vote(apply_soil(res)), "inside", True)
|
||||
steps["+(가) 좋은 것(토질 · 이름 다수 · 조합 안 근거 지킴)"] = good
|
||||
allc = apply_soil(apply_book(res, pool, "line"))
|
||||
allc, _ = apply_combo(allc, "whole")
|
||||
steps["+모두(책 · 토질 · 통째)"] = allc
|
||||
for tag, r in steps.items():
|
||||
rows.append(S.table(f"{base}{tag}", r))
|
||||
keep[base] = {k: v for k, v in steps.items()}
|
||||
if base in ("지금", "묶음①"):
|
||||
print(f"== {base}: 조합 통째 맞음 호표 {len(hits_w)} · 안에 다 듦 {len(hits_i)}")
|
||||
for key, ck, cname, n, rank in hits_w + hits_i:
|
||||
print(f" {key} {HEADS.get(key)[:24]} → {ck} {cname} (맞는 조합 {n} · 순위합 {rank})")
|
||||
for tag in ("+책(줄→호표)", "+책(호표만)", "+토질", "+조합 통째", "+조합 안", "+조합 통째(근거 지킴)", "+조합 안(근거 지킴)", "+같은 이름 다수(문서)"):
|
||||
for row in changed(res, steps[tag]):
|
||||
print(f" [{tag}] {row}")
|
||||
for row in rows:
|
||||
S.show(row)
|
||||
(HERE / "closed.json").write_text(
|
||||
json.dumps({b: {k: v for k, v in s.items()} for b, s in keep.items()}, ensure_ascii=False), "utf-8"
|
||||
)
|
||||
(HERE / "closed_table.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,254 @@
|
||||
"""req226 C5 — 조합 풀이 엔진(닫힌 세계 · 금액 안 씀).
|
||||
|
||||
블록(단산 · STmate D) 또는 직접 줄 호표의 노임 · 장비 벡터를 우리 로직 단위당 벡터 조합으로 풂:
|
||||
① 통째 하나 — 블록 합을 로직 하나가 설명(맞음 ≥ 0.85)
|
||||
② 절마다 하나 — 맨 윗 번호 절(1. 2. 3.)마다(맞음 ≥ 0.75)
|
||||
③ 몫마다 — 절 안 가. 나. 몫마다 · 장비만 든 몫 = 「기계 시간당 기계경비」(기종 입력 · 닫힌 세계 규칙) ·
|
||||
노임 몫 = 맞음 ≥ 0.6
|
||||
후보 풀 = 지금 맞춤 합친 후보 15(그대로 부름) + 직종 색인(드문 직종 · 장비 40 개 이하) + 절 제목 이름 후보 8.
|
||||
순위 = 벡터 맞음 + 앞선 믿음(지금 순위 0.3 · 줄 근거 조항 같음(윗마디 포함) 0.3 · 같은 책 0.1).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225" / "C1"
|
||||
for p in (HERE, C1):
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import c03_vec as V # noqa: E402
|
||||
from c01_trees import ROOT # noqa: E402,F401
|
||||
from s04_eval import clause_prefix, hb, hm # noqa: E402
|
||||
|
||||
MACHINE = {"굴착기": "GC000267"} # 굴착기(무한궤도) 시간당 기계경비 · 그 밖 장비 = GC000268
|
||||
MACHINE_ANY = "GC000268"
|
||||
MACHINE_SET = {"GC000267", "GC000268", "GC000269", "GC000275"}
|
||||
RARE = {d for d, codes in V.IDX.items() if len(codes) <= 40}
|
||||
|
||||
|
||||
def merged(line: dict, got: dict, n: int = 15) -> list[str]:
|
||||
basis = hb.candidates(line, got)
|
||||
named = hm.candidates(line, "로직", got, None)
|
||||
return [c["로직"] for c in hb.merge(basis, named) if c.get("로직")][:n]
|
||||
|
||||
|
||||
def title_cands(title: str, unit: str, got: dict, n: int = 8) -> list[str]:
|
||||
text = re.sub(r"[\d.·:()\[\]%]+", " ", str(title or ""))
|
||||
text = re.sub(r"\s+", "", text)
|
||||
if len(text) < 2:
|
||||
return []
|
||||
return [c["로직"] for c in hm.candidates({"이름": text, "단위": unit}, "로직", got, n) if c.get("로직")]
|
||||
|
||||
|
||||
class Prior:
|
||||
"""앞선 믿음 — 지금 순위 · 줄 근거 조항(윗마디 포함) · 같은 책."""
|
||||
|
||||
def __init__(self, line: dict, ranked: list[str]):
|
||||
self.rank = {c: k for k, c in enumerate(ranked)}
|
||||
self.n = max(len(ranked), 1)
|
||||
self.theirs = [list(x) for x in ((line.get("근거") or {}).get("출처") or [])]
|
||||
self.books = {b for b, _ in self.theirs}
|
||||
self.table = hb.ours()
|
||||
|
||||
def __call__(self, code: str) -> float:
|
||||
p = 0.0
|
||||
if code in self.rank:
|
||||
p += 0.3 * (1 - self.rank[code] / self.n)
|
||||
our = self.table.get(code)
|
||||
if our and self.theirs:
|
||||
if clause_prefix(self.theirs, our)[0] >= 0.9:
|
||||
p += 0.3
|
||||
if our["책"] in self.books:
|
||||
p += 0.1
|
||||
return p
|
||||
|
||||
|
||||
def _sum(parts: list[dict]) -> dict:
|
||||
out = {"노임": {}, "장비": {}}
|
||||
for s in parts:
|
||||
for k in ("노임", "장비"):
|
||||
for d, q in (s.get(k) or {}).items():
|
||||
out[k][d] = out[k].get(d, 0) + q
|
||||
return out
|
||||
|
||||
|
||||
def best(E: dict, pool: list[str], prior: Prior, share: bool) -> list[tuple]:
|
||||
"""[(순위 점수, 맞음, 몫 x, 로직, 입력)] — 순위 차례."""
|
||||
out = []
|
||||
for c in pool:
|
||||
f, given, x = fit(E, c, share)
|
||||
if f <= 0:
|
||||
continue
|
||||
out.append((f + prior(c), f, x, c, given))
|
||||
out.sort(key=lambda z: -z[0])
|
||||
return out
|
||||
|
||||
|
||||
def fit(E: dict, code: str, share: bool) -> tuple:
|
||||
top = (0.0, "{}", 1.0)
|
||||
for given, O in V.variants(code):
|
||||
s = V.sim(E, O)
|
||||
if s > top[0]:
|
||||
top = (s, given, 1.0)
|
||||
if share:
|
||||
for x in V.SHARES:
|
||||
s2 = 0.95 * V.sim(E, O, x)
|
||||
if s2 > top[0]:
|
||||
top = (s2, given, x)
|
||||
return top
|
||||
|
||||
|
||||
def machine_of(E: dict) -> str:
|
||||
kinds = {k.split("|")[0] for k in E.get("장비") or {}}
|
||||
return MACHINE["굴착기"] if kinds == {"굴착기"} else MACHINE_ANY
|
||||
|
||||
|
||||
def pool_for(E: dict, base: list[str], extra: list[str]) -> list[str]:
|
||||
rare = set()
|
||||
for d in V.dims(E):
|
||||
if d in RARE:
|
||||
rare |= V.IDX[d]
|
||||
return list(dict.fromkeys([*base, *extra, *sorted(rare)]))
|
||||
|
||||
|
||||
def solve(parts: list[dict], line: dict, got: dict, base: list[str] | None = None) -> dict:
|
||||
"""parts(몫 목록 · 절 표시) → {고름: [(로직, 어디, 맞음, x, 단계)], 통째: …}."""
|
||||
base = base if base is not None else merged(line, got)
|
||||
prior = Prior(line, base)
|
||||
unit = str(line.get("단위") or "")
|
||||
if not parts:
|
||||
return {"고름": [], "단계": "벡터 없음"}
|
||||
whole = _sum(parts)
|
||||
share = any("%" in str(p.get("제목")) for p in parts)
|
||||
titles = list(dict.fromkeys(str(p.get("절") or p.get("제목") or "") for p in parts))
|
||||
extra = [c for t in titles for c in title_cands(t, unit, got)]
|
||||
pool = pool_for(whole, base, extra)
|
||||
ranked = best(whole, pool, prior, share)
|
||||
if ranked and ranked[0][1] >= 0.85:
|
||||
_, f, x, c, given = ranked[0]
|
||||
return {"고름": [(c, "통째", round(f, 3), x, "①")], "단계": "①"}
|
||||
chosen = []
|
||||
groups: dict[str, list] = {}
|
||||
for p in parts:
|
||||
groups.setdefault(str(p.get("절") or p.get("제목") or ""), []).append(p)
|
||||
for title, ps in groups.items():
|
||||
E = _sum(ps)
|
||||
sh = share or "%" in title or any("%" in str(p.get("제목")) for p in ps)
|
||||
cand = best(E, pool_for(E, base, title_cands(title, unit, got)), prior, sh)
|
||||
if cand and cand[0][1] >= 0.75:
|
||||
_, f, x, c, given = cand[0]
|
||||
chosen.append((c, title, round(f, 3), x, "②"))
|
||||
continue
|
||||
for p in ps:
|
||||
if p.get("장비") and not p.get("노임"):
|
||||
chosen.append((machine_of(p), p.get("제목"), 0.0, 1.0, "③장비"))
|
||||
continue
|
||||
sh2 = sh or "%" in str(p.get("제목"))
|
||||
cand = best(p, pool_for(p, base, title_cands(p.get("제목"), unit, got)), prior, sh2)
|
||||
if cand and cand[0][1] >= 0.6:
|
||||
_, f, x, c, given = cand[0]
|
||||
chosen.append((c, p.get("제목"), round(f, 3), x, "③"))
|
||||
return {"고름": chosen, "단계": "②③"}
|
||||
|
||||
|
||||
def codes(result: dict) -> list[str]:
|
||||
return list(dict.fromkeys(c for c, *_ in result.get("고름") or []))
|
||||
|
||||
|
||||
# ── 1순위 + 설명 안 된 몫(조합 풀이) ──────────────────────────────────────
|
||||
_SHARE = re.compile(r"(\d{1,3})\s*%")
|
||||
_RATIO = re.compile(r"(\d)\s*:\s*(\d)")
|
||||
|
||||
|
||||
def cover(E: dict, code: str) -> float:
|
||||
"""E 의 차원을 로직 벡터가 얼마나 덮나(로직의 남는 차원은 안 셈) — 갈래 중 가장 좋은 것."""
|
||||
top = 0.0
|
||||
for _, O in V.variants(code):
|
||||
num = den = 0.0
|
||||
for t, a in (E.get("노임") or {}).items():
|
||||
w = 0.5 if t == V.COMMON else 1.0
|
||||
b = (O.get("노임") or {}).get(t, 0.0)
|
||||
den += w
|
||||
num += w * (min(a, b) / max(a, b) if a > 0 and b > 0 else 0)
|
||||
for k, a in (E.get("장비") or {}).items():
|
||||
kind = k.split("|")[0]
|
||||
den += 1
|
||||
if any(k2.split("|")[0] == kind for k2 in (O.get("장비") or {})):
|
||||
num += 1
|
||||
top = max(top, num / den if den else 0)
|
||||
return top
|
||||
|
||||
|
||||
def is_mixed(parts: list[dict], line: dict) -> bool:
|
||||
"""몫 나눔(기계 90% + 인력 10% · 9 : 1) — 제목 · 이름 · 규격에 몫 표시 + 노임만 든 몫 + 장비만 든 몫."""
|
||||
text = " ".join([str(p.get("제목")) for p in parts] + [str(line.get("이름")), str(line.get("규격"))])
|
||||
marked = bool(_SHARE.search(text) or _RATIO.search(text))
|
||||
man = any(p.get("노임") and not p.get("장비") for p in parts)
|
||||
mach = any(p.get("장비") and not p.get("노임") for p in parts)
|
||||
return marked and man and mach
|
||||
|
||||
|
||||
def _hint(title: str) -> float | None:
|
||||
m = _SHARE.search(str(title or ""))
|
||||
if m and 0 < int(m.group(1)) < 100:
|
||||
return int(m.group(1)) / 100
|
||||
return None
|
||||
|
||||
|
||||
def best_hint(E, pool, prior, share, hint):
|
||||
out = []
|
||||
for score, f, x, c, given in best(E, pool, prior, share):
|
||||
if hint is not None:
|
||||
score += 0.1 if abs(x - hint) < 1e-9 else -0.1
|
||||
out.append((score, f, x, c, given))
|
||||
out.sort(key=lambda z: -z[0])
|
||||
return out
|
||||
|
||||
|
||||
def combine(primary: str | None, parts: list[dict], line: dict, got: dict, base: list[str]) -> list[tuple]:
|
||||
"""[(로직, 어디, 맞음, x, 단계)] — 1순위 + 1순위가 못 덮는 절 · 몫의 로직."""
|
||||
chosen = [(primary, "1순위", None, 1.0, "1순위")] if primary else []
|
||||
if not parts:
|
||||
return chosen
|
||||
prior = Prior(line, base)
|
||||
unit = str(line.get("단위") or "")
|
||||
mixed = is_mixed(parts, line)
|
||||
groups: dict[str, list] = {}
|
||||
for p in parts:
|
||||
groups.setdefault(str(p.get("절") or p.get("제목") or ""), []).append(p)
|
||||
have = {primary} if primary else set()
|
||||
for title, ps in groups.items():
|
||||
E = _sum(ps)
|
||||
if primary and cover(E, primary) >= 0.6:
|
||||
continue
|
||||
if E["장비"] and not E["노임"]: # 장비만 든 절(상차 · 운반 · 기계 몫) = 기계 시간 로직(닫힌 세계 규칙)
|
||||
if mixed or len(groups) > 1:
|
||||
m = machine_of(E)
|
||||
chosen.append((m, title, None, 1.0, "장비 몫"))
|
||||
have.add(m)
|
||||
continue
|
||||
share = any(_hint(p.get("제목")) for p in ps) or _hint(title) is not None
|
||||
cand = best_hint(E, pool_for(E, base, title_cands(title, unit, got)), prior, share, _hint(title))
|
||||
if cand and cand[0][1] >= 0.75 and cand[0][3] not in have:
|
||||
chosen.append((cand[0][3], title, round(cand[0][1], 3), cand[0][2], "절"))
|
||||
have.add(cand[0][3])
|
||||
continue
|
||||
for p in ps:
|
||||
if primary and cover(p, primary) >= 0.6:
|
||||
continue
|
||||
if p.get("장비") and not p.get("노임"):
|
||||
if mixed or len(groups) > 1:
|
||||
m = machine_of(p)
|
||||
chosen.append((m, p.get("제목"), None, 1.0, "장비 몫"))
|
||||
have.add(m)
|
||||
continue
|
||||
hint = _hint(p.get("제목"))
|
||||
cand = best_hint(p, pool_for(p, base, title_cands(p.get("제목"), unit, got)), prior, hint is not None or share, hint)
|
||||
if cand and cand[0][1] >= 0.6 and cand[0][3] not in have:
|
||||
chosen.append((cand[0][3], p.get("제목"), round(cand[0][1], 3), cand[0][2], "몫"))
|
||||
have.add(cand[0][3])
|
||||
return chosen
|
||||
@@ -0,0 +1,109 @@
|
||||
"""req226 C5 — 정답 있는 세 파일 줄에 조합 풀이(c05) 걸기 → 줄마다 고른 로직 모음 · 갈래별 풂(4 번 물음).
|
||||
|
||||
python c06_lines.py → lines_solved.json · 갈래 표
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225" / "C1"
|
||||
for p in (HERE, C1):
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import c05_solve as SV # noqa: E402
|
||||
import s10_all as S # noqa: E402
|
||||
from s04_eval import clause_prefix, hb, hm, patched # noqa: E402
|
||||
from s10_all import candidates_nofold # noqa: E402
|
||||
|
||||
TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8"))
|
||||
PARTS = {}
|
||||
for f in TREES:
|
||||
for h in f["호표"]:
|
||||
for one in h["줄"]:
|
||||
if one.get("id") and one.get("몫") and one["id"] not in PARTS:
|
||||
PARTS[one["id"]] = one["몫"]
|
||||
|
||||
FINER = ["N003", "N019", "N045", "S055", "S059", "Y010"] # 우리 로직이 더 잘게(철근 가공 + 조립)
|
||||
COMPOSITE = { # 엑셀 복합 호표 · 복합 줄 — 모두 들어야 풂
|
||||
"S062": {"GC000559", "GC000560"},
|
||||
"N050": {"GC000705", "GC000714", "GC000712"},
|
||||
"S095": {"GF000135", "GF000284"},
|
||||
"S097": {"GF000135", "GF000284"},
|
||||
**{i: {"GC000206", "GC000207"} for i in FINER},
|
||||
}
|
||||
|
||||
|
||||
BEST = "+(가) 좋은 것(토질 · 이름 다수 · 조합 안 근거 지킴)"
|
||||
CLOSED = json.loads((HERE / "closed.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def mixed_ids() -> list[str]:
|
||||
"""섞인 줄 = 몫 표시(10% · 9 : 1) + 노임만 든 몫 + 장비만 든 몫."""
|
||||
return sorted(i for i, parts in PARTS.items() if S.LAB.get(i, {}).get("정답") and SV.is_mixed(parts, S.BY[i]))
|
||||
|
||||
|
||||
def run(base_name: str = "묶음①", step: str = BEST) -> dict:
|
||||
out = {}
|
||||
tops = CLOSED[base_name][step]
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold):
|
||||
hm._targets.cache_clear()
|
||||
hb._ours.cache_clear()
|
||||
got = hm.targets()
|
||||
for r in S.ALL:
|
||||
i = r["id"]
|
||||
line = {k: r.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")}
|
||||
primary = (tops[i]["top"] or [None])[0]
|
||||
base = SV.merged(line, got)
|
||||
chosen = SV.combine(primary, PARTS.get(i) or [], line, got, base)
|
||||
out[i] = {"고름": chosen, "단계": "조합"}
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
res = run()
|
||||
SV.V.save_cache()
|
||||
(HERE / "lines_solved.json").write_text(json.dumps(res, ensure_ascii=False, indent=1), "utf-8")
|
||||
names = {x["로직"]: x["이름"] for x in hm.targets()["로직"]}
|
||||
|
||||
def show(i):
|
||||
got = SV.codes(res[i])
|
||||
lab = S.LAB[i]
|
||||
print(f" {i} {S.BY[i]['이름'][:20]:<20} 정답 {lab['정답']} 허용 {lab['허용']}")
|
||||
for c, where, f, x, step in res[i]["고름"]:
|
||||
print(f" {step} {c} {names.get(c, '')[:22]:<22} 맞음 {f} x {x} ← {str(where)[:30]}")
|
||||
return got
|
||||
|
||||
print("== 우리 로직이 더 잘게(철근 가공 + 조립)")
|
||||
ok = sum(1 for i in FINER if COMPOSITE[i] <= set(show(i)))
|
||||
print(f" 풂 {ok}/{len(FINER)}")
|
||||
print("== 엑셀 복합 호표 · 복합 줄")
|
||||
for i in ("S062", "N050", "S095", "S097"):
|
||||
got = show(i)
|
||||
print(f" → {'풂' if COMPOSITE[i] <= set(got) else '못 풂'}")
|
||||
print("== 섞인 줄(기계 몫 + 인력 몫)")
|
||||
mix = mixed_ids()
|
||||
man_ok = both = 0
|
||||
for i in mix:
|
||||
got = set(show(i))
|
||||
lab = set(S.LAB[i]["정답"]) | set(S.LAB[i]["허용"])
|
||||
m = bool(got & lab)
|
||||
k = bool(got & SV.MACHINE_SET)
|
||||
man_ok += m
|
||||
both += m and k
|
||||
print(f" → 인력 몫(정답 · 허용) {'O' if m else 'X'} · 기계 몫(기계 시간 로직) {'O' if k else 'X'}")
|
||||
print(f" 섞인 줄 {len(mix)} {mix} · 인력 몫 맞음 {man_ok} · 둘 다 {both}")
|
||||
# 홑 정답 줄에 덧붙은 로직(잘못 쪼갬) 셈
|
||||
single = [i for i in res if S.LAB[i]["정답"] and i not in COMPOSITE and i not in mix]
|
||||
split = [i for i in single if len(SV.codes(res[i])) > 1]
|
||||
hit1 = [i for i in single if set(SV.codes(res[i])) & (set(S.LAB[i]["정답"]) | set(S.LAB[i]["허용"]))]
|
||||
print(f"== 홑 정답 줄 {len(single)} · 둘 넘게 고름 {len(split)} · 고른 것에 정답(느슨) 듦 {len(hit1)} · 벡터 없음 {sum(1 for i in single if not PARTS.get(i))}")
|
||||
for i in split[:12]:
|
||||
print(f" {i} {S.BY[i]['이름'][:18]} → {[names.get(c, c)[:12] for c in SV.codes(res[i])]} 정답 {S.LAB[i]['정답']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,170 @@
|
||||
"""req226 C5 — 새 지표 「조합 맞음」(호표 단위 · 정답 있는 세 파일).
|
||||
|
||||
정답 = 앞 정답(labels · labels_y)을 호표로 모음 — 줄마다 칸(slot) 하나(정답 여럿 = 그중 하나) ·
|
||||
철근 가공 + 조립 · 도장 셋 · 잡석 운반 둘 = 모두(칸 여럿) · 정답 없는 줄 = 「없음」 칸(채울 수 없음) ·
|
||||
호표 참조 줄 = 그 호표 칸을 아래에서 위로 더함.
|
||||
T2(정할 것 8 다른 안) = 섞인 줄에 「기계 몫」 칸(기계 시간 로직) · 철골 = 가공조립 + 용접 두 칸.
|
||||
우리 = 줄 1순위 모음(지금 · 묶음 ① · (가)) 또는 + 조합 풀이(c06 고른 것).
|
||||
지표 = 통째 맞음(칸 다 참 · 남는 것 없음) · 칸 맞음 비율 · 고른 것 맞음 비율.
|
||||
|
||||
python c07_hopyo.py → 표 · hopyo_metric.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225" / "C1"
|
||||
for p in (HERE, C1):
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import c05_solve as SV # noqa: E402
|
||||
import s10_all as S # noqa: E402
|
||||
from c06_lines import COMPOSITE, FINER, PARTS # noqa: E402
|
||||
|
||||
TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8"))
|
||||
CLOSED = json.loads((HERE / "closed.json").read_text(encoding="utf-8"))
|
||||
SOLVED = json.loads((HERE / "lines_solved.json").read_text(encoding="utf-8"))
|
||||
LABELED = ("북부청임도", "북부청사방", "동부청영월")
|
||||
BEST = "+(가) 좋은 것(토질 · 이름 다수 · 조합 안 근거 지킴)"
|
||||
ALL_OF = {i: sorted(v) for i, v in COMPOSITE.items() if i != "S062"}
|
||||
MIXED = {i for i, parts in PARTS.items() if S.LAB.get(i, {}).get("정답") and SV.is_mixed(parts, S.BY[i])}
|
||||
|
||||
|
||||
def slots_of(i: str, t2: bool) -> list[tuple[set, set]]:
|
||||
"""[(엄격 칸, 느슨 칸)] — 정답 없으면 (set(), set())."""
|
||||
lab = S.LAB[i]
|
||||
loose_extra = set(lab["허용"])
|
||||
if not lab["정답"]:
|
||||
return [(set(), set())]
|
||||
if i in ALL_OF:
|
||||
out = [({x}, {x}) for x in ALL_OF[i]]
|
||||
else:
|
||||
out = [(set(lab["정답"]), set(lab["정답"]) | loose_extra)]
|
||||
if t2 and i == "S062":
|
||||
out.append(({"GC000560"}, {"GC000560"}))
|
||||
if t2 and i in MIXED:
|
||||
out.append((set(SV.MACHINE_SET), set(SV.MACHINE_SET)))
|
||||
return out
|
||||
|
||||
|
||||
def hopyo_index():
|
||||
out = {}
|
||||
for f in TREES:
|
||||
if f["파일"] not in LABELED:
|
||||
continue
|
||||
for h in f["호표"]:
|
||||
ids = list(dict.fromkeys(one["id"] for one in h["줄"] if one.get("id")))
|
||||
refs = [one["참조"].get("번") for one in h["줄"] if one["갈래"] == "호표" and one.get("참조")]
|
||||
out[(f["파일"], h["번"])] = {"ids": ids, "refs": [r for r in refs if r], "이름": h["이름"]}
|
||||
return out
|
||||
|
||||
|
||||
HOPYO = hopyo_index()
|
||||
|
||||
|
||||
def gather(key, seen=()) -> list[str]:
|
||||
"""호표 → 로직 줄 id(참조 호표 것까지 · 아래에서 위로)."""
|
||||
h = HOPYO.get(key)
|
||||
if not h or key in seen:
|
||||
return []
|
||||
out = list(h["ids"])
|
||||
for no in h["refs"]:
|
||||
out += gather((key[0], no), (*seen, key))
|
||||
return list(dict.fromkeys(out))
|
||||
|
||||
|
||||
FINAL = json.loads((HERE / "final.json").read_text(encoding="utf-8")) if (HERE / "final.json").exists() else {}
|
||||
|
||||
|
||||
def chosen_of(i: str, pick: str) -> set:
|
||||
if pick == "조합+뺌":
|
||||
return set(FINAL["sets"][i]) - {None}
|
||||
if pick == "조합+뺌-기계": # 정할 것 8 = 인력 몫 하나(기계 몫 로직은 안 넣음)
|
||||
top = (FINAL["tops"][i]["top"] or [None])[0]
|
||||
return {c for c in FINAL["sets"][i] if c and (c == top or c not in SV.MACHINE_SET)}
|
||||
if pick.startswith("조합"):
|
||||
return set(SV.codes({"고름": SOLVED[i]["고름"]})) - {None}
|
||||
base, step = pick.split("|")
|
||||
top = CLOSED[base][step][i]["top"]
|
||||
return {top[0]} if top else set()
|
||||
|
||||
|
||||
def judge(key, pick: str, t2: bool) -> dict | None:
|
||||
ids = gather(key)
|
||||
if not ids:
|
||||
return None
|
||||
slots, chosen = [], set()
|
||||
for i in ids:
|
||||
slots += slots_of(i, t2)
|
||||
chosen |= chosen_of(i, pick)
|
||||
out = {}
|
||||
for mode, k in (("엄격", 0), ("느슨", 1)):
|
||||
alts = set().union(*[s[k] for s in slots])
|
||||
sat = sum(1 for s in slots if s[k] and chosen & s[k])
|
||||
extra = chosen - alts
|
||||
out[mode] = {
|
||||
"통째": sat == len(slots) and not extra,
|
||||
"칸": sat / len(slots),
|
||||
"고름": (len(chosen & alts) / len(chosen)) if chosen else 0,
|
||||
}
|
||||
out["없음칸"] = any(not s[0] for s in slots)
|
||||
out["칸수"] = len(slots)
|
||||
return out
|
||||
|
||||
|
||||
PICKS = {
|
||||
"지금": "지금|",
|
||||
"묶음 ①": "묶음①|",
|
||||
"묶음 ① + (가)": f"묶음①|{BEST}",
|
||||
"묶음 ① + (가) + 조합 풀이": "조합",
|
||||
"… + 못 덮는 1순위 뺌(섞인 줄)": "조합+뺌",
|
||||
"… 기계 몫 안 넣음(T1 쪽)": "조합+뺌-기계",
|
||||
}
|
||||
|
||||
|
||||
def table(t2: bool) -> list:
|
||||
rows = []
|
||||
for label, pick in PICKS.items():
|
||||
res = {k: judge(k, pick, t2) for k in HOPYO}
|
||||
res = {k: v for k, v in res.items() if v}
|
||||
row = {"우리": label}
|
||||
for scope, keys in (
|
||||
("전체", list(res)),
|
||||
("덮을 수 있음", [k for k, v in res.items() if not v["없음칸"]]),
|
||||
):
|
||||
for mode in ("엄격", "느슨"):
|
||||
n = len(keys)
|
||||
row[f"{scope} {mode} 통째"] = round(100 * sum(res[k][mode]["통째"] for k in keys) / n, 1)
|
||||
row[f"{scope} {mode} 칸"] = round(100 * sum(res[k][mode]["칸"] for k in keys) / n, 1)
|
||||
row[f"{scope} {mode} 고름"] = round(100 * sum(res[k][mode]["고름"] for k in keys) / n, 1)
|
||||
row[f"{scope} 호표"] = len(keys)
|
||||
for f in LABELED:
|
||||
keys = [k for k in res if k[0] == f and not res[k]["없음칸"]]
|
||||
row[f"{f} 엄격 통째"] = f"{sum(res[k]['엄격']['통째'] for k in keys)}/{len(keys)}"
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
out = {}
|
||||
for t2 in (False, True):
|
||||
name = "T2(섞인 줄 기계 몫 · 철골 두 칸)" if t2 else "T1(앞 정답 그대로)"
|
||||
rows = table(t2)
|
||||
out[name] = rows
|
||||
print("==", name)
|
||||
for r in rows:
|
||||
print(
|
||||
f" {r['우리']:<22} 호표 {r['전체 호표']}(덮을 수 있음 {r['덮을 수 있음 호표']}) | "
|
||||
f"전체 통째 엄격 {r['전체 엄격 통째']} 느슨 {r['전체 느슨 통째']} · 칸 {r['전체 엄격 칸']}/{r['전체 느슨 칸']} · 고름 {r['전체 엄격 고름']}/{r['전체 느슨 고름']} | "
|
||||
f"덮을 수 있음 통째 {r['덮을 수 있음 엄격 통째']}/{r['덮을 수 있음 느슨 통째']} · 칸 {r['덮을 수 있음 엄격 칸']}/{r['덮을 수 있음 느슨 칸']} | "
|
||||
f"임도 {r['북부청임도 엄격 통째']} 사방 {r['북부청사방 엄격 통째']} 영월 {r['동부청영월 엄격 통째']}"
|
||||
)
|
||||
(HERE / "hopyo_metric.json").write_text(json.dumps(out, ensure_ascii=False, indent=1), "utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
"""조합 풀이 뒤 「줄 벡터를 못 덮는 1순위는 뺌」(닫힌 세계) — 줄 지표 · 호표 지표 다시 셈."""
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
import c05_solve as SV
|
||||
import s10_all as S
|
||||
from c06_lines import PARTS, CLOSED, BEST
|
||||
|
||||
SOLVED = json.loads((Path(__file__).parent / "lines_solved.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def regroup(cut: float = 0.35) -> tuple[dict, dict]:
|
||||
"""(줄 결과 {id: {상태, top}}, 고른 것 {id: [로직]})."""
|
||||
base = CLOSED["묶음①"][BEST]
|
||||
tops, sets, dropped = {}, {}, []
|
||||
for i, got in base.items():
|
||||
chosen = [c for c, *_ in SOLVED[i]["고름"] if c]
|
||||
chosen = list(dict.fromkeys(chosen))
|
||||
top = list(got["top"])
|
||||
parts = PARTS.get(i) or []
|
||||
if top and len(chosen) > 1 and parts and SV.is_mixed(parts, S.BY[i]): # 몫 나눈 줄만
|
||||
whole = SV._sum(parts)
|
||||
if SV.cover(whole, top[0]) < cut:
|
||||
rest = [c for c in chosen if c != top[0]]
|
||||
labor = [c for c, where, f, x, step in SOLVED[i]["고름"] if c in rest and step in ("몫", "절")]
|
||||
if labor:
|
||||
dropped.append((i, top[0], labor[0]))
|
||||
chosen = rest
|
||||
top = [labor[0]] + [c for c in top if c != labor[0]][:4]
|
||||
tops[i] = {"상태": got["상태"], "top": top}
|
||||
sets[i] = chosen
|
||||
return tops, sets, dropped
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tops, sets, dropped = regroup(0.35)
|
||||
(Path(__file__).parent / "final.json").write_text(json.dumps({"tops": tops, "sets": sets}, ensure_ascii=False), "utf-8")
|
||||
for cut in (0.2, 0.35, 0.5):
|
||||
tops, sets, dropped = regroup(cut)
|
||||
S.show(S.table(f"(가)+조합 1순위 뺌 {cut}", tops))
|
||||
print(" 뺀 줄", [(i, a, b) for i, a, b in dropped])
|
||||
S.show(S.table("(가) 그대로", CLOSED["묶음①"][BEST]))
|
||||
@@ -0,0 +1,183 @@
|
||||
"""req226 C5 — (나) 직접 줄 호표(노임 · 장비 줄이 호표에 바로) → 우리 로직 조합 풀이.
|
||||
|
||||
E = 호표의 노임 · 장비 줄 벡터(참조 줄 뺌). 셋을 견줌:
|
||||
이름만 = 호표 머리(이름 · 규격 · 단위)를 지금 맞춤 함수에 줄로 넣음(330 근거 없음)
|
||||
이름 + 인용 = STmate 근거글(「건설표준품셈 7-2-2(찰붙임)」)을 근거 출처로 함께
|
||||
벡터 풀이 = c05 조합 풀이(이름 + 인용 후보 · 드문 직종 색인) — 둘 넘게면 음 아닌 최소제곱 짝까지
|
||||
판정 = 인용 정답(인용 조항 · 직종이 맞는 우리 로직 · 짐작) · 거창 다섯 사람 판정(짐작).
|
||||
|
||||
python c09_direct.py → 표 · direct.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from scipy.optimize import nnls
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225" / "C1"
|
||||
for p in (HERE, C1):
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import c05_solve as SV # noqa: E402
|
||||
from s04_eval import clause_prefix, hb, hm # noqa: E402
|
||||
|
||||
TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8"))
|
||||
HUMAN = { # 사람 판정(짐작) — 거창 직접 줄 호표 · 일위대가조합 UA000014(거창 5 · 6 출처)와 맞음
|
||||
("거창", 1): {"GC000198"},
|
||||
("거창", 2): {"GC000198"},
|
||||
("거창", 3): {"GC000211"},
|
||||
("거창", 10): {"GC000198"},
|
||||
("거창", 11): {"GC000015"},
|
||||
}
|
||||
|
||||
|
||||
def direct_hopyos():
|
||||
for f in TREES:
|
||||
for h in f["호표"]:
|
||||
if not any(one["갈래"] in ("노임", "장비") for one in h["줄"]):
|
||||
continue
|
||||
E = {"노임": {}, "장비": {}}
|
||||
for one in h["줄"]:
|
||||
for k in ("노임", "장비"):
|
||||
for d, q in (one.get(k) or {}).items():
|
||||
if d and q:
|
||||
E[k][d] = E[k].get(d, 0) + float(q)
|
||||
cite = " ".join(f"{one['이름']} {one['규격']}" for one in h["줄"] if one["갈래"] == "근거글")
|
||||
yield f["파일"], h, E, cite
|
||||
|
||||
|
||||
def cite_truth(theirs: list, E: dict) -> set:
|
||||
trades = {t for t in E["노임"] if t != "보통인부"}
|
||||
out = set()
|
||||
table = hb.ours()
|
||||
for one in hm.targets()["로직"]:
|
||||
our = table.get(one["로직"])
|
||||
if not our or clause_prefix(theirs, our)[0] < 0.9:
|
||||
continue
|
||||
if trades and not (trades & our["직종"]):
|
||||
continue
|
||||
out.add(one["로직"])
|
||||
return out
|
||||
|
||||
|
||||
def pair_fit(E: dict, pool: list[str]) -> tuple:
|
||||
"""둘 짝 음 아닌 최소제곱 — (맞음, (로직 · x) …)."""
|
||||
dims = sorted({("노임", d) for d in E["노임"]} | {("장비", d.split("|")[0]) for d in E["장비"]})
|
||||
if len(dims) < 2:
|
||||
return (0.0, ())
|
||||
|
||||
def col(O):
|
||||
out = []
|
||||
for k, d in dims:
|
||||
if k == "노임":
|
||||
out.append((O.get("노임") or {}).get(d, 0.0))
|
||||
else:
|
||||
out.append(sum(v for kk, v in (O.get("장비") or {}).items() if kk.split("|")[0] == d))
|
||||
return np.array(out)
|
||||
|
||||
target = col({"노임": E["노임"], "장비": E["장비"]})
|
||||
scale = np.where(target > 0, 1 / np.maximum(target, 1e-9), 1.0)
|
||||
best = (0.0, ())
|
||||
ranked = sorted(pool, key=lambda c: -SV.fit(E, c, False)[0])[:6]
|
||||
vecs = {c: [O for _, O in SV.V.variants(c)][:3] for c in ranked}
|
||||
for a, b in itertools.combinations(ranked, 2):
|
||||
for Oa in vecs[a]:
|
||||
for Ob in vecs[b]:
|
||||
A = np.stack([col(Oa) * scale, col(Ob) * scale], axis=1)
|
||||
x, _ = nnls(A, target * scale)
|
||||
if (x <= 0.05).any():
|
||||
continue
|
||||
mix = {"노임": {}, "장비": {}}
|
||||
for O, w in ((Oa, x[0]), (Ob, x[1])):
|
||||
for k in ("노임", "장비"):
|
||||
for d, q in (O.get(k) or {}).items():
|
||||
mix[k][d] = mix[k].get(d, 0) + q * w
|
||||
s = SV.V.sim(E, mix)
|
||||
if s > best[0]:
|
||||
best = (s, ((a, round(x[0], 2)), (b, round(x[1], 2))))
|
||||
return best
|
||||
|
||||
|
||||
def main():
|
||||
got = hm.targets()
|
||||
names = {x["로직"]: x["이름"] for x in got["로직"]}
|
||||
rows = []
|
||||
for tag, h, E, cite in direct_hopyos():
|
||||
head = {"이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "갈래": "단가산출"}
|
||||
theirs = hb.sources(cite) if cite else []
|
||||
trades = sorted(t for t in E["노임"] if t)
|
||||
with_cite = {**head, "근거": {"출처": theirs, "직종": trades}} if theirs else head
|
||||
m0 = hm.match_line(head, [], {}, True, got)
|
||||
m1 = hm.match_line(with_cite, [], {}, True, got)
|
||||
|
||||
def top(m):
|
||||
if m["상태"] == "같음":
|
||||
return m["대상"].get("로직")
|
||||
return next((c.get("로직") for c in m.get("후보") or [] if c.get("로직")), None)
|
||||
|
||||
base = SV.merged(with_cite, got)
|
||||
sol = SV.solve([{"제목": "", "노임": E["노임"], "장비": E["장비"]}], with_cite, got, base)
|
||||
chosen = SV.codes(sol)
|
||||
sol0 = SV.solve([{"제목": "", "노임": E["노임"], "장비": E["장비"]}], head, got, SV.merged(head, got))
|
||||
chosen0 = SV.codes(sol0)
|
||||
f1 = sol["고름"][0][2] if sol["고름"] else 0
|
||||
pair = (0.0, ())
|
||||
if not chosen or (f1 or 0) < 0.85:
|
||||
pair = pair_fit(E, SV.pool_for(E, base, [])[:20])
|
||||
truth = HUMAN.get((tag, h["번"])) or (cite_truth(theirs, E) if theirs else set())
|
||||
rows.append(
|
||||
{
|
||||
"파일": tag,
|
||||
"호표": h["번"],
|
||||
"이름": f"{h['이름']} {h['규격']}".strip(),
|
||||
"E": E,
|
||||
"인용": cite,
|
||||
"정답": sorted(truth),
|
||||
"정답출처": "사람" if (tag, h["번"]) in HUMAN else ("인용" if theirs else ""),
|
||||
"이름만": top(m0),
|
||||
"이름+인용": top(m1),
|
||||
"벡터": chosen,
|
||||
"벡터(인용 없이)": chosen0,
|
||||
"벡터맞음": f1,
|
||||
"짝": pair,
|
||||
}
|
||||
)
|
||||
SV.V.save_cache()
|
||||
(HERE / "direct.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1, default=str), "utf-8")
|
||||
judged = [r for r in rows if r["정답"]]
|
||||
for key in ("이름만", "이름+인용"):
|
||||
ok = sum(1 for r in judged if r[key] in r["정답"])
|
||||
print(f"{key}: {ok}/{len(judged)}")
|
||||
ok = sum(1 for r in judged if r["벡터"] and r["벡터"][0] in r["정답"])
|
||||
print(f"벡터 풀이 1순위: {ok}/{len(judged)}")
|
||||
import c04_closed as CC # 느슨 = 같은 일 다른 책(정리 이름 글자 묶음 겹침)
|
||||
|
||||
def loose(c, truth):
|
||||
return bool(c) and (c in truth or any(CC.same_work(c, t) for t in truth))
|
||||
|
||||
for key in ("이름만", "이름+인용"):
|
||||
print(f"{key} 느슨: {sum(1 for r in judged if loose(r[key], r['정답']))}/{len(judged)}")
|
||||
print(f"벡터 느슨: {sum(1 for r in judged if r['벡터'] and loose(r['벡터'][0], r['정답']))}/{len(judged)}")
|
||||
v0 = [r for r in judged if r["벡터(인용 없이)"]]
|
||||
print(f"벡터(인용 없이) 엄격: {sum(1 for r in judged if r['벡터(인용 없이)'] and r['벡터(인용 없이)'][0] in r['정답'])}/{len(judged)} · 느슨 {sum(1 for r in v0 if loose(r['벡터(인용 없이)'][0], r['정답']))}/{len(judged)}")
|
||||
nojudge = [r for r in rows if not r["정답"]]
|
||||
print(f"정답 없는 직접 줄 호표 {len(nojudge)} — 벡터 고른 것 있음 {sum(1 for r in nojudge if r['벡터'])} · 맞음 0.85 넘음 {sum(1 for r in nojudge if (r['벡터맞음'] or 0) >= 0.85)}")
|
||||
both = sum(1 for r in judged if (r["벡터"] and r["벡터"][0] in r["정답"]) or r["이름+인용"] in r["정답"])
|
||||
print(f"둘 중 하나: {both}/{len(judged)}")
|
||||
for r in rows:
|
||||
mark = lambda c: ("O" if c in r["정답"] else "x") if r["정답"] else "?" # noqa: E731
|
||||
v = r["벡터"][0] if r["벡터"] else None
|
||||
print(
|
||||
f"{r['파일']} {r['호표']} {r['이름'][:22]:<22} [{r['정답출처']}] 이름만 {mark(r['이름만'])} {names.get(r['이름만'], '')[:12]:<12} | "
|
||||
f"+인용 {mark(r['이름+인용'])} {names.get(r['이름+인용'], '')[:12]:<12} | 벡터 {mark(v)} {names.get(v, '')[:12]:<12} {r['벡터맞음']} | 짝 {r['짝'][1] if r['짝'][0] else ''}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,206 @@
|
||||
"""req226 C5 — 정답 없는 꼴(거창 · STmate 다섯) 호표 조합을 아래에서 위로 풀고, 일위대가조합.json 37 조합의
|
||||
「실무 호표 — 봉화 B00005 …」 출처 표시를 사람 판정(짐작)으로 삼아 견줌.
|
||||
|
||||
우리 = (지금) 줄마다 지금 맞춤 1순위 모음(노임 · 장비 줄도 지금처럼 로직 줄로) /
|
||||
(조합 풀이) 단산 줄 = 묶음 ① 1순위 + 조합 풀이(기계 몫 뺌 · 섞인 줄 1순위 뺌) ·
|
||||
직접 노임 · 장비 줄 = c05 벡터 풀이(호표 이름 + 인용) · 호표 참조 = 그 호표 풀이(아래에서 위로).
|
||||
조합 정답 = 그 호표를 출처로 적은 조합의 로직(출처 하나뿐인 조합 = 통째 견줌 · 여럿 = 고른 것 맞음만).
|
||||
|
||||
python c10_others.py [끔] → 표 · others.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225" / "C1"
|
||||
for p in (HERE, C1):
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import c05_solve as SV # noqa: E402
|
||||
from c01_trees import ROOT # noqa: E402
|
||||
from s04_eval import clause_prefix, hb, hm, patched # noqa: E402
|
||||
from s10_all import candidates_nofold, no_seeds # noqa: E402
|
||||
from s16_guard import typo_guard # noqa: E402
|
||||
|
||||
TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8"))
|
||||
FILES = ("거창", "봉화", "동부청영월", "울진대흥", "영덕계류", "울진소광")
|
||||
ALIAS = {"봉화": "봉화", "울진대흥": "울진대흥", "영월": "동부청영월", "계류보전": "영덕계류", "울진소광": "울진소광", "거창": "거창"}
|
||||
COMBOS = json.loads((ROOT / "resources/master_data/일위대가조합.json").read_text(encoding="utf-8"))["줄"]
|
||||
|
||||
|
||||
def combo_truth() -> dict:
|
||||
"""{(파일, 호표 열쇠): [(조합 키, {로직}, 출처 수)]} — 호표 열쇠 = STmate B 코드 · 거창 번."""
|
||||
out: dict = {}
|
||||
for r in COMBOS:
|
||||
note = str(r.get("비고") or "")
|
||||
m = re.search(r"실무 호표 — (.+)$", note, re.M)
|
||||
if not m:
|
||||
continue
|
||||
srcs = [s.strip() for s in m.group(1).split("·")]
|
||||
codes = {x["로직"] for x in r["담은로직"]}
|
||||
skip = tuple(
|
||||
re.sub(r"\s+", "", ln.split(" — ")[0])
|
||||
for ln in note.splitlines()
|
||||
if " — " in ln and ("우리 로직에 없음" in ln or "이름이 달라 뺌" in ln)
|
||||
)
|
||||
for s in srcs:
|
||||
hit = re.match(r"(\S+)\s+(B\d{5}|제(\d+)호표)", s)
|
||||
if not hit or hit.group(1) not in ALIAS:
|
||||
continue
|
||||
key = (ALIAS[hit.group(1)], int(hit.group(3)) if hit.group(3) else hit.group(2))
|
||||
out.setdefault(key, []).append((r["키"], codes, len(srcs), skip))
|
||||
return out
|
||||
|
||||
|
||||
def line_dict(one: dict) -> dict:
|
||||
return {k: one.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")}
|
||||
|
||||
|
||||
def as_logic_line(one: dict) -> dict:
|
||||
"""지금 읽기 갈래로 — 노임 줄 = 기타 · 장비 = 기계 · 단산 = 단가산출."""
|
||||
kind = {"단산": "단가산출", "노임": "기타", "장비": "기계"}.get(one["갈래"], one["갈래"])
|
||||
return {**line_dict(one), "갈래": kind}
|
||||
|
||||
|
||||
def first(m: dict) -> str | None:
|
||||
if m["상태"] == "같음":
|
||||
return (m.get("대상") or {}).get("로직")
|
||||
return next((c.get("로직") for c in m.get("후보") or [] if c.get("로직")), None)
|
||||
|
||||
|
||||
class Solver:
|
||||
def __init__(self, mode: str, got: dict):
|
||||
self.mode, self.got = mode, got
|
||||
self.memo: dict = {}
|
||||
|
||||
def hopyo_set(self, f: dict, no, trail=(), skip: tuple = ()) -> set:
|
||||
"""skip = 사람이 조합에서 뺀 줄 글(「우리 로직에 없음」 · 「이름이 달라 뺌」) — 그 줄은 안 셈."""
|
||||
key = (f["파일"], no, skip)
|
||||
if key in self.memo:
|
||||
return self.memo[key]
|
||||
h = next((x for x in f["호표"] if x["번"] == no), None)
|
||||
if h is None or key in trail:
|
||||
return set()
|
||||
out: set = set()
|
||||
direct = {"노임": {}, "장비": {}}
|
||||
flat = lambda t: re.sub(r"\s+", "", str(t)) # noqa: E731
|
||||
for one in h["줄"]:
|
||||
kind = one["갈래"]
|
||||
text = flat(f"{one['이름']}{one['규격']}")
|
||||
if skip and any(text and (text in x or x in text) for x in skip):
|
||||
continue
|
||||
if kind == "호표":
|
||||
out |= self.hopyo_set(f, (one.get("참조") or {}).get("번"), (*trail, key), skip)
|
||||
elif kind in ("단산", "노임", "장비") and self.mode == "지금":
|
||||
t = first(hm.match_line(as_logic_line(one), [], {}, True, self.got))
|
||||
if t:
|
||||
out.add(t)
|
||||
elif kind == "단산":
|
||||
line = as_logic_line(one)
|
||||
primary = typo_guard(line, self.got, {"단산글": one.get("단산글") or []})["top"]
|
||||
primary = primary[0] if primary else None
|
||||
parts = one.get("몫") or []
|
||||
chosen = SV.combine(primary, parts, line, self.got, SV.merged(line, self.got))
|
||||
codes = [c for c, *_ in chosen if c and c not in SV.MACHINE_SET]
|
||||
if parts and SV.is_mixed(parts, line) and primary and SV.cover(SV._sum(parts), primary) < 0.35:
|
||||
rest = [c for c, w, ff, x, st in chosen if c != primary and st in ("몫", "절")]
|
||||
if rest:
|
||||
codes = [c for c in codes if c != primary]
|
||||
out |= set(codes)
|
||||
elif kind in ("노임", "장비"):
|
||||
for k in ("노임", "장비"):
|
||||
for d, q in (one.get(k) or {}).items():
|
||||
if d and q:
|
||||
direct[k][d] = direct[k].get(d, 0) + float(q)
|
||||
if self.mode != "지금" and (direct["노임"] or direct["장비"]):
|
||||
cite = " ".join(f"{o['이름']} {o['규격']}" for o in h["줄"] if o["갈래"] == "근거글")
|
||||
head = {"이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "갈래": "단가산출"}
|
||||
theirs = hb.sources(cite) if cite else []
|
||||
if theirs:
|
||||
head["근거"] = {"출처": theirs, "직종": sorted(direct["노임"])}
|
||||
sol = SV.solve([{"제목": "", **direct}], head, self.got, SV.merged(head, self.got))
|
||||
out |= {c for c in SV.codes(sol) if c not in SV.MACHINE_SET}
|
||||
self.memo[key] = out
|
||||
return out
|
||||
|
||||
|
||||
def run(mode: str) -> list:
|
||||
truth = combo_truth()
|
||||
rows = []
|
||||
with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold):
|
||||
hm._targets.cache_clear()
|
||||
hb._ours.cache_clear()
|
||||
got = hm.targets()
|
||||
for f in TREES:
|
||||
if f["파일"] not in FILES:
|
||||
continue
|
||||
sv = Solver(mode, got)
|
||||
for h in f["호표"]:
|
||||
hkey = h["번"] if f["파일"] == "거창" else h.get("코드")
|
||||
for ck, codes, nsrc, skip in truth.get((f["파일"], hkey), []):
|
||||
ours = sv.hopyo_set(f, h["번"])
|
||||
kept = sv.hopyo_set(f, h["번"], (), skip)
|
||||
rows.append(
|
||||
{
|
||||
"파일": f["파일"],
|
||||
"호표": h["번"],
|
||||
"이름": f"{h['이름']} {h['규격']}".strip(),
|
||||
"조합": ck,
|
||||
"출처수": nsrc,
|
||||
"정답": sorted(codes),
|
||||
"우리": sorted(ours),
|
||||
"우리(사람이 뺀 줄 뺌)": sorted(kept),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def report(rows: list, title: str) -> dict:
|
||||
one = [r for r in rows if r["출처수"] == 1]
|
||||
many = [r for r in rows if r["출처수"] > 1]
|
||||
|
||||
def stats(rs, exact: bool, col: str = "우리"):
|
||||
if not rs:
|
||||
return "0"
|
||||
ex = sum(1 for r in rs if set(r[col]) == set(r["정답"]))
|
||||
rec = sum(len(set(r[col]) & set(r["정답"])) / len(r["정답"]) for r in rs) / len(rs)
|
||||
pre = sum((len(set(r[col]) & set(r["정답"])) / len(r[col])) if r[col] else 0 for r in rs) / len(rs)
|
||||
head = f"통째 {ex}/{len(rs)} · " if exact else ""
|
||||
return f"{head}조합 로직 찾음 {round(100 * rec, 1)}% · 고른 것 맞음 {round(100 * pre, 1)}%"
|
||||
|
||||
print(f"== {title}")
|
||||
print(f" 출처 하나 조합(호표 {len(one)}): {stats(one, True)}")
|
||||
print(f" └ 사람이 뺀 줄 빼고: {stats(one, True, '우리(사람이 뺀 줄 뺌)')}")
|
||||
print(f" 출처 여럿 조합(호표 {len(many)}): {stats(many, False)}")
|
||||
print(f" └ 사람이 뺀 줄 빼고: {stats(many, False, '우리(사람이 뺀 줄 뺌)')}")
|
||||
return {"하나": stats(one, True), "하나(뺀 줄 뺌)": stats(one, True, "우리(사람이 뺀 줄 뺌)"), "여럿": stats(many, False), "여럿(뺀 줄 뺌)": stats(many, False, "우리(사람이 뺀 줄 뺌)")}
|
||||
|
||||
|
||||
def main():
|
||||
seeds_off = sys.argv[1:] == ["끔"]
|
||||
out = {}
|
||||
for mode in ("지금", "조합 풀이"):
|
||||
if seeds_off:
|
||||
with no_seeds():
|
||||
rows = run(mode)
|
||||
else:
|
||||
rows = run(mode)
|
||||
out[mode] = {"줄": rows, "표": report(rows, f"{mode}{' (씨앗 끔)' if seeds_off else ''}")}
|
||||
SV.V.save_cache()
|
||||
name = "others_off.json" if seeds_off else "others.json"
|
||||
(HERE / name).write_text(json.dumps(out, ensure_ascii=False, indent=1), "utf-8")
|
||||
names = {x["로직"]: x["이름"] for x in hm.targets()["로직"]}
|
||||
for r in out["조합 풀이"]["줄"]:
|
||||
if r["출처수"] == 1:
|
||||
miss = [names.get(c, c)[:10] for c in set(r["정답"]) - set(r["우리(사람이 뺀 줄 뺌)"])]
|
||||
extra = [names.get(c, c)[:10] for c in set(r["우리(사람이 뺀 줄 뺌)"]) - set(r["정답"])]
|
||||
print(f" {r['파일']} {r['호표']} {r['이름'][:16]:<16} {r['조합']} 빠짐 {miss} 남음 {extra}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,19 @@
|
||||
import c07_hopyo as H
|
||||
from c07_hopyo import SV, S, FINAL, CLOSED, BEST, SOLVED
|
||||
|
||||
def sets_B(i): # (가) 1순위 + 조합 덧(기계 몫 뺌) · 뺌 없음
|
||||
top = (CLOSED["묶음①"][BEST][i]["top"] or [None])[0]
|
||||
extra = {c for c, *_ in SOLVED[i]["고름"] if c and c not in SV.MACHINE_SET}
|
||||
return ({top} | extra) - {None}
|
||||
|
||||
def sets_C(i): # 뺌만(섞인 줄 1순위 바꿈) · 덧 없음
|
||||
top = (FINAL["tops"][i]["top"] or [None])[0]
|
||||
return {top} - {None}
|
||||
|
||||
for name, fn in (("B 덧만", sets_B), ("C 뺌만", sets_C)):
|
||||
H.chosen_of = lambda i, pick, fn=fn: fn(i)
|
||||
res = {k: H.judge(k, "x", False) for k in H.HOPYO}
|
||||
res = {k: v for k, v in res.items() if v}
|
||||
keys = [k for k, v in res.items() if not v["없음칸"]]
|
||||
ex = sum(res[k]["엄격"]["통째"] for k in keys)
|
||||
print(name, f"덮을 수 있음 통째 엄격 {ex}/{len(keys)} = {round(100*ex/len(keys),1)}", "칸", round(100*sum(res[k]["엄격"]["칸"] for k in keys)/len(keys),1))
|
||||
@@ -0,0 +1,26 @@
|
||||
import sys, collections
|
||||
from c07_hopyo import *
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
names = {x["로직"]: x["이름"] for x in hm.targets()["로직"]}
|
||||
pick = sys.argv[1] if len(sys.argv) > 1 else "조합"
|
||||
t2 = len(sys.argv) > 2
|
||||
miss_c, extra_c = collections.Counter(), collections.Counter()
|
||||
n = 0
|
||||
for key in HOPYO:
|
||||
ids = gather(key)
|
||||
if not ids: continue
|
||||
r = judge(key, pick, t2)
|
||||
if r["없음칸"] or r["엄격"]["통째"]: continue
|
||||
n += 1
|
||||
slots, chosen = [], set()
|
||||
for i in ids:
|
||||
for s in slots_of(i, t2): slots.append((i, s))
|
||||
chosen |= chosen_of(i, pick)
|
||||
alts = set().union(*[s[0] for _, s in slots])
|
||||
miss = [(i, sorted(s[0])) for i, s in slots if not chosen & s[0]]
|
||||
extra = chosen - alts
|
||||
for i, _ in miss: miss_c[i] += 1
|
||||
for e in extra: extra_c[e] += 1
|
||||
print("실패 호표", n)
|
||||
print("빠진 칸(줄 id) 많은 차례", [(i, S.BY[i]["이름"][:12], c) for i, c in miss_c.most_common(20)])
|
||||
print("남는 것 많은 차례", [(e, names.get(e, "")[:14], c) for e, c in extra_c.most_common(20)])
|
||||
@@ -0,0 +1,15 @@
|
||||
"""품 식 풀이 실패(금액 ÷ 노임으로 되찾음) 셈 — 북부청 · 거창 블록."""
|
||||
import c01_trees as T
|
||||
cnt = {"식": 0, "되찾음": 0}
|
||||
orig = T.qty_of
|
||||
def wrapped(expr):
|
||||
v = orig(expr)
|
||||
cnt["식" if v is not None else "되찾음"] += 1
|
||||
return v
|
||||
T.qty_of = wrapped
|
||||
ids = T._ids_north()
|
||||
for t, p in T.NORTH.items():
|
||||
T.read_excel(t, p, ids, False)
|
||||
for t, p in T.GEO.items():
|
||||
T.read_excel(t, p, {}, True)
|
||||
print(cnt)
|
||||
@@ -0,0 +1,33 @@
|
||||
"""씨앗 끔 바탕으로 같은 셈(줄 지표 · 호표 조합 지표) — 저장 파일 안 덮음."""
|
||||
import json
|
||||
import c06_lines as L6
|
||||
import c07_hopyo as H
|
||||
from c07_hopyo import SV, S
|
||||
from c06_lines import CLOSED, BEST, PARTS
|
||||
|
||||
off = L6.run("묶음①(씨앗 끔)", BEST)
|
||||
base = CLOSED["묶음①(씨앗 끔)"][BEST]
|
||||
tops, sets = {}, {}
|
||||
for i, got in base.items():
|
||||
chosen = list(dict.fromkeys(c for c, *_ in off[i]["고름"] if c))
|
||||
top = list(got["top"])
|
||||
parts = PARTS.get(i) or []
|
||||
if top and len(chosen) > 1 and parts and SV.is_mixed(parts, S.BY[i]) and SV.cover(SV._sum(parts), top[0]) < 0.35:
|
||||
labor = [c for c, w, f, x, st in off[i]["고름"] if c != top[0] and st in ("몫", "절")]
|
||||
if labor:
|
||||
chosen = [c for c in chosen if c != top[0]]
|
||||
top = [labor[0]] + [c for c in top if c != labor[0]][:4]
|
||||
tops[i] = {"상태": got["상태"], "top": top}
|
||||
sets[i] = chosen
|
||||
S.show(S.table("씨앗 끔 (가)", base))
|
||||
S.show(S.table("씨앗 끔 (가)+조합 뺌", tops))
|
||||
for name, fn in (
|
||||
("(가)", lambda i: {(base[i]["top"] or [None])[0]} - {None}),
|
||||
("(가)+조합 풀이+뺌-기계", lambda i: {c for c in sets[i] if c and (c == (tops[i]["top"] or [None])[0] or c not in SV.MACHINE_SET)}),
|
||||
):
|
||||
H.chosen_of = lambda i, pick, fn=fn: fn(i)
|
||||
res = {k: H.judge(k, "x", False) for k in H.HOPYO}
|
||||
res = {k: v for k, v in res.items() if v}
|
||||
keys = [k for k, v in res.items() if not v["없음칸"]]
|
||||
ex = sum(res[k]["엄격"]["통째"] for k in keys)
|
||||
print(name, f"T1 덮을 수 있음 통째 엄격 {ex}/{len(keys)} = {round(100*ex/len(keys),1)} · 칸 {round(100*sum(res[k]['엄격']['칸'] for k in keys)/len(keys),1)}")
|
||||
@@ -0,0 +1,18 @@
|
||||
import time, itertools
|
||||
import c09_direct as D
|
||||
from c09_direct import SV, hm, hb
|
||||
got = hm.targets()
|
||||
t = time.time()
|
||||
for k, (tag, h, E, cite) in enumerate(D.direct_hopyos()):
|
||||
if time.time() - t > 90: break
|
||||
t0 = time.time()
|
||||
head = {"이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "갈래": "단가산출"}
|
||||
theirs = hb.sources(cite) if cite else []
|
||||
with_cite = {**head, "근거": {"출처": theirs, "직종": sorted(E["노임"])}} if theirs else head
|
||||
m0 = hm.match_line(head, [], {}, True, got); t1 = time.time()
|
||||
base = SV.merged(with_cite, got); t2 = time.time()
|
||||
sol = SV.solve([{"제목": "", "노임": E["노임"], "장비": E["장비"]}], with_cite, got, base); t3 = time.time()
|
||||
pool = SV.pool_for(E, base, [])
|
||||
pr = D.pair_fit(E, pool[:20]); t4 = time.time()
|
||||
tr = D.cite_truth(theirs, E) if theirs else set(); t5 = time.time()
|
||||
print(tag, h["번"], "match", round(t1-t0,2), "merged", round(t2-t1,2), "solve", round(t3-t2,2), "pool", len(pool), "pair", round(t4-t3,2), "truth", round(t5-t4,2))
|
||||
@@ -0,0 +1,7 @@
|
||||
import json
|
||||
from c06_lines import CLOSED, BEST
|
||||
import s10_all as S
|
||||
F = json.load(open("final.json", encoding="utf-8"))
|
||||
for name, res in (("지금", CLOSED["지금"][""]), ("묶음①", CLOSED["묶음①"][""]), ("(가)", CLOSED["묶음①"][BEST]), ("(가)+조합 뺌", F["tops"])):
|
||||
t = S.table(name, res)["전체"]
|
||||
print(name, "고유", t["1위 엄격"], t["1위 느슨"], t["5안 엄격"], t["5안 느슨"], "| 엑셀 줄 무게 1위 느슨", t["줄 1위 느슨"], "5안 느슨", t["줄 5안 느슨"])
|
||||
@@ -0,0 +1,18 @@
|
||||
import json
|
||||
from c06_lines import PARTS, FINER, COMPOSITE
|
||||
import s10_all as S
|
||||
import c05_solve as SV
|
||||
t = json.load(open("trees.json", encoding="utf-8"))
|
||||
for f in t:
|
||||
d = [l for h in f["호표"] for l in h["줄"] if l["갈래"] == "단산"]
|
||||
src = sum(1 for l in d if (l.get("근거") or {}).get("출처"))
|
||||
trd = sum(1 for l in d if (l.get("근거") or {}).get("직종"))
|
||||
vec = sum(1 for l in d if l.get("몫"))
|
||||
multi = sum(1 for l in d if len({p.get("절") or p.get("제목") for p in l.get("몫") or []}) > 1)
|
||||
print(f["파일"], "단산 줄", len(d), "출처", src, "직종", trd, "몫 벡터", vec, "절 둘 넘음", multi)
|
||||
rows = lambda ids: sum(S.BY[i]["줄수"] for i in ids)
|
||||
mixed = sorted(i for i, p in PARTS.items() if S.LAB.get(i, {}).get("정답") and SV.is_mixed(p, S.BY[i]))
|
||||
print("철근 줄", FINER, rows(FINER), "섞인", mixed, rows(mixed))
|
||||
F = json.load(open("final.json", encoding="utf-8"))
|
||||
ok = [i for i in FINER if COMPOSITE[i] <= set(F["sets"][i])]
|
||||
print("철근 풂", ok, rows(ok))
|
||||
@@ -0,0 +1,23 @@
|
||||
import json, sys, time
|
||||
import c03_vec as V
|
||||
import c01_trees # noqa: F401
|
||||
from c01_trees import ROOT
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
t=json.load(open('trees.json',encoding='utf-8'))
|
||||
names={x['로직']:x['이름'] for x in hm.targets()['로직']}
|
||||
want={'N000','N003','N005','S062','S066','N039','N008','N047'}
|
||||
t0=time.time()
|
||||
for f in t:
|
||||
for h in f['호표']:
|
||||
for l in h['줄']:
|
||||
if l.get('id') in want:
|
||||
want.discard(l['id'])
|
||||
print('##', l['id'], l['이름'], l['규격'])
|
||||
for s in l.get('몫') or []:
|
||||
pool=set().union(*[V.IDX.get(d,set()) for d in V.dims(s)]) if V.dims(s) else set()
|
||||
sc=sorted(((V.fit(s,c),c) for c in pool), key=lambda z:-z[0][0])[:4]
|
||||
print(' 몫', s['제목'][:20], {k:round(v,3) for k,v in s['노임'].items()}, {k:round(v,3) for k,v in s['장비'].items()}, 'pool', len(pool))
|
||||
for (sc_,g,x),c in sc:
|
||||
print(' ', round(sc_,3), c, names.get(c), g[:60], x)
|
||||
V.save_cache()
|
||||
print('sec', round(time.time()-t0,1))
|
||||
@@ -0,0 +1,26 @@
|
||||
"""시트 이름 · 크기만 훑음(읽기만)."""
|
||||
import sys, warnings
|
||||
from pathlib import Path
|
||||
import openpyxl
|
||||
warnings.filterwarnings("ignore")
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발\resources\knowledge\original\실무문서")
|
||||
FILES = [
|
||||
"01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx",
|
||||
"04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx",
|
||||
"05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
|
||||
"2024년 간선임도(기번3-울진.대흥)(공통)/2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx",
|
||||
"20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx",
|
||||
"25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
|
||||
]
|
||||
if __name__ == "__main__":
|
||||
for f in FILES:
|
||||
p = ROOT / f
|
||||
head = p.read_bytes()[:8]
|
||||
print("##", f, head[:4])
|
||||
try:
|
||||
b = openpyxl.load_workbook(p, read_only=True, data_only=True)
|
||||
except Exception as e:
|
||||
print(" fail", type(e).__name__, str(e)[:80]); continue
|
||||
for ws in b.worksheets:
|
||||
print(" ", repr(ws.title), ws.max_row, ws.max_column, ws.sheet_state)
|
||||
b.close()
|
||||
@@ -0,0 +1,14 @@
|
||||
"""시트 앞 몇 줄 보기(읽기만) — python x01_peek.py <파일 번호> <시트> <시작> <끝>"""
|
||||
import sys, warnings
|
||||
from pathlib import Path
|
||||
import openpyxl
|
||||
warnings.filterwarnings("ignore")
|
||||
from x00_sheets import FILES, ROOT
|
||||
i, sheet, a, b = int(sys.argv[1]), sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
|
||||
book = openpyxl.load_workbook(ROOT / FILES[i], read_only=True, data_only=True)
|
||||
ws = book[sheet]
|
||||
for at, row in enumerate(ws.iter_rows(values_only=True), 1):
|
||||
if at < a: continue
|
||||
if at > b: break
|
||||
cells = [(c, v) for c, v in enumerate(row) if v not in (None, "")]
|
||||
print(at, " | ".join(f"{c}:{str(v)[:40]}" for c, v in cells))
|
||||
@@ -0,0 +1,21 @@
|
||||
"""STmate 일위대가표 줄 코드 갈래 셈(읽기만)."""
|
||||
import re, warnings, collections
|
||||
import openpyxl
|
||||
from x00_sheets import FILES, ROOT
|
||||
warnings.filterwarnings("ignore")
|
||||
for i in range(1, 6):
|
||||
book = openpyxl.load_workbook(ROOT / FILES[i], read_only=True, data_only=True)
|
||||
rows = [list(r) for r in book["일위대가표"].iter_rows(values_only=True)]
|
||||
book.close()
|
||||
c = collections.Counter(); ex = {}
|
||||
for r in rows:
|
||||
code = str(r[12] or "").strip() if len(r) > 12 else ""
|
||||
if not code: continue
|
||||
m = re.match(r"^(별산)?([A-Z])\d", code)
|
||||
k = (m.group(1) or "") + m.group(2) if m else "?" + code[:6]
|
||||
c[k] += 1
|
||||
ex.setdefault(k, []).append((str(r[0])[:20], str(r[1])[:20], r[3], code))
|
||||
print(FILES[i][:30], dict(c))
|
||||
for k, v in ex.items():
|
||||
if k[-1] in "B%S?" or k.startswith("?"):
|
||||
print(" ", k, v[:4])
|
||||
@@ -0,0 +1,20 @@
|
||||
"""C6 — 실무 xlsx 시트 이름 · 크기 훑기(읽기만)."""
|
||||
import sys, warnings, time
|
||||
from pathlib import Path
|
||||
import openpyxl
|
||||
warnings.filterwarnings("ignore")
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
BASE = ROOT / "resources/knowledge/original/실무문서"
|
||||
for p in sorted(BASE.rglob("*.xlsx")):
|
||||
if "수량" in p.name and "내역" not in p.name:
|
||||
continue
|
||||
t = time.time()
|
||||
head = p.read_bytes()[:8]
|
||||
try:
|
||||
wb = openpyxl.load_workbook(p, read_only=True, data_only=True)
|
||||
info = [(ws.title, ws.max_row, ws.max_column) for ws in wb.worksheets]
|
||||
wb.close()
|
||||
except Exception as e:
|
||||
info = f"ERR {type(e).__name__}: {e} head={head!r}"
|
||||
print(p.relative_to(BASE), round(time.time()-t,1))
|
||||
print(" ", info)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""C6 — STmate 꼴 시트 줄 꼴 엿보기(읽기만). python s01_peek.py <파일번호> <시트> <시작> <끝>"""
|
||||
import sys, warnings
|
||||
from pathlib import Path
|
||||
import openpyxl
|
||||
warnings.filterwarnings("ignore")
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
B = ROOT / "resources/knowledge/original/실무문서"
|
||||
FILES = {
|
||||
"봉화": B / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx",
|
||||
"영월": B / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
|
||||
"대흥설계": B / "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx",
|
||||
"대흥착공": B / "2024년 간선임도(기번3-울진.대흥)(공통)/2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx",
|
||||
"계류보전": B / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx",
|
||||
"소광": B / "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
|
||||
"거창": B / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx",
|
||||
}
|
||||
if __name__ == "__main__":
|
||||
tag, sheet, a, b = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
|
||||
wb = openpyxl.load_workbook(FILES[tag], read_only=True, data_only=True)
|
||||
for i, r in enumerate(wb[sheet].iter_rows(values_only=True), 1):
|
||||
if i < a: continue
|
||||
if i > b: break
|
||||
cells = [(j, v) for j, v in enumerate(r) if v not in (None, "")]
|
||||
print(i, cells)
|
||||
@@ -0,0 +1,210 @@
|
||||
"""C6 — 실무 내역 넓히기: STmate 꼴 6 파일 + 거창(엑셀꼴) 읽기 → data_<태그>.json (읽기만).
|
||||
|
||||
STmate: 일위대가표(호표 · 줄 코드 D L M X B S W) + 단가산출근거(블록 = BDCOD|D 코드 · 자원 줄 = 12열 「L00002_1」 ·
|
||||
7열 = 단위당 품(계수) · 23열 = 「이름 / 규격」) + 노무비목록표(L 코드 → 직종 이름 · 23열과 맞대 봄).
|
||||
거창: 저장소 읽기 함수(hx.read_book · hb 블록) 그대로 — 「단산N참조」 줄만 블록이 있음.
|
||||
북부청 두 파일은 C1 결과(out_items · out_lines)를 씀(정답 L 과 같은 id).
|
||||
|
||||
python s02_read.py → 파일마다 호표 · 줄 · 블록 수 표 + data_<태그>.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import openpyxl
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
|
||||
|
||||
B = ROOT / "resources/knowledge/original/실무문서"
|
||||
STMATE = {
|
||||
"봉화": B / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx",
|
||||
"영월": B / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
|
||||
"대흥설계": B / "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx",
|
||||
"대흥착공": B / "2024년 간선임도(기번3-울진.대흥)(공통)/2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx",
|
||||
"계류보전": B / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx",
|
||||
"소광": B / "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
|
||||
}
|
||||
EXCEL = {"거창": B / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx"}
|
||||
KIND = {"D": "단가산출", "X": "기계", "L": "노임", "M": "자재", "B": "호표", "S": "별도계상", "W": "일식"}
|
||||
_HOPYO = re.compile(r"제\s*(\d+)\s*호표")
|
||||
_LINE = re.compile(r"^([A-Z])(\d{5})_\d+$")
|
||||
_COD = re.compile(r"COD\|([A-Z]\d{5})")
|
||||
_BLOCK = re.compile(r"BDCOD\|(D\d{5})")
|
||||
_HEADB = re.compile(r"BDCOD\|(B\d{5})")
|
||||
|
||||
|
||||
def _num(v):
|
||||
try:
|
||||
return float(str(v).replace(",", ""))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _s(v):
|
||||
return str(v if v is not None else "").strip()
|
||||
|
||||
|
||||
def _cell(r, i):
|
||||
return r[i] if i < len(r) else None
|
||||
|
||||
|
||||
def read_stmate(tag, path):
|
||||
book = openpyxl.load_workbook(path, read_only=True, data_only=True)
|
||||
rows = [list(r) for r in book["일위대가표"].iter_rows(values_only=True)]
|
||||
calc = [list(r) for r in book["단가산출근거"].iter_rows(values_only=True)]
|
||||
labor_rows = [list(r) for r in book["노무비목록표"].iter_rows(values_only=True)]
|
||||
list_rows = [list(r) for r in book["단가산출근거목록표"].iter_rows(values_only=True)]
|
||||
book.close()
|
||||
# 단가산출근거목록표 — D 코드 → 이름 · 규격 · 단위(블록 머리 글은 이름 · 규격이 붙어 있음)
|
||||
heads = {}
|
||||
for r in list_rows:
|
||||
hit = re.search(r"COD\|(D\d{5})", " ".join(_s(v) for v in r))
|
||||
if hit and _s(_cell(r, 1)):
|
||||
heads[hit.group(1)] = {"이름": _s(_cell(r, 1)), "규격": _s(_cell(r, 2)), "단위": _s(_cell(r, 3))}
|
||||
# 노무비목록표 — L 코드 → 이름(열 7 코드 · 열 1 이름)
|
||||
labor = {}
|
||||
for r in labor_rows:
|
||||
hit, name = re.search(r"COD\|(L\d{5})", " ".join(_s(v) for v in r)), _s(_cell(r, 1))
|
||||
if hit and name:
|
||||
labor[hit.group(1)] = name
|
||||
# 일위대가표
|
||||
hopyos, cur, need_head = [], None, False
|
||||
for r in rows:
|
||||
first = _s(_cell(r, 0))
|
||||
hit = _HOPYO.match(first)
|
||||
if hit:
|
||||
bcode = _HEADB.search(_s(_cell(r, 13)))
|
||||
cur = {"번": int(hit.group(1)), "코드": bcode.group(1) if bcode else None, "줄": []}
|
||||
hopyos.append(cur)
|
||||
need_head = True
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
if need_head and first:
|
||||
cur.update(이름=first, 규격=_s(_cell(r, 1)), 단위=_s(_cell(r, 3)))
|
||||
need_head = False
|
||||
continue
|
||||
if first.replace(" ", "") == "합계":
|
||||
cur = None
|
||||
continue
|
||||
m = _LINE.match(_s(_cell(r, 14))) or _COD.search(_s(_cell(r, 13)))
|
||||
if not m or not first:
|
||||
continue
|
||||
code = m.group(1) + m.group(2) if m.re is _LINE else m.group(1)
|
||||
cur["줄"].append(
|
||||
{
|
||||
"차례": len(cur["줄"]) + 1,
|
||||
"이름": first,
|
||||
"규격": _s(_cell(r, 1)),
|
||||
"수량": _num(_cell(r, 2)),
|
||||
"단위": _s(_cell(r, 3)),
|
||||
"단가": {"계": _num(_cell(r, 4)), "노무비": _num(_cell(r, 6)), "재료비": _num(_cell(r, 8)), "경비": _num(_cell(r, 10))},
|
||||
"코드": code,
|
||||
"갈래": KIND.get(code[0], "기타"),
|
||||
}
|
||||
)
|
||||
# 단가산출근거 블록
|
||||
blocks, blk, mismatch = {}, None, 0
|
||||
for at, r in enumerate(calc, 1):
|
||||
joined = " ".join(_s(v) for v in r[:8])
|
||||
bm = _BLOCK.search(joined)
|
||||
if bm:
|
||||
blk = blocks.setdefault(bm.group(1), {"코드": bm.group(1), "머리": "", "글": [], "자원": [], "행": [at, at]})
|
||||
continue
|
||||
if blk is None:
|
||||
continue
|
||||
if _s(_cell(r, 0)).replace(" ", "") == "총계":
|
||||
blk = None
|
||||
continue
|
||||
blk["행"][1] = at
|
||||
text = _s(_cell(r, 1))
|
||||
if _s(_cell(r, 12)) == blk["코드"] and not blk["머리"]:
|
||||
blk["머리"] = text
|
||||
continue
|
||||
if text:
|
||||
blk["글"].append(text)
|
||||
lm = _LINE.match(_s(_cell(r, 12)))
|
||||
if lm:
|
||||
letter, code = lm.group(1), lm.group(1) + lm.group(2)
|
||||
name, _, spec = _s(_cell(r, 23)).partition("/")
|
||||
if letter == "L" and labor.get(code) and labor[code].replace(" ", "") != name.replace(" ", ""):
|
||||
mismatch += 1
|
||||
blk["자원"].append({"갈래": letter, "코드": code, "이름": name.strip() or labor.get(code, ""), "규격": spec.strip(), "품": _num(_cell(r, 7)) or 0.0})
|
||||
for code, blk in blocks.items():
|
||||
blk.update(heads.get(code) or {})
|
||||
return {"파일": tag, "꼴": "STmate", "호표": hopyos, "블록": blocks, "노무목록": labor, "이름어긋남": mismatch}
|
||||
|
||||
|
||||
def read_excel(tag, path):
|
||||
data = path.read_bytes()
|
||||
book = hx.read_book(path.name, data)
|
||||
sheets = hx._sheets(path.name, data)
|
||||
calc = sheets.get(hb.SHEET) or []
|
||||
start, col, _ = hb._columns(calc)
|
||||
blocks = {}
|
||||
for b in book["단산"]:
|
||||
text, spread = [], []
|
||||
for at in range(b["행"][0], b["행"][1] + 1):
|
||||
row = calc[at - 1]
|
||||
t = row[col] if col < len(row) else ""
|
||||
if isinstance(t, str) and t.strip():
|
||||
text.append(t.rstrip())
|
||||
# 거창 꼴 — 글이 여러 칸에 흩어짐(한 칸 한 덩이) · 블록 끝 = 다음 머리 앞까지 · 칸을 빈칸으로 이어 붙임
|
||||
nxt = min([x["행"][0] for x in book["단산"] if x["행"][0] > b["행"][0]] or [len(calc) + 1])
|
||||
for at in range(b["행"][0] + 1, nxt):
|
||||
row = calc[at - 1]
|
||||
cells = [str(v).strip() for v in row[col + 1 : 141] if v not in (None, "") and str(v).strip()]
|
||||
if cells:
|
||||
spread.append(" ".join(cells))
|
||||
blocks[f"단산{b['번']}"] = {"코드": f"단산{b['번']}", "머리": f"{b['이름']} [{b['단위']}]", "글": text[1:] if len(text) > 1 else spread, "흩어짐": len(text) <= 1, "자원": [], "행": b["행"], "이름": b["이름"], "규격": "", "단위": b["단위"]}
|
||||
hopyos = []
|
||||
for h in book["호표"]:
|
||||
lines = []
|
||||
for line in h["줄"]:
|
||||
ref = line.get("참조") or {}
|
||||
code = f"단산{ref['번']}" if ref.get("종류") == "단산" else (f"일위{ref['번']}" if ref.get("종류") == "일위" else None)
|
||||
lines.append({k: hx._json(line.get(k)) for k in ("차례", "이름", "규격", "수량", "단위", "단가", "갈래")} | {"코드": code})
|
||||
hopyos.append({"번": h["번"], "코드": f"일위{h['번']}", "이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "줄": lines})
|
||||
return {"파일": tag, "꼴": "엑셀꼴", "호표": hopyos, "블록": blocks, "노무목록": {}, "이름어긋남": 0}
|
||||
|
||||
|
||||
def summary(doc):
|
||||
lines = [x for h in doc["호표"] for x in h["줄"]]
|
||||
kinds = {}
|
||||
for x in lines:
|
||||
kinds[x["갈래"]] = kinds.get(x["갈래"], 0) + 1
|
||||
logic = [x for x in lines if x["갈래"] in ("단가산출", "기계", "기타")]
|
||||
uniq = {(x["이름"], x["규격"], x["단위"], x["코드"]) for x in logic}
|
||||
used = {x["코드"] for x in lines if x["코드"] in doc["블록"]}
|
||||
return {
|
||||
"호표": len(doc["호표"]),
|
||||
"줄": len(lines),
|
||||
"로직 줄": len(logic),
|
||||
"로직 고유": len(uniq),
|
||||
"블록": len(doc["블록"]),
|
||||
"쓰인 블록": len(used),
|
||||
"갈래": kinds,
|
||||
"노무목록": len(doc["노무목록"]),
|
||||
"이름어긋남": doc["이름어긋남"],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for tag, path in {**STMATE, **EXCEL}.items():
|
||||
try:
|
||||
doc = read_stmate(tag, path) if tag in STMATE else read_excel(tag, path)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(tag, "못 읽음", type(e).__name__, e)
|
||||
continue
|
||||
(HERE / f"data_{tag}.json").write_text(json.dumps(doc, ensure_ascii=False, indent=0, default=str), "utf-8")
|
||||
print(tag, summary(doc))
|
||||
@@ -0,0 +1,202 @@
|
||||
"""C6 — 평가 단위(고유 줄) 만들기 + 블록 속(출처 · 직종 · 품 · 장비) 뽑기 → items.json · hopyo.json.
|
||||
|
||||
단위 = 파일마다 「로직 줄」 고유(이름 · 규격 · 단위 · 코드) — STmate 는 호표에 안 쓰인 D 블록(내역서가 바로 씀)도 넣음(호표쓰임 표시) ·
|
||||
북부청 두 파일 = C1 out_items(같은 id · 정답 L) · 영월 = C1 id(Y000~) 이어 씀.
|
||||
품: STmate = 자원 L 줄 7열(단위당 품) 합 · 엑셀꼴 = 글 「직종 : 단가 × … = 금액」 → 금액 ÷ 단가(가장 큰 수) — 금액은 품을 되찾는 데만 씀.
|
||||
장비: STmate = 자원 X 줄 이름 · 엑셀꼴 = 글 속 장비 낱말 → 같은 낱말로 모음.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225/C1"
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
|
||||
FILES = ["영월", "봉화", "대흥설계", "계류보전", "소광", "거창"]
|
||||
PREFIX = {"영월": "Y", "봉화": "B", "대흥설계": "U", "계류보전": "K", "소광": "G", "거창": "C"}
|
||||
SHOW = {"영월": "동부청영월", "봉화": "봉화", "대흥설계": "울진대흥", "계류보전": "영덕계류", "소광": "울진소광", "거창": "거창"}
|
||||
LOGIC = ("단가산출", "기계", "기타")
|
||||
MACH = [ # (찾을 낱말, 모을 이름) — 앞이 먼저
|
||||
("굴삭기", "굴착기"), ("굴착기", "굴착기"), ("백호", "굴착기"), ("b/h", "굴착기"),
|
||||
("덤프", "덤프트럭"), ("크레인", "크레인"), ("불도저", "불도저"), ("불도우저", "불도저"), ("도저", "불도저"),
|
||||
("진동롤러", "롤러"), ("롤러", "롤러"), ("로울러", "롤러"), ("래머", "래머"), ("램머", "래머"),
|
||||
("콤팩터", "콤팩터"), ("컴팩터", "콤팩터"), ("브레이커", "브레이커"), ("브레이카", "브레이커"),
|
||||
("펌프카", "펌프차"), ("펌프차", "펌프차"), ("그레이더", "그레이더"), ("살수차", "살수차"),
|
||||
("발전기", "발전기"), ("공기압축기", "공기압축기"), ("드릴", "드릴"), ("믹서", "믹서"), ("트레일러", "트레일러"),
|
||||
]
|
||||
_LABOR_LINE = re.compile(r"^[\s\d\.\)\(○◇·\-*①-⑳가-힣]*?([가-힣][가-힣\s]*?)\s*:\s*(.+?)=\s*([\d,]+(?:\.\d+)?)\s*$")
|
||||
_NUMS = re.compile(r"\d[\d,]*(?:\.\d+)?")
|
||||
|
||||
|
||||
def machines(text):
|
||||
low = re.sub(r"\s+", "", str(text or "")).lower()
|
||||
out = set()
|
||||
for word, name in MACH:
|
||||
if word in low:
|
||||
out.add(name)
|
||||
low = low.replace(word, " ")
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def trade_name(raw):
|
||||
return hb.trade_names().get(re.sub(r"\s+", "", str(raw or "")))
|
||||
|
||||
|
||||
def text_labor(lines):
|
||||
"""엑셀꼴 글 → {직종: 품} (금액 ÷ 단가 · 단가 = 왼쪽에서 가장 큰 수 ≥ 1000)."""
|
||||
out = {}
|
||||
for t in lines:
|
||||
m = _LABOR_LINE.match(str(t))
|
||||
if not m:
|
||||
continue
|
||||
name = trade_name(m.group(1))
|
||||
if not name:
|
||||
continue
|
||||
nums = [float(x.replace(",", "")) for x in _NUMS.findall(m.group(2))]
|
||||
amount = float(m.group(3).replace(",", ""))
|
||||
wage = max(nums) if nums else 0
|
||||
if wage >= 1000 and amount > 0:
|
||||
out[name] = round(out.get(name, 0) + amount / wage, 6)
|
||||
return out
|
||||
|
||||
|
||||
def block_features(blk):
|
||||
text = blk.get("글") or []
|
||||
joined = " ".join(text)
|
||||
src = hb.sources(joined)
|
||||
if blk.get("자원"):
|
||||
labor, mach = {}, set()
|
||||
for r in blk["자원"]:
|
||||
if r["갈래"] == "L" and r["품"]:
|
||||
name = trade_name(r["이름"])
|
||||
if name:
|
||||
labor[name] = round(labor.get(name, 0) + r["품"], 6)
|
||||
if r["갈래"] == "X":
|
||||
mach |= set(machines(r["이름"]))
|
||||
mach = sorted(mach)
|
||||
else:
|
||||
labor, mach = text_labor(text), machines(joined)
|
||||
return {"출처": src, "직종": sorted(labor), "품": labor, "장비": mach}
|
||||
|
||||
|
||||
def from_c1():
|
||||
"""북부청 두 파일 — C1 고유 줄 그대로(id · 근거) + 품 · 장비."""
|
||||
out = []
|
||||
for r in json.loads((C1 / "out_items.json").read_text(encoding="utf-8")):
|
||||
f = {"출처": (r.get("근거") or {}).get("출처") or [], "직종": [], "품": {}, "장비": []}
|
||||
if r.get("단산글"):
|
||||
g = r["단산글"][1:]
|
||||
f = block_features({"글": g})
|
||||
f["출처"] = (r.get("근거") or {}).get("출처") or f["출처"]
|
||||
out.append(
|
||||
{
|
||||
"id": r["id"], "파일": r["파일"], "이름": r["이름"], "규격": r["규격"], "단위": r["단위"],
|
||||
"갈래": r["갈래"], "코드": f"단산{r['참조']['번']}" if (r.get("참조") or {}).get("종류") == "단산" else None,
|
||||
"호표쓰임": True, "줄수": r["줄수"], "호표들": sorted(set(r["호표들"])),
|
||||
"근거": {"출처": f["출처"], "직종": (r.get("근거") or {}).get("직종") or f["직종"]},
|
||||
"품": f["품"], "장비": f["장비"], "글": (r.get("단산글") or [])[1:40],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def from_file(tag):
|
||||
d = json.loads((HERE / f"data_{tag}.json").read_text(encoding="utf-8"))
|
||||
old = {}
|
||||
if tag == "영월":
|
||||
for y in json.loads((C1 / "out_stmate.json").read_text(encoding="utf-8")):
|
||||
old[(y["이름"], y["규격"], y["단위"], y["코드"])] = y["id"]
|
||||
seen, out, n = {}, [], 0
|
||||
feats = {code: block_features(b) for code, b in d["블록"].items()}
|
||||
|
||||
def add(name, spec, unit, kind, code, used, hno):
|
||||
nonlocal n
|
||||
key = (name, spec, unit, code)
|
||||
if key in seen:
|
||||
if used:
|
||||
seen[key]["줄수"] += 1
|
||||
seen[key]["호표쓰임"] = True
|
||||
if hno not in seen[key]["호표들"]:
|
||||
seen[key]["호표들"].append(hno)
|
||||
return
|
||||
f = feats.get(code) or {"출처": [], "직종": [], "품": {}, "장비": machines(f"{name} {spec}") if kind == "기계" else []}
|
||||
rid = old.get(key)
|
||||
if rid is None:
|
||||
rid = f"{PREFIX[tag]}{100 + n if tag == '영월' else n:03d}"
|
||||
n += 1
|
||||
rec = {
|
||||
"id": rid, "파일": SHOW[tag], "이름": name, "규격": spec, "단위": unit, "갈래": kind, "코드": code,
|
||||
"호표쓰임": used, "줄수": 1 if used else 0, "호표들": [hno] if used else [],
|
||||
"근거": {"출처": f["출처"], "직종": f["직종"]}, "품": f["품"], "장비": f["장비"],
|
||||
"글": (d["블록"].get(code) or {}).get("글", [])[:40],
|
||||
}
|
||||
seen[key] = rec
|
||||
out.append(rec)
|
||||
|
||||
for h in d["호표"]:
|
||||
for x in h["줄"]:
|
||||
if x["갈래"] in LOGIC:
|
||||
add(x["이름"], x["규격"], x["단위"], x["갈래"], x["코드"], True, h["번"])
|
||||
for code, b in d["블록"].items(): # 호표에 안 쓰인 블록(내역서가 바로 씀)
|
||||
if not any(r["코드"] == code for r in out):
|
||||
add(b.get("이름") or b["머리"], b.get("규격") or "", b.get("단위") or "", "단가산출", code, False, None)
|
||||
return out
|
||||
|
||||
|
||||
def hopyo_units(tag):
|
||||
"""호표 = 로직(노임 줄을 바로 적은 호표) — 이름 · 단위 · 직종 품 · 장비 · 줄 코드."""
|
||||
if tag in ("북부청임도", "북부청사방"):
|
||||
return []
|
||||
d = json.loads((HERE / f"data_{tag}.json").read_text(encoding="utf-8"))
|
||||
out = []
|
||||
for h in d["호표"]:
|
||||
labor = {}
|
||||
for x in h["줄"]:
|
||||
is_labor = x["갈래"] == "노임" or (x["갈래"] == "기타" and hm.mm.plain_unit(x["단위"]) == "인")
|
||||
name = trade_name(x["이름"]) if is_labor else None
|
||||
if name and x.get("수량"):
|
||||
labor[name] = round(labor.get(name, 0) + float(x["수량"]), 6)
|
||||
if labor:
|
||||
mach = sorted({m for x in h["줄"] if x["갈래"] == "기계" for m in machines(x["이름"])})
|
||||
out.append({"파일": SHOW[tag], "호표": h["번"], "이름": h.get("이름"), "규격": h.get("규격"), "단위": h.get("단위"), "품": labor, "장비": mach})
|
||||
return out
|
||||
|
||||
|
||||
def hopyo_map():
|
||||
"""파일 → 호표 번 → 그 호표 로직 줄 id 목록(호표 조합 지문용)."""
|
||||
lines = json.loads((C1 / "out_lines.json").read_text(encoding="utf-8"))
|
||||
out = {}
|
||||
return out, lines
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
items = from_c1()
|
||||
units = []
|
||||
for tag in FILES:
|
||||
items += from_file(tag)
|
||||
units += hopyo_units(tag)
|
||||
(HERE / "items.json").write_text(json.dumps(items, ensure_ascii=False, indent=0), "utf-8")
|
||||
(HERE / "hopyo_units.json").write_text(json.dumps(units, ensure_ascii=False, indent=0), "utf-8")
|
||||
by = {}
|
||||
for r in items:
|
||||
s = by.setdefault(r["파일"], {"고유": 0, "호표쓰임": 0, "엑셀줄": 0, "출처있음": 0, "직종있음": 0, "품있음": 0})
|
||||
s["고유"] += 1
|
||||
s["호표쓰임"] += r["호표쓰임"]
|
||||
s["엑셀줄"] += r["줄수"]
|
||||
s["출처있음"] += bool(r["근거"]["출처"])
|
||||
s["직종있음"] += bool(r["근거"]["직종"])
|
||||
s["품있음"] += bool(r["품"])
|
||||
for k, v in by.items():
|
||||
print(k, v)
|
||||
hu = {}
|
||||
for u in units:
|
||||
hu[u["파일"]] = hu.get(u["파일"], 0) + 1
|
||||
print("호표=로직", hu)
|
||||
@@ -0,0 +1,202 @@
|
||||
"""C6 — 자동 정답(엄격): 엑셀 블록 인용 조항(같은 책 · 번호 같음) + 직종 묶음 · 품(±3%)이 우리 돈 로직 하나와만 맞으면 그 로직.
|
||||
|
||||
우리 품 = 로직을 입력 조합마다 돌려(고르기 모두 · 범위 끝 둘 · 지역 · 기계 고르기는 견본값) 「인」 줄 수량 —
|
||||
원본 로직 · 마스터는 읽기만(엔진 run 을 같은 프로세스에서 부름).
|
||||
python s04_auto.py → auto.json(id → {정답, 까닭, 후보 수}) + 사람 정답(L · LY) · 씨앗과 맞대 본 표
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225/C1"
|
||||
sys.path.insert(0, str(C1))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
|
||||
TOL = 0.03
|
||||
LOOSE = "loose" in sys.argv
|
||||
CAP = 600
|
||||
mf, mau = m01.cm.mf, m01.mau
|
||||
WHOLE = m01.loaded()[1]
|
||||
|
||||
|
||||
def _choices(spec):
|
||||
opts = spec.get("고르기")
|
||||
if opts:
|
||||
if spec["이름"] == "지역" or all(re.match(r"^\d{4}-\d{4}$", str(o)) for o in opts):
|
||||
return None
|
||||
return list(opts)
|
||||
rng = spec.get("범위")
|
||||
if isinstance(rng, list) and len(rng) == 2:
|
||||
return [Decimal(str(rng[0])), Decimal(str(rng[1]))]
|
||||
return None
|
||||
|
||||
|
||||
_CACHE_PATH = HERE / "vec_cache.json"
|
||||
try:
|
||||
_CACHE = {k: tuple(v) for k, v in json.loads(_CACHE_PATH.read_text(encoding="utf-8")).items()}
|
||||
except (OSError, ValueError):
|
||||
_CACHE = {}
|
||||
|
||||
|
||||
def vectors(key):
|
||||
if key not in _CACHE:
|
||||
_CACHE[key] = _vectors(key)
|
||||
return _CACHE[key]
|
||||
|
||||
|
||||
def save_cache():
|
||||
_CACHE_PATH.write_text(json.dumps({k: list(v) for k, v in _CACHE.items()}, ensure_ascii=False), "utf-8")
|
||||
|
||||
|
||||
def _vectors(key):
|
||||
"""[{직종: 품}] — 입력 조합마다(0 빼고) · 같은 것 한 번."""
|
||||
try:
|
||||
row = WHOLE.logic(key)
|
||||
base = mau.values(WHOLE, row)
|
||||
except Exception: # noqa: BLE001
|
||||
return ()
|
||||
names, pools = [], []
|
||||
for spec in row.get("입력") or []:
|
||||
c = _choices(spec)
|
||||
if c:
|
||||
names.append(spec["이름"])
|
||||
pools.append(c)
|
||||
size = 1
|
||||
for p in pools:
|
||||
size *= len(p)
|
||||
if size > CAP: # 범위 흔들기 뺌
|
||||
keep = [(n, p) for n, p in zip(names, pools) if not isinstance(p[0], Decimal)]
|
||||
names, pools = [n for n, _ in keep], [p for _, p in keep]
|
||||
out, seen = [], set()
|
||||
for combo in itertools.islice(itertools.product(*pools), CAP):
|
||||
given = mf.fill_empty(row, {**base, **dict(zip(names, combo))})
|
||||
try:
|
||||
res = mf.run(WHOLE, key, given)
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
vec = {}
|
||||
for line in res.get("줄") or []:
|
||||
if hm.mm.plain_unit(line.get("단위")) == "인":
|
||||
q = float(line.get("수량") or 0)
|
||||
if q > 0:
|
||||
vec[str(line["이름"])] = round(vec.get(str(line["이름"]), 0) + q, 6)
|
||||
sig = tuple(sorted(vec.items()))
|
||||
if vec and sig not in seen:
|
||||
seen.add(sig)
|
||||
out.append(vec)
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def same_labor(ext, ours):
|
||||
if set(ext) != set(ours):
|
||||
return False
|
||||
return all(abs(ext[t] - ours[t]) <= TOL * max(ext[t], ours[t]) for t in ext)
|
||||
|
||||
|
||||
def auto_label(item, table, logics):
|
||||
src = item["근거"]["출처"]
|
||||
labor = item.get("품") or {}
|
||||
if not src:
|
||||
return None, "출처 없음", 0
|
||||
if not labor:
|
||||
return None, "직종 · 품 없음", 0
|
||||
cands = [k for k in logics if any(b == table[k]["책"] and n in table[k]["조항"] for b, n in src)]
|
||||
if not cands:
|
||||
return None, "조항 같은 로직 없음", 0
|
||||
hits = [k for k in cands if any(same_labor(labor, v) for v in vectors(k))]
|
||||
if len(hits) == 1:
|
||||
return hits[0], "하나", len(cands)
|
||||
if LOOSE and not hits: # 준엄격 — 품 값은 안 보고 직종 묶음만
|
||||
hits = [k for k in cands if any(set(labor) == set(v) for v in vectors(k))]
|
||||
if len(hits) == 1:
|
||||
return hits[0], "하나(직종만)", len(cands)
|
||||
if not hits:
|
||||
return None, "조항은 같으나 직종 · 품 다름", len(cands)
|
||||
return None, f"둘 이상({len(hits)})", len(cands)
|
||||
|
||||
|
||||
def seeds_of(item, got):
|
||||
s = got["씨앗"]
|
||||
return s.get(hm.mm.plain(f"{item['이름']}{item['규격']}")) or s.get(hm.mm.plain(item["이름"])) or set()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from labels import L # noqa: E402
|
||||
from labels_y import LY # noqa: E402
|
||||
|
||||
hand = {**L, **LY}
|
||||
items = json.loads((HERE / "items.json").read_text(encoding="utf-8"))
|
||||
got = hm.targets()
|
||||
table = hb.ours()
|
||||
logics = [x["로직"] for x in got["로직"] if x["로직"] in table]
|
||||
out, why = {}, {}
|
||||
for r in items:
|
||||
if r["갈래"] != "단가산출":
|
||||
out[r["id"]] = {"정답": None, "까닭": "단가산출 줄 아님"}
|
||||
continue
|
||||
k, reason, n = auto_label(r, table, logics)
|
||||
out[r["id"]] = {"정답": k, "까닭": reason, "조항후보": n}
|
||||
save_cache()
|
||||
(HERE / ("auto_loose.json" if LOOSE else "auto.json")).write_text(json.dumps(out, ensure_ascii=False, indent=0), "utf-8")
|
||||
files = list(dict.fromkeys(r["파일"] for r in items))
|
||||
print("파일 | 고유 | 단가산출 | 자동정답 | 호표쓰임 중 자동 | 까닭별")
|
||||
for f in files:
|
||||
rs = [r for r in items if r["파일"] == f]
|
||||
d = [r for r in rs if r["갈래"] == "단가산출"]
|
||||
a = [r for r in d if out[r["id"]]["정답"]]
|
||||
au = [r for r in a if r["호표쓰임"]]
|
||||
reasons = {}
|
||||
for r in d:
|
||||
if not out[r["id"]]["정답"]:
|
||||
key = out[r["id"]]["까닭"].split("(")[0]
|
||||
reasons[key] = reasons.get(key, 0) + 1
|
||||
print(f, len(rs), len(d), len(a), f"{len(au)}/{sum(1 for r in d if r['호표쓰임'])}", reasons)
|
||||
# 사람 정답과 맞대 봄
|
||||
agree = {"엄격 같음": 0, "느슨만 같음": 0, "다름": 0, "사람 없음(우리에 없음)": 0}
|
||||
bad = []
|
||||
for r in items:
|
||||
a = out[r["id"]]["정답"]
|
||||
if not a or r["id"] not in hand:
|
||||
continue
|
||||
lab = hand[r["id"]]
|
||||
if not lab["정답"]:
|
||||
agree["사람 없음(우리에 없음)"] += 1
|
||||
bad.append((r["id"], r["이름"], a, "사람 없음"))
|
||||
elif a in lab["정답"]:
|
||||
agree["엄격 같음"] += 1
|
||||
elif a in lab.get("허용", []):
|
||||
agree["느슨만 같음"] += 1
|
||||
bad.append((r["id"], r["이름"], a, lab["정답"]))
|
||||
else:
|
||||
agree["다름"] += 1
|
||||
bad.append((r["id"], r["이름"], a, lab["정답"], out[r["id"]]["까닭"]))
|
||||
print("사람 정답 대 자동:", agree)
|
||||
for b in bad:
|
||||
print(" ", b)
|
||||
# 씨앗과 맞대 봄(씨앗 = 같은 실무 파일에서 사람이 골라 둔 짝)
|
||||
sa = {"같음": 0, "다름": 0}
|
||||
sbad = []
|
||||
for r in items:
|
||||
a = out[r["id"]]["정답"]
|
||||
s = seeds_of(r, got)
|
||||
if a and s:
|
||||
if a in s:
|
||||
sa["같음"] += 1
|
||||
else:
|
||||
sa["다름"] += 1
|
||||
sbad.append((r["id"], r["이름"], r["규격"], a, sorted(s)))
|
||||
print("씨앗 대 자동:", sa)
|
||||
for b in sbad:
|
||||
print(" ", b)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""C6 — 손 확인 표본: 사람 정답이 없는 줄의 자동 정답(엄격 먼저 · 모자라면 준엄격) 20 줄 보기."""
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parent.parent / "req225/C1"))
|
||||
from s04_auto import hb, hm, vectors, WHOLE # noqa: E402
|
||||
from labels import L # noqa: E402
|
||||
from labels_y import LY # noqa: E402
|
||||
hand = {**L, **LY}
|
||||
items = {r["id"]: r for r in json.loads((HERE / "items.json").read_text(encoding="utf-8"))}
|
||||
strict = json.loads((HERE / "auto.json").read_text(encoding="utf-8"))
|
||||
loose = json.loads((HERE / "auto_loose.json").read_text(encoding="utf-8"))
|
||||
got = hm.targets(); names = {x["로직"]: x for x in got["로직"]}; table = hb.ours()
|
||||
pick = [i for i, a in strict.items() if a["정답"] and i not in hand]
|
||||
extra = [i for i, a in loose.items() if a["정답"] and i not in hand and i not in pick]
|
||||
sample = (pick + extra)[:20]
|
||||
print("엄격(사람 정답 없는 줄)", len(pick), "준엄격 더함", len(extra), "표본", len(sample))
|
||||
for i in sample:
|
||||
r = items[i]; a = (strict.get(i) if i in pick else loose[i])["정답"]
|
||||
o = table[a]; row = WHOLE.logic(a)
|
||||
print(f"## {i} [{r['파일']}] {'엄격' if i in pick else '준엄격'} 호표쓰임 {r['호표쓰임']} | {r['이름']} | {r['규격']} | {r['단위']}")
|
||||
print(f" 엑셀 출처 {r['근거']['출처']} 품 {r['품']} 장비 {r['장비']}")
|
||||
for t in r["글"][:9]:
|
||||
print(" ·", " ".join(str(t).split())[:110])
|
||||
print(f" → {a} {names[a]['이름']} [{names[a]['단위']}] {o['구분']} {sorted(o['조항'])} 입력 {[s['이름'] for s in row.get('입력') or []]}")
|
||||
print(f" 우리 품 {[v for v in vectors(a)][:4]}")
|
||||
@@ -0,0 +1,69 @@
|
||||
"""C6 — 지금 맞춤(hm.match_line · 기억 없음)을 넓힌 자료 모든 줄에 돌림 → match.json.
|
||||
|
||||
갈래(근거를 얼마나 읽었다고 치나 — STmate · 거창만 바꿈 · 북부청은 저장소 읽기 그대로):
|
||||
이름만 = 근거 없음(거창 지금 읽기 · STmate 를 이름만 읽는다면)
|
||||
출처만 = 조항만(C1 영월 흉내와 같음 — 직종 이음 빠짐)
|
||||
출처직종 = 조항 + 노무 목록으로 이은 직종(STmate 읽기 보강 뒤 · 거창 흩어진 칸 붙임 뒤)
|
||||
씨앗 켬 · 끔 둘 다. 저장소 안 고침 — 씨앗 끔 = 같은 프로세스에서 hm._seeds 를 빈 것으로 바꿔 끼움.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
sys.path.insert(0, str(ROOT))
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
|
||||
ITEMS = json.loads((HERE / "items.json").read_text(encoding="utf-8"))
|
||||
OLD = ("북부청임도", "북부청사방")
|
||||
VARIANTS = ("이름만", "출처만", "출처직종")
|
||||
|
||||
|
||||
def line_of(r, variant):
|
||||
basis = dict(r["근거"])
|
||||
if r["파일"] not in OLD:
|
||||
if variant == "이름만":
|
||||
basis = {}
|
||||
elif variant == "출처만":
|
||||
basis = {"출처": basis.get("출처") or [], "직종": []}
|
||||
return {"이름": r["이름"], "규격": r["규격"], "단위": r["단위"], "단가": {}, "갈래": r["갈래"], "근거": basis or None}
|
||||
|
||||
|
||||
def top_of(m):
|
||||
if m["상태"] in ("같음", "기억"):
|
||||
return [m["대상"].get("로직")] if m.get("대상") and m["대상"].get("로직") else []
|
||||
return [c.get("로직") for c in m.get("후보") or [] if c.get("로직")]
|
||||
|
||||
|
||||
def run(seed, variant, items=ITEMS):
|
||||
real = hm._seeds
|
||||
if not seed:
|
||||
hm._seeds = lambda: {}
|
||||
hm._targets.cache_clear()
|
||||
hb._ours.cache_clear()
|
||||
try:
|
||||
got = hm.targets()
|
||||
out = {}
|
||||
for r in items:
|
||||
m = hm.match_line(line_of(r, variant), [], {}, True, got)
|
||||
out[r["id"]] = {"상태": m["상태"], "top": top_of(m)}
|
||||
return out
|
||||
finally:
|
||||
hm._seeds = real
|
||||
hm._targets.cache_clear()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
want = sys.argv[1:] or [f"{v}|{s}" for v in VARIANTS for s in ("켬", "끔")]
|
||||
path = HERE / "match.json"
|
||||
allm = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
|
||||
for w in want:
|
||||
v, s = w.split("|")
|
||||
allm[w] = run(s == "켬", v)
|
||||
print(w, "done", len(allm[w]))
|
||||
path.write_text(json.dumps(allm, ensure_ascii=False), "utf-8")
|
||||
@@ -0,0 +1,96 @@
|
||||
"""C6 — 지금 실력 표(정답 셋 · 갈래 · 씨앗 켬/끔).
|
||||
|
||||
정답 셋: 사람(C1 L · LY · 엄격/느슨) · 자동 엄격(새 파일 · 사람 정답 없는 줄) · 자동 준엄격 · 씨앗 짝(씨앗 끔에서만 · 이름이 씨앗 메모와 같은 줄).
|
||||
python s07_score.py → 표 찍고 score.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225/C1"
|
||||
sys.path.insert(0, str(C1))
|
||||
sys.path.insert(0, str(HERE))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from labels import L # noqa: E402
|
||||
from labels_y import LY # noqa: E402
|
||||
from s04_auto import hm, seeds_of # noqa: E402
|
||||
|
||||
ITEMS = json.loads((HERE / "items.json").read_text(encoding="utf-8"))
|
||||
BY = {r["id"]: r for r in ITEMS}
|
||||
M = json.loads((HERE / "match.json").read_text(encoding="utf-8"))
|
||||
AUTO = json.loads((HERE / "auto.json").read_text(encoding="utf-8"))
|
||||
LOOSE = json.loads((HERE / "auto_loose.json").read_text(encoding="utf-8"))
|
||||
HAND = {k: v for k, v in {**L, **LY}.items() if k in BY}
|
||||
FILES = list(dict.fromkeys(r["파일"] for r in ITEMS))
|
||||
|
||||
|
||||
def answers(source):
|
||||
"""{id: (엄격 set, 느슨 set)} — 정답 있는 줄만."""
|
||||
out = {}
|
||||
if source == "사람":
|
||||
for i, lab in HAND.items():
|
||||
if lab["정답"]:
|
||||
out[i] = (set(lab["정답"]), set(lab["정답"]) | set(lab.get("허용") or []))
|
||||
elif source in ("자동", "준엄격"):
|
||||
src = AUTO if source == "자동" else LOOSE
|
||||
for i, a in src.items():
|
||||
if a["정답"] and i not in HAND:
|
||||
out[i] = ({a["정답"]}, {a["정답"]})
|
||||
elif source == "씨앗":
|
||||
got = hm.targets()
|
||||
for r in ITEMS:
|
||||
if r["파일"] in ("북부청임도", "북부청사방") or r["id"] in HAND:
|
||||
continue
|
||||
s = seeds_of(r, got)
|
||||
if s:
|
||||
out[r["id"]] = (set(s), set(s))
|
||||
return out
|
||||
|
||||
|
||||
def rate(ids, res, ans, k, loose=False, used_only=False):
|
||||
ids = [i for i in ids if i in ans and (BY[i]["호표쓰임"] or not used_only)]
|
||||
if not ids:
|
||||
return None, 0
|
||||
hit = 0
|
||||
for i in ids:
|
||||
top = res[i]["top"][:k]
|
||||
hit += bool(set(top) & ans[i][1 if loose else 0])
|
||||
return round(100 * hit / len(ids), 1), len(ids)
|
||||
|
||||
|
||||
def table(source, key, used_only=False):
|
||||
ans = answers(source)
|
||||
res = M[key]
|
||||
rows = {}
|
||||
for f in FILES + ["모두"]:
|
||||
ids = [r["id"] for r in ITEMS if (r["파일"] == f or f == "모두")]
|
||||
a1, n = rate(ids, res, ans, 1, used_only=used_only)
|
||||
if not n:
|
||||
continue
|
||||
rows[f] = {
|
||||
"n": n,
|
||||
"1위 엄격": a1,
|
||||
"1위 느슨": rate(ids, res, ans, 1, True, used_only)[0],
|
||||
"5안 엄격": rate(ids, res, ans, 5, used_only=used_only)[0],
|
||||
"5안 느슨": rate(ids, res, ans, 5, True, used_only)[0],
|
||||
}
|
||||
return rows
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = {}
|
||||
for source in ("사람", "자동", "준엄격", "씨앗"):
|
||||
for key in M:
|
||||
if source == "씨앗" and key.endswith("켬"):
|
||||
continue
|
||||
for used in (False, True):
|
||||
t = table(source, key, used)
|
||||
out[f"{source}|{key}|{'호표만' if used else '모두'}"] = t
|
||||
(HERE / "score.json").write_text(json.dumps(out, ensure_ascii=False, indent=0), "utf-8")
|
||||
for k, t in out.items():
|
||||
if k.endswith("모두") and not k.startswith("씨앗") or k.startswith("씨앗") and k.endswith("모두"):
|
||||
cells = " · ".join(f"{f} {v['n']}:{v['1위 엄격']}/{v['5안 엄격']}" for f, v in t.items())
|
||||
print(k, "|", cells)
|
||||
@@ -0,0 +1,290 @@
|
||||
"""C6 — 기억 흉내: 앞 파일에서 사람이 확정했다고 치고(확정 = 정답) 다음 파일 1순위가 얼마나 오르나 · 틀린 기억 해로움.
|
||||
|
||||
열쇠: a 품명|규격|단위(지금 line_key) · b 품명|단위 · c 로직 속 지문(단위 · 직종:품 · 장비 · 조항 — 금액 안 씀) ·
|
||||
c2 지문(단위 · 직종:품만 — 조항 · 장비 뺌) · d 호표 조합 지문(호표 단위 + 그 호표 로직 줄 지문 묶음 → 그 안 자리).
|
||||
확정(기억 값) = 사람 정답(C1) ∪ 자동 정답(엄격) ∪ 씨앗 짝(씨앗을 만든 파일 줄 = 사람이 고른 것) · 판정 = 사람 ∪ 자동(엄격)만.
|
||||
바탕(기억 없음) = 지금 맞춤 · 출처직종 · 씨앗 끔(씨앗도 「앞 파일 확정」 한 꼴로 기억 쪽에 넣음).
|
||||
python s08_memory.py [호표만|모두] [준엄격] → 표 · memory.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C1 = HERE.parent.parent / "req225/C1"
|
||||
sys.path.insert(0, str(C1))
|
||||
sys.path.insert(0, str(HERE))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from labels import L # noqa: E402
|
||||
from labels_y import LY # noqa: E402
|
||||
from s04_auto import hb, hm, seeds_of # noqa: E402
|
||||
|
||||
USED_ONLY = "모두" not in sys.argv
|
||||
LOOSE = "준엄격" in sys.argv
|
||||
ITEMS = json.loads((HERE / "items.json").read_text(encoding="utf-8"))
|
||||
if USED_ONLY:
|
||||
ITEMS = [r for r in ITEMS if r["호표쓰임"]]
|
||||
BY = {r["id"]: r for r in ITEMS}
|
||||
M = json.loads((HERE / "match.json").read_text(encoding="utf-8"))
|
||||
BASE = M["출처직종|끔"]
|
||||
AUTO = json.loads((HERE / ("auto_loose.json" if LOOSE else "auto.json")).read_text(encoding="utf-8"))
|
||||
HAND = {k: v for k, v in {**L, **LY}.items() if k in BY}
|
||||
META = { # 파일: (발주처, 해, 꼴)
|
||||
"울진대흥": ("남부청", 2024, "STmate"),
|
||||
"봉화": ("남부청", 2024, "STmate"),
|
||||
"동부청영월": ("동부청", 2024, "STmate"),
|
||||
"울진소광": ("남부청", 2025, "STmate"),
|
||||
"영덕계류": ("남부청", 2025, "STmate"),
|
||||
"거창": ("남부청", 2025, "엑셀꼴"),
|
||||
"북부청임도": ("북부청", 2026, "엑셀꼴"),
|
||||
"북부청사방": ("북부청", 2026, "엑셀꼴"),
|
||||
}
|
||||
ORDER = list(META)
|
||||
GOT = hm.targets()
|
||||
OURS = hb.ours()
|
||||
|
||||
|
||||
# ── 정답 · 확정 ────────────────────────────────────────────────────────
|
||||
def truth(i):
|
||||
"""판정 정답(엄격 set) — 사람 · 자동만 · 없으면 None."""
|
||||
if i in HAND:
|
||||
return set(HAND[i]["정답"]) or None
|
||||
a = AUTO.get(i, {}).get("정답")
|
||||
return {a} if a else None
|
||||
|
||||
|
||||
def confirmed(i):
|
||||
"""사람이 확정한 값(기억으로 들어갈 것) — 사람 · 자동 · 씨앗 짝 · 없으면 None. 여럿이면 차례 첫째."""
|
||||
t = truth(i)
|
||||
if t:
|
||||
return sorted(t)[0]
|
||||
r = BY[i]
|
||||
if r["파일"] in ("북부청임도", "북부청사방"):
|
||||
return None
|
||||
s = seeds_of(r, GOT)
|
||||
return sorted(s)[0] if s else None
|
||||
|
||||
|
||||
# ── 열쇠 ───────────────────────────────────────────────────────────────
|
||||
def k_a(r):
|
||||
return hm.line_key(r)
|
||||
|
||||
|
||||
def k_b(r):
|
||||
return f"{hm.mm.plain(r['이름'])}|{hm.mm.plain_unit(r['단위'])}"
|
||||
|
||||
|
||||
def _labor(r):
|
||||
return ",".join(f"{t}:{q:.3g}" for t, q in sorted((r.get("품") or {}).items()))
|
||||
|
||||
|
||||
def k_c(r):
|
||||
src = ",".join(sorted(f"{b[:2]}{n}" for b, n in r["근거"]["출처"]))
|
||||
mach = ",".join(r.get("장비") or [])
|
||||
if not r.get("품") and not (mach and src):
|
||||
return None
|
||||
return f"{hm.mm.plain_unit(r['단위'])}|{_labor(r)}|{mach}|{src}"
|
||||
|
||||
|
||||
def k_c2(r):
|
||||
if not r.get("품"):
|
||||
return None
|
||||
return f"{hm.mm.plain_unit(r['단위'])}|{_labor(r)}"
|
||||
|
||||
|
||||
HOPYO = {} # (파일, 호표 번) → [id]
|
||||
for r in ITEMS:
|
||||
for h in r["호표들"]:
|
||||
HOPYO.setdefault((r["파일"], h), []).append(r["id"])
|
||||
|
||||
|
||||
def elem(r):
|
||||
return k_c(r) or f"N:{k_a(r)}"
|
||||
|
||||
|
||||
def k_d_all(r):
|
||||
"""줄이 든 호표마다 (호표 조합 열쇠, 자리 열쇠)."""
|
||||
out = []
|
||||
for h in r["호표들"]:
|
||||
ids = HOPYO[(r["파일"], h)]
|
||||
combo = "+".join(sorted(elem(BY[i]) for i in ids))
|
||||
out.append(f"{len(ids)}#{combo}§{elem(r)}")
|
||||
return out
|
||||
|
||||
|
||||
KEYS = {"a": k_a, "b": k_b, "c": k_c, "c2": k_c2, "d": k_d_all}
|
||||
|
||||
|
||||
def keys_of(name, r):
|
||||
got = KEYS[name](r)
|
||||
if got is None:
|
||||
return []
|
||||
return got if isinstance(got, list) else [got]
|
||||
|
||||
|
||||
# ── 기억 만들기 · 쓰기 ─────────────────────────────────────────────────
|
||||
def build(files, name, all_items=False):
|
||||
"""{열쇠: [(파일, 답)]} — files 차례대로 · all_items = 확정 없는 줄도(답 None · 「닿음」 셈용)."""
|
||||
memo = {}
|
||||
for f in files:
|
||||
for r in ITEMS:
|
||||
if r["파일"] != f:
|
||||
continue
|
||||
ans = confirmed(r["id"])
|
||||
if ans is None and not all_items:
|
||||
continue
|
||||
for k in keys_of(name, r):
|
||||
memo.setdefault(k, []).append((f, ans))
|
||||
return memo
|
||||
|
||||
|
||||
def recall(name, r, memo, policy="최근"):
|
||||
"""(답, 출처 목록) — 최근 = 마지막에 확정한 답 · 다수 = 표 많은 답(같으면 최근)."""
|
||||
for k in keys_of(name, r):
|
||||
if k in memo:
|
||||
got = [(f, a) for f, a in memo[k] if a]
|
||||
if not got:
|
||||
return None, []
|
||||
if policy == "다수":
|
||||
votes = Counter(a for _, a in got)
|
||||
top = max(votes.values())
|
||||
best = [a for _, a in reversed(got) if votes[a] == top]
|
||||
return best[0], got
|
||||
return got[-1][1], got
|
||||
return None, []
|
||||
|
||||
|
||||
def basis_of(r, ans):
|
||||
"""기억 답의 우리 조항이 엑셀 인용 조항과 같은가 — 맞음 · 다름 · 인용 없음."""
|
||||
src = r["근거"]["출처"]
|
||||
if not src:
|
||||
return "인용 없음"
|
||||
our = OURS.get(ans) or {}
|
||||
return "근거 맞음" if any(b == our.get("책") and n in our.get("조항", ()) for b, n in src) else "근거 다름"
|
||||
|
||||
|
||||
def evaluate(target, sources, name, policy="최근", cond=None):
|
||||
memo = build(sources, name)
|
||||
reach_memo = build(sources, name, all_items=True)
|
||||
rows = [r for r in ITEMS if r["파일"] == target]
|
||||
reach = sum(1 for r in rows if any(k in reach_memo for k in keys_of(name, r)))
|
||||
s = Counter()
|
||||
for r in rows:
|
||||
t = truth(r["id"])
|
||||
if not t:
|
||||
continue
|
||||
base = BASE[r["id"]]["top"]
|
||||
ans, prov = recall(name, r, memo, policy)
|
||||
before = bool(base[:1]) and base[0] in t
|
||||
after = before if ans is None else ans in t
|
||||
five_before = bool(set(base[:5]) & t)
|
||||
five_after = five_before if ans is None else bool(set([ans, *[x for x in base if x != ans]][:5]) & t)
|
||||
who = "사람" if r["id"] in HAND else "자동"
|
||||
s["판정줄"] += 1
|
||||
s[f"판정줄_{who}"] += 1
|
||||
s["전 1위"] += before
|
||||
s["뒤 1위"] += after
|
||||
s["전 5안"] += five_before
|
||||
s["뒤 5안"] += five_after
|
||||
if ans is None:
|
||||
continue
|
||||
ok = ans in t
|
||||
s["기억 닿음"] += 1
|
||||
s["기억 맞음"] += ok
|
||||
s[f"닿음_{who}"] += 1
|
||||
s[f"맞음_{who}"] += ok
|
||||
conflict = len({a for _, a in prov}) > 1
|
||||
s["충돌 열쇠"] += conflict
|
||||
s["해로움"] += before and not after
|
||||
s["도움"] += after and not before
|
||||
if cond is not None:
|
||||
same_org = any(META[f][0] == META[target][0] for f, _ in prov)
|
||||
same_year = any(META[f][1] == META[target][1] for f, _ in prov)
|
||||
files = len({f for f, a in prov if a == ans})
|
||||
for c in (
|
||||
f"열쇠 {name}",
|
||||
f"{name} · {'같은 발주처' if same_org else '다른 발주처'}",
|
||||
f"{name} · {'같은 해' if same_year else '다른 해'}",
|
||||
f"{name} · {basis_of(r, ans)}",
|
||||
f"{name} · {'두 파일 넘게 같은 답' if files >= 2 else '한 파일'}",
|
||||
f"{name} · {'충돌 있음' if conflict else '충돌 없음'}",
|
||||
f"{name} · 판정 {who}",
|
||||
f"{name} · {'같은 발주처' if same_org else '다른 발주처'} · {basis_of(r, ans)}",
|
||||
):
|
||||
cond.setdefault(c, Counter())
|
||||
cond[c]["닿음"] += 1
|
||||
cond[c]["맞음"] += ok
|
||||
cond[c]["해로움"] += before and not after
|
||||
return {"줄": len(rows), "닿음(모두 확정 셈)": reach, **s}
|
||||
|
||||
|
||||
def pct(a, b):
|
||||
return f"{100 * a / b:.0f}%" if b else "—"
|
||||
|
||||
|
||||
def show(title, res):
|
||||
print(f"\n### {title}")
|
||||
print("열쇠 | 줄 · 닿음(모두 확정) | 판정줄(사람) · 기억 닿음 · 맞음 (사람 닿음·맞음) | 1위 전→뒤 | 5안 전→뒤 | 도움 · 해로움 · 충돌")
|
||||
for name, s in res.items():
|
||||
n = s.get("판정줄", 0)
|
||||
print(
|
||||
f"{name} | {s['줄']} · {s['닿음(모두 확정 셈)']} ({pct(s['닿음(모두 확정 셈)'], s['줄'])}) "
|
||||
f"| {n}({s.get('판정줄_사람', 0)}) · {s.get('기억 닿음', 0)} · {s.get('기억 맞음', 0)} ({s.get('닿음_사람', 0)}·{s.get('맞음_사람', 0)}) "
|
||||
f"| {pct(s.get('전 1위', 0), n)}→{pct(s.get('뒤 1위', 0), n)} | {pct(s.get('전 5안', 0), n)}→{pct(s.get('뒤 5안', 0), n)} "
|
||||
f"| {s.get('도움', 0)} · {s.get('해로움', 0)} · {s.get('충돌 열쇠', 0)}"
|
||||
)
|
||||
|
||||
|
||||
def add(acc, s):
|
||||
for k, v in s.items():
|
||||
acc[k] = acc.get(k, 0) + v
|
||||
|
||||
|
||||
SOUTH24, SOUTH25 = ["울진대흥", "봉화"], ["울진소광", "영덕계류", "거창"]
|
||||
SCEN = {
|
||||
"북부청임도 → 북부청사방": (["북부청임도"], ["북부청사방"]),
|
||||
"북부청사방 → 북부청임도": (["북부청사방"], ["북부청임도"]),
|
||||
"울진대흥 → 봉화(남부 2024)": (["울진대흥"], ["봉화"]),
|
||||
"봉화 → 울진대흥(남부 2024)": (["봉화"], ["울진대흥"]),
|
||||
"남부 2024 → 남부 2025(해 바뀜)": (SOUTH24, SOUTH25),
|
||||
"북부청 → 동부청영월(다른 발주처)": (["북부청임도", "북부청사방"], ["동부청영월"]),
|
||||
"동부청영월 → 북부청(다른 발주처)": (["동부청영월"], ["북부청임도", "북부청사방"]),
|
||||
"동부청영월 → 남부 2024(다른 발주처 · 같은 해)": (["동부청영월"], SOUTH24),
|
||||
"남부 2024·2025 → 북부청(다른 발주처 · 해 바뀜)": (SOUTH24 + SOUTH25, ["북부청임도", "북부청사방"]),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = {"설정": {"호표만": USED_ONLY, "준엄격": LOOSE}}
|
||||
for title, (srcs, dsts) in SCEN.items():
|
||||
res = {}
|
||||
for name in KEYS:
|
||||
acc = {}
|
||||
for d in dsts:
|
||||
add(acc, evaluate(d, srcs, name))
|
||||
res[name] = acc
|
||||
show(title, res)
|
||||
out[title] = res
|
||||
cond = {}
|
||||
for policy in ("최근", "다수"):
|
||||
seq = {name: {} for name in KEYS}
|
||||
for at, target in enumerate(ORDER[1:], 1):
|
||||
for name in KEYS:
|
||||
add(seq[name], evaluate(target, ORDER[:at], name, policy, cond if policy == "최근" else None))
|
||||
show(f"차례 누적(앞 파일 모두 기억 · {' → '.join(ORDER)}) · 충돌 때 {policy}", seq)
|
||||
out[f"차례누적_{policy}"] = seq
|
||||
# 짝 모두(한 파일 → 다른 한 파일) — 조건별 맞음 셈을 늘림
|
||||
for src in ORDER:
|
||||
for dst in ORDER:
|
||||
if src != dst:
|
||||
for name in KEYS:
|
||||
evaluate(dst, [src], name, "최근", cond)
|
||||
print("\n### 조건별 기억 맞음(차례 누적 + 모든 짝 · 판정줄만)")
|
||||
for c, v in sorted(cond.items()):
|
||||
print(f"{c} | 닿음 {v['닿음']} · 맞음 {v['맞음']} ({pct(v['맞음'], v['닿음'])}) · 해로움 {v['해로움']}")
|
||||
out["조건"] = {c: dict(v) for c, v in cond.items()}
|
||||
(HERE / f"memory{'_모두' if not USED_ONLY else ''}{'_준엄격' if LOOSE else ''}.json").write_text(
|
||||
json.dumps(out, ensure_ascii=False, indent=0), "utf-8"
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""C6 — 기억 예: 해로움(틀린 기억이 맞던 1순위를 뒤집음) · 도움(c 지문이 다른 발주처에서 맞힘)."""
|
||||
import sys
|
||||
from s08_memory import BY, ITEMS, ORDER, META, BASE, build, recall, truth, keys_of, basis_of, GOT
|
||||
names = {x["로직"]: x["이름"] for x in GOT["로직"]}
|
||||
seen = set()
|
||||
for name in ("b", "c2", "c", "a"):
|
||||
print(f"\n## 열쇠 {name}")
|
||||
for src in ORDER:
|
||||
for dst in ORDER:
|
||||
if src == dst or META[src][0] == META[dst][0]:
|
||||
continue
|
||||
memo = build([src], name)
|
||||
for r in ITEMS:
|
||||
if r["파일"] != dst or not truth(r["id"]):
|
||||
continue
|
||||
ans, prov = recall(name, r, memo)
|
||||
if ans is None:
|
||||
continue
|
||||
t = truth(r["id"]); base = BASE[r["id"]]["top"]
|
||||
before = bool(base[:1]) and base[0] in t
|
||||
after = ans in t
|
||||
tag = "해로움" if before and not after else ("도움" if after and not before else None)
|
||||
if tag and (name, r["id"], tag) not in seen:
|
||||
seen.add((name, r["id"], tag))
|
||||
src_item = next(x for x in ITEMS if x["파일"] == src and any(k in keys_of(name, x) for k in keys_of(name, r)))
|
||||
print(f"{tag} {src}→{dst} | {r['이름']} {r['규격']} [{r['단위']}] 엑셀출처 {r['근거']['출처']} | 기억 {ans} {names.get(ans)} ({basis_of(r, ans)}) | 정답 {sorted(t)} {[names.get(x) for x in t]} | 앞 파일 줄 {src_item['이름']} {src_item['규격']} 출처 {src_item['근거']['출처']}")
|
||||
@@ -0,0 +1,87 @@
|
||||
"""C6 — 「경우에 따라서는」 가름 흉내: 열쇠 차례(a → c → b → c2) + 근거 판정 · 발주처로 「바로 씀 / 1순위 후보 / 뒤 후보」.
|
||||
|
||||
차례 누적(앞 파일 모두 기억)에서 판정줄 1순위 · 다섯 안 · 바로 들어간 줄 맞음 · 해로움.
|
||||
python s10_policy.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import Counter
|
||||
|
||||
from s08_memory import BASE, BY, ITEMS, META, ORDER, basis_of, build, keys_of, recall, truth # noqa: F401
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
CHAIN = ("a", "c", "b", "c2")
|
||||
|
||||
|
||||
def first_hit(r, memos, chain=CHAIN):
|
||||
for name in chain:
|
||||
ans, prov = recall(name, r, memos[name])
|
||||
if ans is not None:
|
||||
return name, ans, prov
|
||||
return None, None, []
|
||||
|
||||
|
||||
def tier(rule, name, ans, prov, r, target):
|
||||
"""바로 · 1순위 · 뒤 · 없음."""
|
||||
if ans is None:
|
||||
return "없음"
|
||||
basis = basis_of(r, ans)
|
||||
same_org = any(META[f][0] == META[target][0] for f, _ in prov)
|
||||
if rule == "지금(a 바로)":
|
||||
return "바로" if name == "a" else "없음"
|
||||
if rule == "a·b 바로(넓히기만)":
|
||||
return "바로" if name in ("a", "b") else "없음"
|
||||
if rule == "모든 열쇠 바로":
|
||||
return "바로"
|
||||
if rule == "근거 가름":
|
||||
if basis == "근거 다름":
|
||||
return "뒤"
|
||||
return "바로" if name in ("a", "c") else "1순위"
|
||||
if rule == "근거 가름 + 발주처":
|
||||
if same_org and name in ("a", "b", "c"):
|
||||
return "바로"
|
||||
if basis == "근거 다름":
|
||||
return "뒤"
|
||||
return "바로" if name in ("a", "c") else "1순위"
|
||||
raise ValueError(rule)
|
||||
|
||||
|
||||
RULES = ("지금(a 바로)", "a·b 바로(넓히기만)", "모든 열쇠 바로", "근거 가름", "근거 가름 + 발주처")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("규칙 | 판정줄 | 1위 전→뒤 | 5안 전→뒤 | 바로 들어감(맞음) | 1순위 후보(맞음) | 뒤 후보(맞음) | 도움 · 해로움")
|
||||
for rule in RULES:
|
||||
s = Counter()
|
||||
for at, target in enumerate(ORDER[1:], 1):
|
||||
memos = {name: build(ORDER[:at], name) for name in CHAIN}
|
||||
for r in ITEMS:
|
||||
if r["파일"] != target or not truth(r["id"]):
|
||||
continue
|
||||
t = truth(r["id"])
|
||||
base = BASE[r["id"]]["top"]
|
||||
chain = {"지금(a 바로)": ("a",), "a·b 바로(넓히기만)": ("a", "b")}.get(rule, CHAIN)
|
||||
name, ans, prov = first_hit(r, memos, chain)
|
||||
tr = tier(rule, name, ans, prov, r, target)
|
||||
if tr in ("바로", "1순위"):
|
||||
top = [ans, *[x for x in base if x != ans]]
|
||||
elif tr == "뒤":
|
||||
top = [*base[:1], ans, *[x for x in base[1:] if x != ans]]
|
||||
else:
|
||||
top = base
|
||||
before = bool(base[:1]) and base[0] in t
|
||||
after = bool(top[:1]) and top[0] in t
|
||||
s["n"] += 1
|
||||
s["전1"] += before
|
||||
s["뒤1"] += after
|
||||
s["전5"] += bool(set(base[:5]) & t)
|
||||
s["뒤5"] += bool(set(top[:5]) & t)
|
||||
if tr != "없음":
|
||||
s[tr] += 1
|
||||
s[tr + "맞음"] += ans in t
|
||||
s["도움"] += after and not before
|
||||
s["해로움"] += before and not after
|
||||
n = s["n"]
|
||||
print(
|
||||
f"{rule} | {n} | {100 * s['전1'] / n:.1f}→{100 * s['뒤1'] / n:.1f} | {100 * s['전5'] / n:.1f}→{100 * s['뒤5'] / n:.1f} "
|
||||
f"| {s['바로']}({s['바로맞음']}) | {s['1순위']}({s['1순위맞음']}) | {s['뒤']}({s['뒤맞음']}) | {s['도움']} · {s['해로움']}"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""C6 — 덧셈: ① 한 파일 안 같은 열쇠 다른 답(충돌) ② 차례 누적에서 열쇠 사슬(a→c→b→c2)이 닿는 엑셀 줄 무게."""
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from s08_memory import ITEMS, ORDER, KEYS, keys_of, truth, build
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
print("① 한 파일 안 — 정답 있는 줄 둘 넘게 같은 열쇠: 열쇠 묶음 수 · 답이 갈린 묶음")
|
||||
for name in KEYS:
|
||||
groups = defaultdict(list)
|
||||
for r in ITEMS:
|
||||
t = truth(r["id"])
|
||||
if t:
|
||||
for k in keys_of(name, r):
|
||||
groups[(r["파일"], k)].append(frozenset(t))
|
||||
multi = [v for v in groups.values() if len(v) > 1]
|
||||
split = [v for v in multi if not frozenset.intersection(*v)]
|
||||
print(f" {name}: 묶음 {len(multi)} · 갈림 {len(split)}")
|
||||
print("② 차례 누적 — 줄(고유) · 엑셀 줄(되풀이 셈) 중 앞 파일 기억이 닿는 몫(모두 확정 셈)")
|
||||
for chain in (("a",), ("a", "b"), ("a", "c"), ("a", "c", "b", "c2")):
|
||||
u = w = nu = nw = 0
|
||||
for at, target in enumerate(ORDER[1:], 1):
|
||||
memos = {n: build(ORDER[:at], n, all_items=True) for n in chain}
|
||||
for r in ITEMS:
|
||||
if r["파일"] != target:
|
||||
continue
|
||||
nu += 1; nw += r["줄수"]
|
||||
if any(k in memos[n] for n in chain for k in keys_of(n, r)):
|
||||
u += 1; w += r["줄수"]
|
||||
print(f" {'→'.join(chain)}: 고유 {u}/{nu} ({100*u/nu:.0f}%) · 엑셀 줄 {w}/{nw} ({100*w/nw:.0f}%)")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""C6 — 새 지표 「로직 조합 맞음」: 호표 하나의 로직 줄(정답 있는 줄)이 모두 1순위 맞으면 그 호표 조합 맞음.
|
||||
|
||||
판정 = 사람 ∪ 자동(엄격) · 정답 없는 줄은 셈에서 뺌 · 판정 줄 하나도 없는 호표는 뺌.
|
||||
바탕 = 지금 맞춤(출처직종 · 씨앗 켬/끔) · 기억 = 차례 누적 + 「근거 가름(+ 발주처)」 규칙(s10).
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from s08_memory import BY, HOPYO, ITEMS, ORDER, build, truth
|
||||
from s10_policy import CHAIN, first_hit, tier
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
HERE = Path(__file__).resolve().parent
|
||||
M = json.loads((HERE / "match.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def tops(base_key, rule=None):
|
||||
"""{id: 1순위} — rule 있으면 차례 누적 기억을 얹음."""
|
||||
base = M[base_key]
|
||||
out = {i: (v["top"][:1] or [None])[0] for i, v in base.items()}
|
||||
if rule is None:
|
||||
return out
|
||||
for at, target in enumerate(ORDER[1:], 1):
|
||||
memos = {name: build(ORDER[:at], name) for name in CHAIN}
|
||||
for r in ITEMS:
|
||||
if r["파일"] != target:
|
||||
continue
|
||||
name, ans, prov = first_hit(r, memos)
|
||||
if tier(rule, name, ans, prov, r, target) in ("바로", "1순위"):
|
||||
out[r["id"]] = ans
|
||||
return out
|
||||
|
||||
|
||||
def combo_rate(top):
|
||||
by_file = defaultdict(lambda: [0, 0])
|
||||
for (f, h), ids in HOPYO.items():
|
||||
judged = [i for i in ids if i in BY and truth(i)]
|
||||
if not judged:
|
||||
continue
|
||||
by_file[f][0] += 1
|
||||
by_file[f][1] += all(top.get(i) in truth(i) for i in judged)
|
||||
total = [sum(v[0] for v in by_file.values()), sum(v[1] for v in by_file.values())]
|
||||
return by_file, total
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("바탕 · 기억 | 호표(판정 줄 있음) | 조합 맞음 | 파일별(맞음/호표)")
|
||||
for key in ("출처만|켬", "출처직종|켬", "출처직종|끔"):
|
||||
for rule in (None, "근거 가름", "근거 가름 + 발주처"):
|
||||
by, tot = combo_rate(tops(key, rule))
|
||||
cells = " · ".join(f"{f} {v[1]}/{v[0]}" for f, v in by.items())
|
||||
print(f"{key} · {rule or '기억 없음'} | {tot[0]} | {tot[1]} ({100 * tot[1] / tot[0]:.1f}%) | {cells}")
|
||||
@@ -0,0 +1,23 @@
|
||||
"""C6 — 조건별 맞음을 「서로 다른 판정 줄」로 다시 셈(짝 · 누적에서 같은 줄을 여러 번 세지 않게)."""
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from s08_memory import ITEMS, ORDER, META, KEYS, build, recall, truth, basis_of, HAND
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
seen = defaultdict(dict) # (열쇠, 조건) → {(id, 답): 맞음}
|
||||
for src_set in [[s] for s in ORDER] + [ORDER[:i] for i in range(1, len(ORDER))]:
|
||||
for name in KEYS:
|
||||
memo = build(src_set, name)
|
||||
for r in ITEMS:
|
||||
if r["파일"] in src_set or not truth(r["id"]):
|
||||
continue
|
||||
ans, prov = recall(name, r, memo)
|
||||
if ans is None:
|
||||
continue
|
||||
org = "같은 발주처" if any(META[f][0] == META[r["파일"]][0] for f, _ in prov) else "다른 발주처"
|
||||
ok = ans in truth(r["id"])
|
||||
who = "사람" if r["id"] in HAND else "자동"
|
||||
for cond in ("모두", org, basis_of(r, ans), f"{org} · {basis_of(r, ans)}", f"판정 {who}"):
|
||||
seen[(name, cond)][(r["id"], ans)] = ok
|
||||
for (name, cond), d in sorted(seen.items()):
|
||||
n = len(d); ok = sum(d.values()); ids = len({i for i, _ in d})
|
||||
print(f"{name} | {cond} | 서로 다른 (줄·답) {n} · 맞음 {ok} ({100*ok/n:.0f}%) · 줄 {ids}")
|
||||
@@ -0,0 +1,205 @@
|
||||
"""req226 C7 — C1 · C3 · C4 · C5 · C6 함수를 한 프로세스에 불러 겹쳐 잼(저장소 안 고침 · 읽기만).
|
||||
|
||||
단계 = 지금 → 묶음 ① → + 로직 지문(C4 추천) → + (가) · 조합 풀이(C5) → + 조건부 기억(C6 근거 가름) → + 읽기 넓힘(영월 직종 이음).
|
||||
정답 · 셈 = C1 s10_all(고유 136 · 정답 126) · 조합 맞음 = C5 c07 T1(덮을 수 있는 83 호표 통째 엄격).
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
HERE = Path(__file__).resolve().parent
|
||||
PAD = HERE.parent
|
||||
C1, C3 = PAD.parent / "req225" / "C1", PAD.parent / "req225" / "C3"
|
||||
C4, C5, C6 = PAD / "C4", PAD / "C5", PAD / "C6"
|
||||
for p in (ROOT, C1, C3, C4, C5, C6):
|
||||
if str(p) not in sys.path:
|
||||
sys.path.insert(0, str(p))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.setrecursionlimit(10000)
|
||||
|
||||
import s10_all as S # noqa: E402 C1 — 정답 · 셈 · 지금
|
||||
from s04_eval import clause_prefix, hb, hm, patched # noqa: E402
|
||||
from s10_all import candidates_nofold, no_seeds # noqa: E402
|
||||
from s20_c3_extra import combo # noqa: E402 C3 — 묶음 ①
|
||||
import s3_match as F4 # noqa: E402 C4 — 로직 지문
|
||||
import c04_closed as CL # noqa: E402 C5 — (가)
|
||||
import c05_solve as SV # noqa: E402 C5 — 조합 풀이
|
||||
from c06_lines import PARTS # noqa: E402
|
||||
import c07_hopyo as H # noqa: E402 C5 — 조합 맞음
|
||||
import s08_memory as M8 # noqa: E402 C6 — 기억
|
||||
from s10_policy import CHAIN, first_hit, tier # noqa: E402
|
||||
|
||||
LABELED = ("동부청영월", "북부청임도", "북부청사방")
|
||||
C6BY = {r["id"]: r for r in M8.ITEMS}
|
||||
IDS = list(S.BY)
|
||||
|
||||
|
||||
# ── 단계 ──────────────────────────────────────────────────────────────
|
||||
def now():
|
||||
return S.run()
|
||||
|
||||
|
||||
def bundle1():
|
||||
return combo()
|
||||
|
||||
|
||||
_POOLS = {}
|
||||
|
||||
|
||||
def pools(ids=IDS):
|
||||
for i in ids:
|
||||
if i not in _POOLS:
|
||||
_POOLS[i] = F4.rank(F4.EXP[i], None, 40)
|
||||
return _POOLS
|
||||
|
||||
|
||||
def finger(rec, beta=1.0, keep_same=True):
|
||||
"""C4 추천 — 「같음」은 묶음 ① 그대로 · 나머지 = 지문 점수 + β × 묶음 ① 차례 덤."""
|
||||
P = pools()
|
||||
out = {}
|
||||
for i in IDS:
|
||||
if keep_same and rec[i]["상태"] == "같음":
|
||||
out[i] = dict(rec[i])
|
||||
continue
|
||||
F4.CUR["id"] = i
|
||||
out[i] = {"상태": "후보", "top": F4.fuse(None, rec[i], beta, P[i])}
|
||||
return out
|
||||
|
||||
|
||||
def ga(res):
|
||||
"""C5 (가) 좋은 것 — 토질 · 같은 이름 다수 · 조합 안(근거 지킴)."""
|
||||
return CL.apply_combo(CL.apply_name_vote(CL.apply_soil(res)), "inside", True)[0]
|
||||
|
||||
|
||||
def solve(res, cut=0.35, seeds=True):
|
||||
"""C5 조합 풀이 + 섞인 줄 1순위 바꿈 → (줄 결과, 고른 로직 모음)."""
|
||||
tops, sets = {}, {}
|
||||
ctx = no_seeds() if not seeds else _null()
|
||||
with ctx, patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold):
|
||||
hm._targets.cache_clear()
|
||||
hb._ours.cache_clear()
|
||||
got = hm.targets()
|
||||
for r in S.ALL:
|
||||
i = r["id"]
|
||||
line = {k: r.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")}
|
||||
top = list(res[i]["top"])
|
||||
parts = PARTS.get(i) or []
|
||||
chosen = SV.combine((top or [None])[0], parts, line, got, SV.merged(line, got))
|
||||
codes = list(dict.fromkeys(c for c, *_ in chosen if c))
|
||||
if top and len(codes) > 1 and parts and SV.is_mixed(parts, S.BY[i]) and SV.cover(SV._sum(parts), top[0]) < cut:
|
||||
rest = [c for c in codes if c != top[0]]
|
||||
labor = [c for c, _w, _f, _x, step in chosen if c in rest and step in ("몫", "절")]
|
||||
if labor:
|
||||
codes = rest
|
||||
top = [labor[0]] + [c for c in top if c != labor[0]][:4]
|
||||
tops[i] = {"상태": res[i]["상태"], "top": top}
|
||||
sets[i] = codes
|
||||
return tops, sets
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _null():
|
||||
yield
|
||||
|
||||
|
||||
RULE_CHAIN = {"지금(a 바로)": ("a",), "a·b 바로(넓히기만)": ("a", "b")}
|
||||
|
||||
|
||||
def memory(res, sets=None, rule="근거 가름", order=M8.ORDER):
|
||||
"""C6 조건부 기억 — 차례 누적(앞 파일 모두 확정) · 열쇠 a → c → b → c2 · 근거 판정으로 바로 / 1순위 / 뒤."""
|
||||
out = {i: dict(v) for i, v in res.items()}
|
||||
sets = {i: list(v) for i, v in (sets or {i: (v["top"] or [])[:1] for i, v in res.items()}).items()}
|
||||
log = {}
|
||||
for at, target in enumerate(order[1:], 1):
|
||||
if target not in LABELED:
|
||||
continue
|
||||
memos = {name: M8.build(order[:at], name) for name in CHAIN}
|
||||
for r in M8.ITEMS:
|
||||
i = r["id"]
|
||||
if r["파일"] != target or i not in res:
|
||||
continue
|
||||
name, ans, prov = first_hit(r, memos, RULE_CHAIN.get(rule, CHAIN))
|
||||
tr = tier(rule, name, ans, prov, r, target)
|
||||
top = list(res[i]["top"])
|
||||
if tr in ("바로", "1순위"):
|
||||
old = top[0] if top else None
|
||||
out[i] = {"상태": "기억", "top": [ans] + [c for c in top if c != ans][:4]}
|
||||
sets[i] = [ans] + [c for c in sets.get(i, []) if c not in (old, ans)]
|
||||
elif tr == "뒤":
|
||||
out[i] = {**res[i], "top": top[:1] + [ans] + [c for c in top[1:] if c != ans][:3]}
|
||||
if tr != "없음":
|
||||
log[i] = (name, tr, ans)
|
||||
return out, sets, log
|
||||
|
||||
|
||||
@contextmanager
|
||||
def read_wide():
|
||||
"""읽기 넓힘 — 영월(STmate) 근거 직종을 노무비 목록으로 이음(C6 items 근거) · 끝나면 되돌림."""
|
||||
keep = {}
|
||||
for r in S.ALL:
|
||||
if r["파일"] == "동부청영월":
|
||||
keep[r["id"]] = r.get("근거")
|
||||
r["근거"] = dict(C6BY[r["id"]]["근거"])
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for r in S.ALL:
|
||||
if r["id"] in keep:
|
||||
r["근거"] = keep[r["id"]]
|
||||
|
||||
|
||||
# ── 셈 ────────────────────────────────────────────────────────────────
|
||||
def hopyo(chosen_fn):
|
||||
"""조합 맞음 — C5 T1 · 덮을 수 있는 호표 통째 엄격 (맞음, 호표 수, 칸 %)."""
|
||||
H.chosen_of = lambda i, pick: set(chosen_fn(i)) - {None}
|
||||
res = {k: H.judge(k, "x", False) for k in H.HOPYO}
|
||||
res = {k: v for k, v in res.items() if v}
|
||||
keys = [k for k, v in res.items() if not v["없음칸"]]
|
||||
ok = sum(res[k]["엄격"]["통째"] for k in keys)
|
||||
cell = round(100 * sum(res[k]["엄격"]["칸"] for k in keys) / len(keys), 1)
|
||||
return ok, len(keys), cell
|
||||
|
||||
|
||||
def top1_set(res):
|
||||
return lambda i: (res[i]["top"] or [None])[:1]
|
||||
|
||||
|
||||
def combo_set(res, sets):
|
||||
"""1순위 + 조합 풀이 덧(기계 시간 로직은 뺌 — 정할 것 8 기본안 T1)."""
|
||||
|
||||
def f(i):
|
||||
top = (res[i]["top"] or [None])[0]
|
||||
return [c for c in sets.get(i, []) if c and (c == top or c not in SV.MACHINE_SET)] or [top]
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def row(name, res, chosen_fn=None):
|
||||
t = S.table(name, res)
|
||||
a = t["전체"]
|
||||
h = hopyo(chosen_fn or top1_set(res))
|
||||
return {
|
||||
"단계": name,
|
||||
"1위": f"{a['1위 엄격']} / {a['1위 느슨']}",
|
||||
"5안": f"{a['5안 엄격']} / {a['5안 느슨']}",
|
||||
"임도·사방·영월 1위": f"{t['북부청임도']['1위 엄격']} · {t['북부청사방']['1위 엄격']} · {t['동부청영월']['1위 엄격']}",
|
||||
"줄무게 1위 느슨": a["줄 1위 느슨"],
|
||||
"조합 맞음": f"{round(100 * h[0] / h[1], 1)} ({h[0]}/{h[1]})",
|
||||
"칸": h[2],
|
||||
"없음 줄 후보": a["없음인데 후보"],
|
||||
}
|
||||
|
||||
|
||||
def show(r):
|
||||
print(" | ".join(f"{k} {v}" for k, v in r.items()))
|
||||
|
||||
|
||||
def save(name, obj):
|
||||
(HERE / name).write_text(json.dumps(obj, ensure_ascii=False, indent=1, default=str), "utf-8")
|
||||
|
||||
|
||||
def load(name):
|
||||
return json.loads((HERE / name).read_text(encoding="utf-8"))
|
||||
@@ -0,0 +1,82 @@
|
||||
"""req226 C7 ① 누적 실력 — 같은 정답 · 같은 셈(C1 고유 126 줄 · 조합 맞음 = C5 T1 83 호표).
|
||||
|
||||
python k1_stack.py [켬|끔] → 표 · stack_<켬|끔>.json
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import k0_lib as K
|
||||
from k0_lib import S
|
||||
|
||||
SEEDS = "끔" not in sys.argv[1:]
|
||||
MIXED = {i for i in S.BY if K.PARTS.get(i) and K.SV.is_mixed(K.PARTS[i], S.BY[i])}
|
||||
|
||||
|
||||
def mixfirst(fp, ga1):
|
||||
"""섞인 줄(몫 표시 + 노임만 든 몫 + 장비만 든 몫)은 몫 풀이 먼저(C5 길) · 나머지는 지문 1순위."""
|
||||
return {i: dict(ga1[i] if i in MIXED else fp[i]) for i in S.BY}
|
||||
|
||||
|
||||
def stages(wide=False):
|
||||
ctx = K.read_wide() if wide else K._null()
|
||||
rows, keep = [], {}
|
||||
with ctx:
|
||||
if SEEDS:
|
||||
now, b1 = K.now(), K.bundle1()
|
||||
else:
|
||||
with K.no_seeds():
|
||||
now = K.now()
|
||||
b1 = K.bundle1()
|
||||
fp = K.finger(b1)
|
||||
ga1 = K.ga(b1)
|
||||
naive_t, naive_s = K.solve(fp, seeds=SEEDS)
|
||||
t3, s3 = K.solve(mixfirst(fp, ga1), seeds=SEEDS)
|
||||
c5_t, c5_s = K.solve(ga1, seeds=SEEDS)
|
||||
t4, s4, log = K.memory(t3, s3)
|
||||
tag = " · 읽기 넓힘" if wide else ""
|
||||
rows.append(K.row("지금" + tag, now))
|
||||
rows.append(K.row("묶음 ①" + tag, b1))
|
||||
rows.append(K.row("+ 로직 지문" + tag, fp))
|
||||
rows.append(K.row("(참고) 묶음 ① + (가) + 조합 풀이 = C5 길" + tag, c5_t, K.combo_set(c5_t, c5_s)))
|
||||
rows.append(K.row("(참고) + 지문 + 조합 풀이 그냥 겹침" + tag, naive_t, K.combo_set(naive_t, naive_s)))
|
||||
rows.append(K.row("+ 조합 풀이(섞인 줄 몫 먼저)" + tag, t3, K.combo_set(t3, s3)))
|
||||
rows.append(K.row("+ 조건부 기억(근거 가름)" + tag, t4, K.combo_set(t4, s4)))
|
||||
keep = {"지금": now, "묶음①": b1, "지문": fp, "조합": {"tops": t3, "sets": s3}, "기억": {"tops": t4, "sets": s4, "log": log}}
|
||||
return rows, keep
|
||||
|
||||
|
||||
def effect(before, after, ids=None):
|
||||
"""1순위 엄격 — 도움 · 해로움 줄(정답 있는 줄)."""
|
||||
ids = ids or [i for i in S.BY if S.LAB[i]["정답"]]
|
||||
ok = lambda t, i: bool(t) and t[0] in set(S.LAB[i]["정답"]) # noqa: E731
|
||||
help_ = [i for i in ids if ok(after[i]["top"], i) and not ok(before[i]["top"], i)]
|
||||
harm = [i for i in ids if ok(before[i]["top"], i) and not ok(after[i]["top"], i)]
|
||||
return help_, harm
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rows, keep = stages(False)
|
||||
rows_w, keep_w = stages(True)
|
||||
rows.append({**rows_w[-1], "단계": "+ 읽기 넓힘(영월 직종 이음)"})
|
||||
for r in rows + rows_w:
|
||||
K.show(r)
|
||||
out = {"표": rows, "넓힘 표": rows_w}
|
||||
for a, b in (("묶음①", "지문"), ("지문", "조합"), ("조합", "기억")):
|
||||
x = keep[a]["tops"] if "tops" in keep[a] else keep[a]
|
||||
y = keep[b]["tops"] if "tops" in keep[b] else keep[b]
|
||||
h, m = effect(x, y)
|
||||
out[f"{a}→{b}"] = {"도움": h, "해로움": m}
|
||||
print(f"{a} → {b}: 도움 {len(h)} {h} · 해로움 {len(m)} {m}")
|
||||
h, m = effect(keep["기억"]["tops"], keep_w["기억"]["tops"])
|
||||
out["기억→읽기"] = {"도움": h, "해로움": m}
|
||||
print(f"기억 → 읽기 넓힘: 도움 {len(h)} {h} · 해로움 {len(m)} {m}")
|
||||
log = keep["기억"]["log"]
|
||||
from collections import Counter
|
||||
|
||||
c = Counter((v[0], v[1]) for v in log.values())
|
||||
print("기억 닿음(열쇠 · 처리)", dict(c))
|
||||
right = Counter(v[1] for i, v in log.items() if S.LAB[i]["정답"] and v[2] in set(S.LAB[i]["정답"]))
|
||||
print("기억 답 맞음(처리별)", dict(right), "· 정답 없는 줄 닿음", sum(1 for i in log if not S.LAB[i]["정답"]))
|
||||
out["기억 닿음"] = {f"{k[0]}|{k[1]}": v for k, v in c.items()}
|
||||
out["기억 맞음"] = dict(right)
|
||||
K.save(f"stack_{'켬' if SEEDS else '끔'}.json", {**out, "줄": {k: v for k, v in keep.items()}, "줄 넓힘": keep_w})
|
||||
@@ -0,0 +1,31 @@
|
||||
"""req226 C7 ② 겹침 — 기억을 단계마다 얹어 봄 · 맨 위 남는 실패 줄 목록.
|
||||
python k2_detail.py [켬|끔]"""
|
||||
import sys
|
||||
import k0_lib as K
|
||||
from k0_lib import S
|
||||
from cats import NOW
|
||||
|
||||
tag = "끔" if "끔" in sys.argv[1:] else "켬"
|
||||
st = K.load(f"stack_{tag}.json")["줄"]
|
||||
names = {x["로직"]: x["이름"] for x in K.hm.targets()["로직"]}
|
||||
have = [i for i in S.BY if S.LAB[i]["정답"]]
|
||||
ok = lambda t, i: bool(t) and t[0] in set(S.LAB[i]["정답"]) # noqa: E731
|
||||
for nm in ("지금", "묶음①", "지문"):
|
||||
res = st[nm]
|
||||
m, _, log = K.memory(res)
|
||||
a = sum(ok(res[i]["top"], i) for i in have)
|
||||
b = sum(ok(m[i]["top"], i) for i in have)
|
||||
hp = sum(1 for i in have if ok(m[i]["top"], i) and not ok(res[i]["top"], i))
|
||||
hm_ = sum(1 for i in have if ok(res[i]["top"], i) and not ok(m[i]["top"], i))
|
||||
print(f"[{tag}] {nm} + 기억: 1위 엄격 {100*a/len(have):.1f} → {100*b/len(have):.1f} · 도움 {hp} · 해로움 {hm_}")
|
||||
top = st["기억"]["tops"]
|
||||
fails = [i for i in have if not ok(top[i]["top"], i)]
|
||||
print(f"[{tag}] 맨 위 남는 1순위 실패 {len(fails)}")
|
||||
for i in fails:
|
||||
t = top[i]["top"]
|
||||
lab = S.LAB[i]["정답"]
|
||||
rank = next((k + 1 for k, c in enumerate(t[:5]) if c in set(lab)), "-")
|
||||
loose = "느슨O" if t and t[0] in set(lab) | set(S.LAB[i]["허용"]) else ""
|
||||
print(f" {i} {S.BY[i]['이름'][:18]:<18} 줄{S.BY[i]['줄수']:>3} | 1위 {names.get(t[0] if t else None, '')[:16]:<16} | 정답 {names.get(lab[0], '')[:14]} (다섯 안 {rank}) {loose} | {(NOW.get(i) or ['-'])[0]}")
|
||||
none = [i for i in S.BY if not S.LAB[i]["정답"]]
|
||||
print(f"[{tag}] 우리에 없음 {len(none)} — 1위 후보:", [(i, S.BY[i]['이름'][:10], names.get((top[i]['top'] or [None])[0], '')[:12]) for i in none])
|
||||
@@ -0,0 +1,139 @@
|
||||
"""req226 C7 ③ C4 지문 점수를 무게 바꿔 끼울 수 있게 미리 셈(채널 · 변형별) → 가림 시험용.
|
||||
|
||||
점수 = Σ 무게 × 채널(단위 · 이름 · 조항 · 조건 · 자재 · 율) + max_변형(무게 인력 × 인력 − 0.6 · 인력 벌 + 무게 장비 × 장비 − 0.4 · 장비 벌)
|
||||
= C4 s3_match.score_pair 와 같은 식(무게만 바꿈).
|
||||
python k3_vec.py 지문|가벼움 → vec_<…>.npz
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
import k0_lib as K
|
||||
from fp_common import conds, mtypes, specnums # C4
|
||||
|
||||
F4 = K.F4
|
||||
P = F4.P
|
||||
CODES = [o["로직"] for o in P]
|
||||
IDX = {c: k for k, c in enumerate(CODES)}
|
||||
SEG = np.cumsum([0] + [len(o["vs"]) for o in P])[:-1]
|
||||
CH = ("단위", "이름", "조항", "조건", "자재", "율")
|
||||
W0 = dict(F4.W)
|
||||
|
||||
|
||||
def chan(e, o):
|
||||
unit_ok = F4.unit_fit(e["단위"], o["단위"])
|
||||
g1, g2 = e["nameg"], o["nameg"]
|
||||
our = F4.OURC.get(o["로직"])
|
||||
c = F4.clause_prefix(e["theirs"], our)[0] if (our and e["theirs"]) else 0.0
|
||||
hit, _ = F4.cond_score(e["조건"], o["nc"], o["oc"])
|
||||
_, clash = F4.cond_score(e["이름조건"], o["nc"], {})
|
||||
mat = 0.0
|
||||
if e["matl"]:
|
||||
mat = sum(1 for ws in e["matl"] if any(a in b or b in a for a in ws for b in o["matw"])) / len(e["matl"])
|
||||
base = (
|
||||
1.0 if unit_ok else 0.0,
|
||||
2 * len(g1 & g2) / (len(g1) + len(g2)) if g1 and g2 else 0.0,
|
||||
c * (0.5 if e["옛판"] else 1.0),
|
||||
hit - clash,
|
||||
mat,
|
||||
1.0 if (e["pctv"] and o["pctv"] and e["pctv"] & o["pctv"]) else 0.0,
|
||||
)
|
||||
vs = []
|
||||
for v in o["vs"]:
|
||||
if e["E"]:
|
||||
ls, pl = F4.lab_sim(e["E"], e["Eb"], v["L"], unit_ok)[0], 0.0
|
||||
else:
|
||||
ls, pl = 0.0, 1.0 if v["L"] else 0.0
|
||||
if e["장비"]:
|
||||
ms, px = F4.mach_sim(e["장비"], v["X"], o["opt"], unit_ok), 0.0
|
||||
else:
|
||||
ms, px = 0.0, 1.0 if (v["X"] or o["opt"]) else 0.0
|
||||
vs.append((ls, pl, ms, px))
|
||||
return base, vs
|
||||
|
||||
|
||||
def build(elist):
|
||||
n, L, V = len(elist), len(P), int(SEG[-1] + len(P[-1]["vs"]))
|
||||
base = np.zeros((n, L, len(CH)))
|
||||
var = np.zeros((4, n, V))
|
||||
for a, e in enumerate(elist):
|
||||
col = 0
|
||||
for b, o in enumerate(P):
|
||||
bs, vs = chan(e, o)
|
||||
base[a, b] = bs
|
||||
for t in vs:
|
||||
var[:, a, col] = t
|
||||
col += 1
|
||||
return base, var
|
||||
|
||||
|
||||
def scores(base, var, W=None):
|
||||
W = W or W0
|
||||
v = W["인력"] * var[0] - 0.6 * var[1] + W["장비"] * var[2] - 0.4 * var[3]
|
||||
vmax = np.maximum.reduceat(v, SEG, axis=1)
|
||||
w6 = np.array([W.get(k, 0.0) for k in CH])
|
||||
return base @ w6 + vmax
|
||||
|
||||
|
||||
RB = F4.RB
|
||||
|
||||
|
||||
def fused_top(srow, base_top, beta, n=40):
|
||||
"""F4.fuse 와 같은 차례 — 지문 상위 40(점수 차례) + 묶음 ① 다섯 · 점수 + β × 차례 덤 · 안정 정렬."""
|
||||
pool = np.argsort(-srow, kind="stable")[:n]
|
||||
cand = [int(k) for k in pool]
|
||||
for c in base_top[:5]:
|
||||
k = IDX.get(c)
|
||||
if k is not None and k not in cand:
|
||||
cand.append(k)
|
||||
bonus = {IDX[c]: RB.get(r, 0) for r, c in enumerate(base_top[:5]) if c in IDX}
|
||||
order = sorted(cand, key=lambda k: -(srow[k] + beta * bonus.get(k, 0)))
|
||||
return [CODES[k] for k in order[:5]]
|
||||
|
||||
|
||||
# ── 넓힌 파일 줄 — C6 items 로 가벼운 엑셀 지문(인력 품 · 장비 갈래 · 조항 · 조건 · 율 — 자재 · 장비 시간 · 섞음 밑품 없음) ──
|
||||
_PCT = re.compile(r"([가-힣]+)의\s*([\d.]+)\s*%")
|
||||
_OLD = re.compile(r"((?<![\d,.])20(0\d|1\d|20)(?![\d,.])|(?<!\d)(1\d|20)년|임도품셈|임도표준품셈|사방품셈|사방사업표준품셈)")
|
||||
|
||||
|
||||
def lite(r):
|
||||
text = [str(t) for t in (r.get("글") or [])]
|
||||
nm, sp, unit = r["이름"], r.get("규격") or "", r.get("단위") or ""
|
||||
head = " ".join([nm, sp] + text[:4])
|
||||
mach = []
|
||||
for k in r.get("장비") or []:
|
||||
kinds = sorted(mtypes(k) or {k})
|
||||
hours = 1.0 if (not text and re.search(r"(hr|시간)", unit, re.I)) else None
|
||||
mach.append({"갈래": kinds, "규격": sorted(specnums(f"{nm} {sp}")) if not text else [], "시간": hours, "밑시간": None})
|
||||
e = {
|
||||
"id": r["id"], "파일": r["파일"], "이름": nm, "규격": sp, "단위": unit, "갈래": r.get("갈래"),
|
||||
"출처": r["근거"]["출처"], "직종근거": r["근거"]["직종"],
|
||||
"인력": {t: {"품": q, "밑품": q} for t, q in (r.get("품") or {}).items()},
|
||||
"장비": mach, "자재": [], "율": [[k, float(v)] for t in text for k, v in _PCT.findall(t)],
|
||||
"조건": {k: sorted(v) for k, v in conds(head).items()},
|
||||
"이름조건": {k: sorted(v) for k, v in conds(f"{nm} {sp}").items()},
|
||||
"옛판": bool(_OLD.search(" ".join(text[:6]))),
|
||||
}
|
||||
return F4.ex_prep(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind = sys.argv[1] if len(sys.argv) > 1 else "지문"
|
||||
if kind == "지문": # C4 엑셀 지문 136
|
||||
ids = list(K.S.BY)
|
||||
elist = [F4.EXP[i] for i in ids]
|
||||
else: # 가벼운 지문 — C6 items(호표 쓰임 · 북부청 · 영월 136 도 견줌용으로)
|
||||
import json
|
||||
|
||||
items = json.loads((K.C6 / "items.json").read_text(encoding="utf-8"))
|
||||
w = K.load("wide.json")
|
||||
want = set(K.S.BY) if sys.argv[2] == "사람" else set(w["자동"]) | set(w["씨앗"])
|
||||
rows = [r for r in items if r["id"] in want]
|
||||
ids = [r["id"] for r in rows]
|
||||
elist = [lite(r) for r in rows]
|
||||
base, var = build(elist)
|
||||
tag = kind if kind == "지문" else f"가벼움_{sys.argv[2]}"
|
||||
np.savez_compressed(K.HERE / f"vec_{tag}.npz", base=base, var=var, ids=np.array(ids))
|
||||
print(tag, base.shape, var.shape)
|
||||
@@ -0,0 +1,68 @@
|
||||
"""req226 C7 ④ 넓힌 파일 줄(STmate 5 · 거창) — 정답 셋 · 지금 · 묶음 ① (읽기 출처만 / 출처직종 · 씨앗 끔 = 처음 보는 셈).
|
||||
|
||||
정답 = C6 그대로: 자동 엄격(사람 정답 없는 새 줄) · 씨앗 짝(씨앗 끔에서만 · 북부청 · 사람 정답 줄 뺌).
|
||||
python k4_wide.py → wide.json
|
||||
"""
|
||||
import json
|
||||
|
||||
import k0_lib as K
|
||||
from k0_lib import hb, hm, patched, clause_prefix, candidates_nofold, no_seeds
|
||||
from s04_eval import default_line
|
||||
from s16_guard import typo_guard
|
||||
from s06_match import line_of # C6
|
||||
from s04_auto import seeds_of # C6
|
||||
|
||||
ITEMS = json.loads((K.C6 / "items.json").read_text(encoding="utf-8"))
|
||||
AUTO = json.loads((K.C6 / "auto.json").read_text(encoding="utf-8"))
|
||||
HAND = set(K.S.LAB)
|
||||
|
||||
|
||||
def answers():
|
||||
got = hm.targets() # 씨앗 켬 상태로 씨앗 짝을 읽음
|
||||
auto, seed = {}, {}
|
||||
for r in ITEMS:
|
||||
i = r["id"]
|
||||
if i in HAND:
|
||||
continue
|
||||
a = AUTO.get(i, {}).get("정답")
|
||||
if a:
|
||||
auto[i] = [a]
|
||||
if r["파일"] not in ("북부청임도", "북부청사방"):
|
||||
s = seeds_of(r, got)
|
||||
if s:
|
||||
seed[i] = sorted(s)
|
||||
return auto, seed
|
||||
|
||||
|
||||
def run(rows, variant, fn):
|
||||
with no_seeds(), patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold):
|
||||
hm._targets.cache_clear()
|
||||
hb._ours.cache_clear()
|
||||
got = hm.targets()
|
||||
return {r["id"]: fn(line_of(r, variant), got, {"단산글": r.get("글") or []}) for r in rows}
|
||||
|
||||
|
||||
def run_now(rows, variant):
|
||||
with no_seeds():
|
||||
hm._targets.cache_clear()
|
||||
got = hm.targets()
|
||||
return {r["id"]: default_line(line_of(r, variant), got, {}) for r in rows}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
auto, seed = answers()
|
||||
ids = set(auto) | set(seed)
|
||||
rows = [r for r in ITEMS if r["id"] in ids]
|
||||
out = {"자동": auto, "씨앗": seed, "줄": {r["id"]: {"파일": r["파일"], "이름": r["이름"], "호표쓰임": r["호표쓰임"]} for r in rows}}
|
||||
for v in ("출처만", "출처직종"):
|
||||
out[f"지금|{v}"] = run_now(rows, v)
|
||||
out[f"묶음①|{v}"] = run(rows, v, typo_guard)
|
||||
K.save("wide.json", out)
|
||||
from collections import Counter
|
||||
print("자동 새 줄", len(auto), Counter(ITEMS_BY[i]["파일"] for i in auto) if (ITEMS_BY := {r["id"]: r for r in ITEMS}) else "", "· 씨앗 짝", len(seed), Counter(ITEMS_BY[i]["파일"] for i in seed), "· 겹침", len(set(auto) & set(seed)))
|
||||
for k in ("지금|출처만", "지금|출처직종", "묶음①|출처만", "묶음①|출처직종"):
|
||||
for nm, ans in (("자동", auto), ("씨앗", seed)):
|
||||
n = len(ans)
|
||||
a1 = sum(1 for i in ans if out[k][i]["top"][:1] and out[k][i]["top"][0] in set(ans[i]))
|
||||
a5 = sum(1 for i in ans if set(out[k][i]["top"][:5]) & set(ans[i]))
|
||||
print(f"{k} · {nm} {n}: 1위 {100*a1/n:.1f} · 다섯 안 {100*a5/n:.1f}")
|
||||
@@ -0,0 +1,115 @@
|
||||
"""req226 C7 ⑤ 가림 시험 — 문턱 · 무게를 북부청 임도로만 정하고 사방 · 영월 · 넓힌 파일 줄에서 다시 잼.
|
||||
|
||||
손잡이 = C4 무게(인력 · 장비 · 조항 · 이름 · 조건) · β · 「같음 그대로」 · C5 섞인 줄 뺌 문턱(cut) · C6 기억 규칙.
|
||||
최종 1순위 = 기억(바로 · 1순위) > 섞인 줄 몫 먼저(C5 길 · cut) > 같음 그대로 > 지문(무게 · β).
|
||||
python k5_hold.py 준비 → hold_prep.json(섞인 줄 C5 길 · 기억 판단)
|
||||
python k5_hold.py 그물 <인력> → hold_<인력>.json(설정마다 무리별 1순위 · 다섯 안)
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
import k0_lib as K
|
||||
import k3_vec as V
|
||||
from k0_lib import S
|
||||
|
||||
FULL = np.load(K.HERE / "vec_지문.npz")
|
||||
LITE = np.load(K.HERE / "vec_가벼움_넓힘.npz")
|
||||
IDS_F, IDS_W = [str(x) for x in FULL["ids"]], [str(x) for x in LITE["ids"]]
|
||||
B1 = K.load("st_b1.json") # 묶음 ① · 씨앗 켬(126)
|
||||
WIDE = K.load("wide.json")
|
||||
BW = WIDE["묶음①|출처직종"] # 넓힌 줄 · 씨앗 끔 · 읽기 넓힘 뒤
|
||||
MIXED = {i for i in S.BY if K.PARTS.get(i) and K.SV.is_mixed(K.PARTS[i], S.BY[i])}
|
||||
GROUP_OF = {"북부청임도": "임도", "북부청사방": "사방", "동부청영월": "영월"}
|
||||
RULES = ("지금(a 바로)", "a·b 바로(넓히기만)", "모든 열쇠 바로", "근거 가름", "근거 가름 + 발주처")
|
||||
CUTS = (0.2, 0.35, 0.5)
|
||||
BETAS = (0.0, 0.5, 1.0, 2.0, 3.0)
|
||||
|
||||
# (줄, 무리, 정답) 짝 — 126 사람 정답 · 넓힌 자동 20 · 씨앗 짝 43
|
||||
PAIRS = [(i, GROUP_OF[S.BY[i]["파일"]], set(S.LAB[i]["정답"])) for i in IDS_F if S.LAB[i]["정답"]]
|
||||
PAIRS += [(i, "자동", set(v)) for i, v in WIDE["자동"].items()]
|
||||
PAIRS += [(i, "씨앗", set(v)) for i, v in WIDE["씨앗"].items()]
|
||||
GROUPS = ("임도", "사방", "영월", "자동", "씨앗")
|
||||
|
||||
|
||||
def mem_dec(rule):
|
||||
"""{id: (처리, 답)} — 차례 누적(앞 파일 모두 확정) · 대상 = 126 + 넓힌 줄."""
|
||||
items = {r["id"]: r for r in json.loads((K.C6 / "items.json").read_text(encoding="utf-8"))}
|
||||
want = set(IDS_F) | set(IDS_W)
|
||||
out = {}
|
||||
for at, target in enumerate(K.M8.ORDER[1:], 1):
|
||||
memos = {name: K.M8.build(K.M8.ORDER[:at], name) for name in K.CHAIN}
|
||||
for i in want:
|
||||
r = items[i]
|
||||
if r["파일"] != target:
|
||||
continue
|
||||
name, ans, prov = K.first_hit(r, memos, K.RULE_CHAIN.get(rule, K.CHAIN))
|
||||
tr = K.tier(rule, name, ans, prov, r, target)
|
||||
if tr != "없음":
|
||||
out[i] = (tr, ans)
|
||||
return out
|
||||
|
||||
|
||||
def prep():
|
||||
ga1 = K.ga(B1)
|
||||
c5 = {str(c): {i: v["top"] for i, v in K.solve(ga1, c)[0].items() if i in MIXED} for c in CUTS}
|
||||
dec = {r: mem_dec(r) for r in RULES}
|
||||
K.save("hold_prep.json", {"c5": c5, "dec": dec})
|
||||
|
||||
|
||||
def grid(wl):
|
||||
pre = K.load("hold_prep.json")
|
||||
n = len(PAIRS)
|
||||
gi = np.array([GROUPS.index(g) for _, g, _ in PAIRS])
|
||||
same = np.array([i in B1 and B1[i]["상태"] == "같음" for i, _, _ in PAIRS])
|
||||
same_ok = np.array([bool(B1[i]["top"]) and B1[i]["top"][0] in a if i in B1 else False for i, _, a in PAIRS])
|
||||
samew = np.array([i in BW and BW[i]["상태"] == "같음" for i, _, _ in PAIRS])
|
||||
samew_ok = np.array([bool(BW[i]["top"]) and BW[i]["top"][0] in a if i in BW else False for i, _, a in PAIRS])
|
||||
is126 = np.array([g in ("임도", "사방", "영월") for _, g, _ in PAIRS])
|
||||
mixed = np.array([i in MIXED for i, _, _ in PAIRS])
|
||||
c5_ok = {c: np.array([bool(pre["c5"][c].get(i)) and pre["c5"][c][i][0] in a if i in MIXED else False for i, _, a in PAIRS]) for c in pre["c5"]}
|
||||
mem_has, mem_ok = {}, {}
|
||||
for r, d in pre["dec"].items():
|
||||
mem_has[r] = np.array([i in d and d[i][0] in ("바로", "1순위") for i, _, _ in PAIRS])
|
||||
mem_ok[r] = np.array([i in d and d[i][1] in a for i, _, a in PAIRS])
|
||||
keep_same = np.where(is126, same, samew)
|
||||
keep_ok = np.where(is126, same_ok, samew_ok)
|
||||
rowF = {i: a for a, i in enumerate(IDS_F)}
|
||||
rowW = {i: a for a, i in enumerate(IDS_W)}
|
||||
out = []
|
||||
for wx, wc, wn, wk in itertools.product((1.0, 2.0, 3.0), (0.75, 1.5, 3.0), (0.3, 0.6, 1.2), (0.3, 0.6, 1.2)):
|
||||
W = {**V.W0, "인력": wl, "장비": wx, "조항": wc, "이름": wn, "조건": wk}
|
||||
SF = V.scores(FULL["base"], FULL["var"], W)
|
||||
SW = V.scores(LITE["base"], LITE["var"], W)
|
||||
for beta in BETAS:
|
||||
ok1, ok5 = np.zeros(n, bool), np.zeros(n, bool)
|
||||
for k, (i, g, a) in enumerate(PAIRS):
|
||||
if g in ("임도", "사방", "영월"):
|
||||
top = V.fused_top(SF[rowF[i]], B1[i]["top"], beta)
|
||||
else:
|
||||
top = V.fused_top(SW[rowW[i]], BW[i]["top"], beta)
|
||||
ok1[k] = bool(top) and top[0] in a
|
||||
ok5[k] = bool(set(top[:5]) & a)
|
||||
for keep in (True, False):
|
||||
fp1 = np.where(keep & keep_same, keep_ok, ok1)
|
||||
fp5 = np.where(keep & keep_same, keep_ok, ok5)
|
||||
for c in pre["c5"]:
|
||||
mx1 = np.where(mixed, c5_ok[c], fp1)
|
||||
for r in RULES:
|
||||
fin = np.where(mem_has[r], mem_ok[r], mx1)
|
||||
acc1 = np.bincount(gi, weights=fin, minlength=5) / np.bincount(gi, minlength=5)
|
||||
acc5 = np.bincount(gi, weights=fp5, minlength=5) / np.bincount(gi, minlength=5)
|
||||
out.append({"W": [wl, wx, wc, wn, wk], "β": beta, "같음": keep, "cut": c, "기억": r,
|
||||
"1위": [round(100 * x, 1) for x in acc1], "지문5안": [round(100 * x, 1) for x in acc5]})
|
||||
K.save(f"hold_{wl:g}.json", out)
|
||||
print(wl, len(out))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1] == "준비":
|
||||
prep()
|
||||
else:
|
||||
grid(float(sys.argv[2]))
|
||||
@@ -0,0 +1,55 @@
|
||||
"""req226 C7 ⑥ 가림 시험 정리 — 임도만 보고 고른 설정들 대 지금 설정 · 무리별 1순위 엄격."""
|
||||
import json
|
||||
import statistics as st
|
||||
from collections import Counter
|
||||
|
||||
import k0_lib as K
|
||||
|
||||
rows = []
|
||||
for w in ("2", "3", "4"):
|
||||
rows += K.load(f"hold_{w}.json")
|
||||
G = ("임도", "사방", "영월", "자동", "씨앗")
|
||||
print("설정 수", len(rows))
|
||||
|
||||
|
||||
def pick(pred):
|
||||
return [r for r in rows if pred(r)]
|
||||
|
||||
|
||||
def show(name, rs):
|
||||
if not rs:
|
||||
return
|
||||
cols = []
|
||||
for k, g in enumerate(G):
|
||||
v = [r["1위"][k] for r in rs]
|
||||
cols.append(f"{g} {min(v)}~{st.median(v)}~{max(v)}" if len(rs) > 1 else f"{g} {v[0]}")
|
||||
print(f"{name} (설정 {len(rs)}) | " + " | ".join(cols))
|
||||
|
||||
|
||||
pub = pick(lambda r: r["W"] == [3, 2, 1.5, 0.6, 0.6] and r["β"] == 1.0 and r["같음"] and r["cut"] == "0.35" and r["기억"] == "근거 가름")
|
||||
pre = pick(lambda r: r["W"] == [3, 2, 1.5, 0.6, 0.6] and r["β"] == 2.0 and r["같음"] and r["cut"] == "0.35" and r["기억"] == "근거 가름")
|
||||
show("지금 설정(세 파일 보고 고름 · β=1)", pub)
|
||||
show("미리 정한 값(β=2)", pre)
|
||||
best1 = max(r["1위"][0] for r in rows)
|
||||
tied = pick(lambda r: r["1위"][0] == best1)
|
||||
best5 = max(r["지문5안"][0] for r in tied)
|
||||
tied2 = [r for r in tied if r["지문5안"][0] == best5]
|
||||
show(f"임도만 보고 고름 — 임도 1위 최고 {best1}", tied)
|
||||
show(f" + 동점 가름(임도 지문 다섯 안 {best5})", tied2)
|
||||
show("모든 설정(참고)", rows)
|
||||
for k, g in enumerate(G[1:], 1):
|
||||
v = [r["1위"][k] for r in tied2]
|
||||
print(f" {g}: 지금 설정 {pub[0]['1위'][k]} → 임도만 고름 가운데 {st.median(v)} (폭 {round(st.median(v) - pub[0]['1위'][k], 1)}) · 가장 나쁨 {min(v)}")
|
||||
print("임도만 고른 설정의 손잡이 분포:")
|
||||
for key in ("β", "같음", "cut", "기억"):
|
||||
print(" ", key, Counter(str(r[key]) for r in tied2).most_common())
|
||||
for j, nm in enumerate(("인력", "장비", "조항", "이름", "조건")):
|
||||
print(" ", nm, Counter(r["W"][j] for r in tied2).most_common())
|
||||
# 손잡이마다 — 가림 무리(사방 · 영월 · 자동 · 씨앗) 평균 1순위, 다른 손잡이는 지금 설정 그대로
|
||||
base = pub[0]
|
||||
for key, vals in (("β", (0.0, 0.5, 1.0, 2.0, 3.0)), ("같음", (True, False)), ("cut", ("0.2", "0.35", "0.5")), ("기억", ("지금(a 바로)", "a·b 바로(넓히기만)", "모든 열쇠 바로", "근거 가름", "근거 가름 + 발주처"))):
|
||||
cells = []
|
||||
for v in vals:
|
||||
r = next(x for x in rows if x["W"] == base["W"] and all(x[k] == (v if k == key else base[k]) for k in ("β", "같음", "cut", "기억")))
|
||||
cells.append(f"{v}: " + "/".join(str(x) for x in r["1위"]))
|
||||
print(f"[{key}] " + " · ".join(cells))
|
||||
@@ -0,0 +1,100 @@
|
||||
"""req226 C7 ⑦ 넓힌 파일 줄 누적 · 가벼운 지문 눈금 · 딱 맞음 정밀(파일별 · 가림)."""
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
|
||||
import k0_lib as K
|
||||
import k3_vec as V
|
||||
from k0_lib import S
|
||||
|
||||
F4 = K.F4
|
||||
W = K.load("wide.json")
|
||||
pre = K.load("hold_prep.json")
|
||||
dec = pre["dec"]["근거 가름"]
|
||||
decorg = pre["dec"]["근거 가름 + 발주처"]
|
||||
LITE = np.load(K.HERE / "vec_가벼움_넓힘.npz")
|
||||
IDS_W = [str(x) for x in LITE["ids"]]
|
||||
SW = V.scores(LITE["base"], LITE["var"])
|
||||
ANS = {"자동": {i: set(v) for i, v in W["자동"].items()}, "씨앗": {i: set(v) for i, v in W["씨앗"].items()}}
|
||||
|
||||
|
||||
def rate(res, ans, k=1):
|
||||
return round(100 * sum(1 for i in ans if set(res[i]["top"][:k]) & ans[i]) / len(ans), 1)
|
||||
|
||||
|
||||
def mem(res, d):
|
||||
out = {}
|
||||
for i, v in res.items():
|
||||
top = list(v["top"])
|
||||
if i in d and d[i][0] in ("바로", "1순위"):
|
||||
a = d[i][1]
|
||||
top = [a] + [c for c in top if c != a][:4]
|
||||
out[i] = {**v, "top": top}
|
||||
return out
|
||||
|
||||
|
||||
def fp(base, keep, beta=1.0):
|
||||
out = {}
|
||||
for a, i in enumerate(IDS_W):
|
||||
if keep and base[i]["상태"] == "같음":
|
||||
out[i] = dict(base[i])
|
||||
else:
|
||||
out[i] = {"상태": "후보", "top": V.fused_top(SW[a], base[i]["top"], beta)}
|
||||
return out
|
||||
|
||||
|
||||
steps = [
|
||||
("지금(읽기 = 336 영월 흉내)", W["지금|출처만"]),
|
||||
("묶음 ①", W["묶음①|출처만"]),
|
||||
("+ 읽기 넓힘(STmate 직종 이음 · 거창 행 칸 이음)", W["묶음①|출처직종"]),
|
||||
]
|
||||
f_keep = fp(W["묶음①|출처직종"], True)
|
||||
f_free = fp(W["묶음①|출처직종"], False)
|
||||
steps += [("+ 로직 지문(가벼움 · 같음 그대로)", f_keep), ("+ 로직 지문(가벼움 · 같음도 지문)", f_free)]
|
||||
steps += [("+ 조건부 기억(근거 가름) — 같음 그대로 위", mem(f_keep, dec)), ("+ 조건부 기억(근거 가름 + 발주처) — 같음 그대로 위", mem(f_keep, decorg))]
|
||||
print("단계 | 자동 정답 20 1위 · 다섯 안 | 씨앗 짝 43(처음 보는 셈) 1위 · 다섯 안")
|
||||
for name, res in steps:
|
||||
print(f"{name} | {rate(res, ANS['자동'])} · {rate(res, ANS['자동'], 5)} | {rate(res, ANS['씨앗'])} · {rate(res, ANS['씨앗'], 5)}")
|
||||
print("(참고) 지금 위 기억(근거 가름)", rate(mem(W["지금|출처만"], dec), ANS["씨앗"]), "· 묶음① 위", rate(mem(W["묶음①|출처직종"], dec), ANS["씨앗"]))
|
||||
|
||||
# 가벼운 지문 눈금 — 136 줄에서 가벼움 대 C4 지문(같은 무게 · β=1 · 같음 그대로)
|
||||
L = np.load(K.HERE / "vec_가벼움_사람.npz")
|
||||
SL = V.scores(L["base"], L["var"])
|
||||
b1 = K.load("st_b1.json")
|
||||
ok = lambda t, i: bool(t) and t[0] in set(S.LAB[i]["정답"]) # noqa: E731
|
||||
have = [i for i in S.BY if S.LAB[i]["정답"]]
|
||||
rowL = {str(i): a for a, i in enumerate(L["ids"])}
|
||||
lite_res = {i: (b1[i] if b1[i]["상태"] == "같음" else {"top": V.fused_top(SL[rowL[i]], b1[i]["top"], 1.0)}) for i in S.BY}
|
||||
full_res = K.load("st_fp.json")
|
||||
print("가벼운 지문 눈금(126) — C4 지문", round(100 * sum(ok(full_res[i]["top"], i) for i in have) / len(have), 1),
|
||||
"· 가벼움", round(100 * sum(ok(lite_res[i]["top"], i) for i in have) / len(have), 1))
|
||||
for f in ("북부청임도", "북부청사방", "동부청영월"):
|
||||
ids = [i for i in have if S.BY[i]["파일"] == f]
|
||||
print(" ", f, round(100 * sum(ok(full_res[i]["top"], i) for i in ids) / len(ids), 1), "→", round(100 * sum(ok(lite_res[i]["top"], i) for i in ids) / len(ids), 1))
|
||||
|
||||
# 딱 맞음 — 파일별(C4 지문 · 126) + 씨앗 짝(가벼움 · 가림)
|
||||
P = K.pools()
|
||||
by = {}
|
||||
for i in S.BY:
|
||||
if F4.sure(F4.EXP[i], P[i]):
|
||||
f = S.BY[i]["파일"]
|
||||
a = by.setdefault(f, [0, 0, 0])
|
||||
a[0] += 1
|
||||
if S.LAB[i]["정답"]:
|
||||
a[1] += 1
|
||||
a[2] += P[i][0][1] in set(S.LAB[i]["정답"])
|
||||
print("딱 맞음(지문 1위 · 정답 있음 · 맞음) 파일별:", by)
|
||||
items = {r["id"]: r for r in json.loads((K.C6 / "items.json").read_text(encoding="utf-8"))}
|
||||
cnt = {"자동": [0, 0], "씨앗": [0, 0]}
|
||||
wrong = []
|
||||
for i in IDS_W:
|
||||
e = V.lite(items[i])
|
||||
pool = F4.rank(e, None, 2)
|
||||
if F4.sure(e, pool):
|
||||
for g in ("자동", "씨앗"):
|
||||
if i in ANS[g]:
|
||||
cnt[g][0] += 1
|
||||
cnt[g][1] += pool[0][1] in ANS[g][i]
|
||||
if pool[0][1] not in ANS[g][i]:
|
||||
wrong.append((g, i, items[i]["이름"], pool[0][1], sorted(ANS[g][i])))
|
||||
print("딱 맞음(가벼움) 넓힌 줄:", cnt, "틀림", wrong)
|
||||
@@ -0,0 +1,78 @@
|
||||
"""req226 C7 ⑧ 「딱 맞음이 같음을 이김」 — 같음 줄이라도 지문이 딱 맞음(숫자 일치 · 2위와 차 큼)이고 다른 로직이면 지문.
|
||||
126(씨앗 켬 · 끔 · 맨 위 누적까지) · 넓힌 줄(가벼움)."""
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
|
||||
import k0_lib as K
|
||||
import k3_vec as V
|
||||
from k0_lib import S
|
||||
|
||||
F4 = K.F4
|
||||
P = K.pools()
|
||||
SURE = {i: F4.sure(F4.EXP[i], P[i]) for i in S.BY}
|
||||
have = [i for i in S.BY if S.LAB[i]["정답"]]
|
||||
ok = lambda t, i: bool(t) and t[0] in set(S.LAB[i]["정답"]) # noqa: E731
|
||||
MIXED = {i for i in S.BY if K.PARTS.get(i) and K.SV.is_mixed(K.PARTS[i], S.BY[i])}
|
||||
|
||||
|
||||
def finger2(rec):
|
||||
out = {}
|
||||
for i in S.BY:
|
||||
if rec[i]["상태"] == "같음" and not (SURE[i] and P[i][0][1] != rec[i]["top"][0]):
|
||||
out[i] = dict(rec[i])
|
||||
else:
|
||||
K.F4.CUR["id"] = i
|
||||
out[i] = {"상태": "후보", "top": F4.fuse(None, rec[i], 1.0, P[i])}
|
||||
return out
|
||||
|
||||
|
||||
for tag in ("켬", "끔"):
|
||||
st = K.load(f"stack_{tag}.json")["줄"]
|
||||
b1 = st["묶음①"]
|
||||
f1, f2 = st["지문"], finger2(b1)
|
||||
flips = [(i, S.BY[i]["이름"][:12], ok(f1[i]["top"], i), ok(f2[i]["top"], i)) for i in S.BY if f1[i]["top"][:1] != f2[i]["top"][:1]]
|
||||
print(f"[{tag}] 지문 같음 그대로 → 딱 맞음이 이김: {K.S.table('x', f1)['전체']['1위 엄격']} → {K.S.table('x', f2)['전체']['1위 엄격']} · 바뀐 줄 {flips}")
|
||||
ga1 = K.ga(b1)
|
||||
res = {i: dict(ga1[i] if i in MIXED else f2[i]) for i in S.BY}
|
||||
t3, s3 = K.solve(res, seeds=(tag == "켬"))
|
||||
t4, s4, _ = K.memory(t3, s3)
|
||||
K.show(K.row(f"[{tag}] 맨 위 누적(딱 맞음이 같음을 이김)", t4, K.combo_set(t4, s4)))
|
||||
K.save(f"st_top2_{tag}.json", {"tops": t4, "sets": s4})
|
||||
|
||||
# 넓힌 줄
|
||||
W = K.load("wide.json")
|
||||
pre = K.load("hold_prep.json")
|
||||
LITE = np.load(K.HERE / "vec_가벼움_넓힘.npz")
|
||||
IDS_W = [str(x) for x in LITE["ids"]]
|
||||
SW = V.scores(LITE["base"], LITE["var"])
|
||||
items = {r["id"]: r for r in json.loads((K.C6 / "items.json").read_text(encoding="utf-8"))}
|
||||
ANS = {"자동": {i: set(v) for i, v in W["자동"].items()}, "씨앗": {i: set(v) for i, v in W["씨앗"].items()}}
|
||||
base = W["묶음①|출처직종"]
|
||||
res = {}
|
||||
for a, i in enumerate(IDS_W):
|
||||
e = V.lite(items[i])
|
||||
pool = F4.rank(e, None, 2)
|
||||
sure = F4.sure(e, pool)
|
||||
if base[i]["상태"] == "같음" and not (sure and pool[0][1] != base[i]["top"][0]):
|
||||
res[i] = dict(base[i])
|
||||
else:
|
||||
res[i] = {"상태": "후보", "top": V.fused_top(SW[a], base[i]["top"], 1.0)}
|
||||
|
||||
|
||||
def rate(r, ans, k=1):
|
||||
return round(100 * sum(1 for i in ans if set(r[i]["top"][:k]) & ans[i]) / len(ans), 1)
|
||||
|
||||
|
||||
def mem(r, d):
|
||||
out = {}
|
||||
for i, v in r.items():
|
||||
top = list(v["top"])
|
||||
if i in d and d[i][0] in ("바로", "1순위"):
|
||||
top = [d[i][1]] + [c for c in top if c != d[i][1]][:4]
|
||||
out[i] = {**v, "top": top}
|
||||
return out
|
||||
|
||||
|
||||
for nm, r in (("+ 지문(딱 맞음이 같음을 이김)", res), ("+ 기억(근거 가름)", mem(res, pre["dec"]["근거 가름"])), ("+ 기억(근거 가름 + 발주처)", mem(res, pre["dec"]["근거 가름 + 발주처"]))):
|
||||
print(f"넓힌 줄 {nm} | 자동 {rate(r, ANS['자동'])} · {rate(r, ANS['자동'], 5)} | 씨앗 {rate(r, ANS['씨앗'])} · {rate(r, ANS['씨앗'], 5)}")
|
||||
@@ -0,0 +1,485 @@
|
||||
"""K5 공용 — 「로직 + 옵션」 거꾸로 찾기: 옵션 공간 · 엔진 셈(캐시) · 엑셀 품 맞대기.
|
||||
|
||||
우리 쪽 = C4 `s2_ours_fp.run_one`(M01 엔진 `mf.run` — `Labor_Doc.run_line` 과 같은 읽기 길) 그대로 · 읽기만.
|
||||
옵션 공간(로직 입력 칸마다 후보):
|
||||
고르기(지역 · 보정작업 · 조종원직종 뺌 — 1,438 로직 모두 품을 안 바꿈 · p01) = 모두
|
||||
기계 고르기(EQ 키) = 모두(엑셀 규격 수가 있으면 그 규격에 맞는 것만 · 맞는 것 없으면 모두)
|
||||
범위 = 두 끝 + 범위 안의 엑셀 수
|
||||
찾기 조건에 쓰인 수 = 그 표의 줄 값(`master_auto._fits`) + 엑셀 수(표 값과 같은 것만 남음)
|
||||
그 밖 수 = 자동값 + 엑셀 계수(같은 이름 · q f E Cm …) + 엑셀 수(이름 · 규격 · 블록 글) — 「품」 칸은 × 결과단위
|
||||
조합 = 곱이 CAP 이하면 모두 · 넘으면 엑셀 조건 낱말로 고르기를 좁힘 · 그래도 넘으면 하나씩 바꿈 + 고르기 둘씩.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
SP = HERE.parent.parent
|
||||
C4 = SP / "req226" / "C4"
|
||||
for p in (str(C4), str(SP / "req225" / "C1"), str(SP / "req225" / "C3")):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
import s2_ours_fp as O # noqa: E402
|
||||
from fp_common import conds, mtypes, specnums # noqa: E402
|
||||
|
||||
WHOLE, m01, mau = O.WHOLE, O.m01, O.m01.mau
|
||||
EQK = re.compile(r"^\d{4}-\d{4}$")
|
||||
FIXED = ("지역", "보정작업", "조종원직종")
|
||||
CAP = 1500
|
||||
_NUM = re.compile(r"(?<![\d,.])(\d+(?:\.\d+)?)(?![\d,])")
|
||||
|
||||
|
||||
UNITS = {
|
||||
"%": ("%",), "㎝": ("㎝", "cm"), "cm": ("㎝", "cm"), "㎞": ("㎞", "km"), "km": ("㎞", "km"), "㎜": ("㎜", "mm"), "mm": ("㎜", "mm"),
|
||||
"㎥": ("㎥", "m3"), "㎡": ("㎡", "m2"), "m": ("m",), "회": ("회",), "개월": ("개월",), "층": ("층",), "t": ("t", "ton", "톤"),
|
||||
"ton": ("t", "ton", "톤"), "톤": ("t", "ton", "톤"), "분": ("분",), "초": ("초", "sec"), "kg": ("kg", "㎏"), "㎏": ("kg", "㎏"),
|
||||
}
|
||||
|
||||
|
||||
def excel_numbers(texts, spec=None) -> set:
|
||||
"""엑셀 이름 · 규격 · 블록 글의 수 — spec 을 주면 그 칸의 단위가 뒤에 붙거나 칸 이름이 앞에 붙은 수만
|
||||
(천 자리 쉼표 든 단가는 뺌)."""
|
||||
out = set()
|
||||
flat = [unicodedata.normalize("NFKC", str(t or "")) for t in texts]
|
||||
if spec is None:
|
||||
pats = [_NUM]
|
||||
else:
|
||||
u = unicodedata.normalize("NFKC", str(spec.get("단위") or "")).strip().lower()
|
||||
vs = UNITS.get(u, (u,) if u and len(u) <= 3 and "/" not in u else ())
|
||||
if u == "%": # 「노무비의 2%」 · 「38%」 같은 율이 감률 칸 후보로 드는 것을 막음 — % 칸은 이름 붙은 수만
|
||||
vs = ()
|
||||
num = r"(\d+(?:\.\d+)?)"
|
||||
pats = [re.compile(r"(?<![\d,.])" + num + r"\s*(?:" + "|".join(re.escape(x) for x in vs) + r")(?![A-Za-z가-힣])", re.I)] if vs else []
|
||||
nm = re.escape(unicodedata.normalize("NFKC", spec["이름"]))
|
||||
pats.append(re.compile(nm + r"\s*[:=(]?\s*" + num + r"(?![\d,])"))
|
||||
for t in flat:
|
||||
for pat in pats:
|
||||
for n in pat.findall(t):
|
||||
v = Decimal(n)
|
||||
if 0 < v <= 2000:
|
||||
out.add(v.normalize() if v == v.to_integral() else v)
|
||||
return out
|
||||
|
||||
|
||||
def _uses(row):
|
||||
try:
|
||||
return mau._uses(row)
|
||||
except Exception: # noqa: BLE001
|
||||
return {}, set()
|
||||
|
||||
|
||||
def eq_label(code):
|
||||
one = O.EQ.get(str(code)) or {}
|
||||
return mtypes(f"{one.get('상세구분')} {one.get('이름')}"), specnums(one.get("규격"))
|
||||
|
||||
|
||||
def space(code, ex=None):
|
||||
"""[(입력 이름, 후보 목록, 갈래)] — ex = 엑셀 줄(수 · 규격 수 · 계수 · 조건 낱말)."""
|
||||
row = WHOLE.logic(code)
|
||||
auto = mau.values(WHOLE, row)
|
||||
finds, _ = _uses(row)
|
||||
texts = (ex or {}).get("글") or []
|
||||
coef = {k.lower(): Decimal(str(v)) for k, v in ((ex or {}).get("계수") or {}).items()}
|
||||
spn = (ex or {}).get("규격수") or set()
|
||||
times = Decimal(str(O.times_of(row)))
|
||||
out = []
|
||||
for s in row.get("입력") or []:
|
||||
name = s["이름"]
|
||||
opts = s.get("고르기") or []
|
||||
if name in FIXED:
|
||||
continue
|
||||
if opts and all(isinstance(v, str) and EQK.match(v) for v in opts):
|
||||
keep = [v for v in opts if spn and eq_label(v)[1] & spn]
|
||||
out.append((name, keep or list(opts), "기계"))
|
||||
elif opts:
|
||||
out.append((name, list(opts), "고르기"))
|
||||
elif isinstance(s.get("범위"), list) and len(s["범위"]) == 2:
|
||||
lo, hi = (Decimal(str(x)) for x in s["범위"])
|
||||
vals = {lo, hi} | {v for v in excel_numbers(texts, s) if lo <= v <= hi}
|
||||
out.append((name, sorted(vals), "범위"))
|
||||
elif name in finds:
|
||||
rows = set()
|
||||
for ref, cond in finds[name]:
|
||||
rows |= set(mau._fits(WHOLE, ref, cond))
|
||||
cand = sorted(rows) or [auto.get(name)]
|
||||
out.append((name, cand, "표"))
|
||||
else:
|
||||
base = auto.get(name)
|
||||
vals = {base} if base is not None else set()
|
||||
if name.lower() in coef:
|
||||
vals.add(coef[name.lower()])
|
||||
vals |= excel_numbers(texts, s) if texts else set()
|
||||
if str(s.get("단위") or "").strip() == "%":
|
||||
vals.add(Decimal(0))
|
||||
if name == "품":
|
||||
vals |= {v * times for v in (ex or {}).get("품값", set())}
|
||||
vals.discard(None)
|
||||
out.append((name, sorted(vals, key=lambda v: (v != base, v)) or [base], "수"))
|
||||
return out
|
||||
|
||||
|
||||
def narrow(sp, ex):
|
||||
"""고르기 후보를 엑셀 조건 낱말로 좁힘 — 낱말이 든 후보가 있으면 그것만."""
|
||||
words = set()
|
||||
for ax, ws in ((ex or {}).get("조건") or {}).items():
|
||||
words |= set(ws)
|
||||
flat = re.sub(r"\s+", "", unicodedata.normalize("NFKC", " ".join((ex or {}).get("글머리") or [])))
|
||||
out = []
|
||||
for name, opts, kind in sp:
|
||||
if kind == "고르기" and len(opts) > 1:
|
||||
hit = [o for o in opts if any(w in str(o) for w in words) or (len(str(o)) >= 2 and str(o) in flat)]
|
||||
if hit:
|
||||
out.append((name, hit, kind))
|
||||
continue
|
||||
out.append((name, opts, kind))
|
||||
return out
|
||||
|
||||
|
||||
def combos(sp, ex=None, cap=CAP):
|
||||
"""조합 목록 · 어떻게 냈나(모두 / 좁힘 / 하나씩)."""
|
||||
size = math.prod(len(o) for _, o, _ in sp) if sp else 1
|
||||
if size <= cap:
|
||||
return [dict(zip([n for n, _, _ in sp], vals)) for vals in itertools.product(*[o for _, o, _ in sp])], "모두"
|
||||
sp2 = narrow(sp, ex)
|
||||
size2 = math.prod(len(o) for _, o, _ in sp2)
|
||||
if size2 <= cap:
|
||||
return [dict(zip([n for n, _, _ in sp2], vals)) for vals in itertools.product(*[o for _, o, _ in sp2])], "좁힘"
|
||||
# 하나씩 + 고르기 · 표 · 기계 둘씩(수 칸은 하나씩만)
|
||||
base = {n: o[0] for n, o, _ in sp2}
|
||||
out = [dict(base)]
|
||||
for n, o, _ in sp2:
|
||||
out += [{**base, n: v} for v in o[1:]]
|
||||
small = [(n, o) for n, o, k in sp2 if k != "수"]
|
||||
for (n1, o1), (n2, o2) in itertools.combinations(small, 2):
|
||||
if len(o1) * len(o2) <= 400:
|
||||
out += [{**base, n1: a, n2: b} for a in o1 for b in o2]
|
||||
seen, uniq = set(), []
|
||||
for c in out:
|
||||
k = json.dumps(c, sort_keys=True, default=str, ensure_ascii=False)
|
||||
if k not in seen:
|
||||
seen.add(k)
|
||||
uniq.append(c)
|
||||
return uniq[:cap], "하나씩"
|
||||
|
||||
|
||||
_RUN = {}
|
||||
_ROWS = {}
|
||||
|
||||
|
||||
def _row(code):
|
||||
if code not in _ROWS:
|
||||
row = WHOLE.logic(code)
|
||||
by_name = {}
|
||||
for h in row.get("호표") or []:
|
||||
by_name.setdefault(h.get("이름"), h)
|
||||
_ROWS[code] = (row, by_name, O.times_of(row))
|
||||
return _ROWS[code]
|
||||
|
||||
|
||||
def run(code, given):
|
||||
"""엔진 셈 → {L, X, M, S, mix} · 못 돌면 None(캐시).
|
||||
「기계 시간당 기계경비」 로직 = 고른 기계(EQ 칸)의 갈래 · 규격 1 시간(조종원 줄은 기계경비 속이라 뺌)."""
|
||||
k = (code, json.dumps(given, sort_keys=True, default=str, ensure_ascii=False))
|
||||
if k not in _RUN:
|
||||
row, by_name, times = _row(code)
|
||||
try:
|
||||
v = O.run_one(code, row, dict(given), times, by_name)
|
||||
except Exception: # noqa: BLE001
|
||||
v = None
|
||||
_RUN[k] = v
|
||||
v = _RUN[k]
|
||||
row = _row(code)[0]
|
||||
if v is not None and O.is_mach_logic(row):
|
||||
auto = _AUTO.setdefault(code, mau.values(WHOLE, row))
|
||||
X = []
|
||||
for s in row.get("입력") or []:
|
||||
opts = s.get("고르기") or []
|
||||
if opts and all(isinstance(o, str) and EQK.match(o) for o in opts):
|
||||
c = given.get(s["이름"], auto.get(s["이름"]))
|
||||
ts, sp = eq_label(c)
|
||||
X.append((sorted(ts | set(O.FIXED_MACH.get(code, set()))), sorted(sp), 1.0))
|
||||
if not X and O.FIXED_MACH.get(code):
|
||||
X = [(sorted(O.FIXED_MACH[code]), [], 1.0)]
|
||||
v = {**v, "L": {}, "X": X}
|
||||
return v
|
||||
|
||||
|
||||
_AUTO = {}
|
||||
|
||||
|
||||
def sig(v, nd=4):
|
||||
"""품 꼴 서명 — 직종 · 장비(갈래 · 시간) · 자재 · 딸린 로직 수량(유효 nd 자리)."""
|
||||
if v is None:
|
||||
return None
|
||||
r = lambda x: float(f"{x:.{nd}g}") # noqa: E731
|
||||
return json.dumps(
|
||||
[
|
||||
sorted((k, r(q)) for k, q in v["L"].items() if q),
|
||||
sorted((tuple(a), tuple(b), r(q)) for a, b, q in v["X"] if q),
|
||||
sorted((k, r(q)) for k, q in v["M"].items() if q),
|
||||
sorted((k, r(q)) for k, q in v["S"].items() if q),
|
||||
],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
# ── 맞대기 ──────────────────────────────────────────────────────────────
|
||||
def near(a, b, t, r=0.0):
|
||||
"""a(엑셀) ≈ b(우리) — 비 t 안 · r = 엑셀 값이 적힌 자릿수의 반올림 폭(끝수 허용)."""
|
||||
return a is not None and b is not None and a > 0 and b > 0 and abs(a - b) <= max(t * max(a, b), r)
|
||||
|
||||
|
||||
def ru(x):
|
||||
"""엑셀 값의 반올림 폭 — 소수 둘째~넷째 자리로 적힌 값만 · 값의 4% 안(0.15 → 0.005 · 0.1 → 0 · 0.109091 → 0)."""
|
||||
s = repr(float(x))
|
||||
if "e" in s or "." not in s:
|
||||
return 0.0
|
||||
d = len(s.split(".")[1].rstrip("0"))
|
||||
return min(0.5 * 10 ** (-d), 0.04 * float(x)) if 2 <= d <= 4 else 0.0
|
||||
|
||||
|
||||
UNITW = {"mm", "cm", "kg", "ton", "sec", "kgf", "mpa"}
|
||||
|
||||
|
||||
def mat_words(name):
|
||||
"""자재 이름 낱말 — 한글 두 자 넘는 덩이 · 영문 세 자 넘는 덩이(괄호 속도) · 단위 낱말 뺌."""
|
||||
t = unicodedata.normalize("NFKC", str(name or "")).lower().replace(".", "")
|
||||
return {w for w in re.findall(r"[가-힣]{2,}|[a-z]{3,}", t) if w not in UNITW}
|
||||
|
||||
|
||||
def compare(e, v, t, base=False, rnd=False):
|
||||
"""엑셀 지문 e(C4 꼴) ↔ 우리 셈 v · 허용 t.
|
||||
→ {직종: 같음/다름/없음, 장비: …, 자재: …, 핵심: bool, 덧: [엑셀만 장비 …]}
|
||||
base=True 면 엑셀 밑품(섞음 · 할증 뺀 품)으로 견줌."""
|
||||
if v is None:
|
||||
return None
|
||||
E = {k: (x["밑품"] if base else x["품"]) for k, x in e["인력"].items() if x.get("품")}
|
||||
O_ = {k: q for k, q in v["L"].items() if q and q > 0}
|
||||
out = {"덧": []}
|
||||
if E:
|
||||
out["직종"] = "같음" if set(E) == set(O_) and all(near(E[k], O_[k], t, ru(E[k]) if rnd else 0) for k in E) else ("직종 같음" if set(E) == set(O_) else "다름")
|
||||
elif O_:
|
||||
out["직종"] = "우리만"
|
||||
else:
|
||||
out["직종"] = "없음"
|
||||
# 장비 — 같은 갈래끼리 시간
|
||||
EX = [m for m in e["장비"] if m.get("시간")]
|
||||
OX = [(set(a), q) for a, _, q in v["X"] if q]
|
||||
if EX and OX:
|
||||
ok = []
|
||||
for m in EX:
|
||||
mt = set(m["갈래"])
|
||||
mine = [q for a, q in OX if a & mt]
|
||||
if not mine:
|
||||
out["덧"].append("엑셀만 장비 " + "·".join(sorted(mt)))
|
||||
continue
|
||||
h = m["밑시간"] if base and m.get("밑시간") else m["시간"]
|
||||
ok.append(any(near(h, q, t, ru(h) if rnd else 0) for q in mine) or near(h, sum(mine), t, ru(h) if rnd else 0))
|
||||
et = set().union(*[set(m["갈래"]) for m in e["장비"]])
|
||||
for a, _ in OX:
|
||||
if not a & et:
|
||||
out["덧"].append("우리만 장비 " + "·".join(sorted(a)))
|
||||
out["장비"] = ("같음" if all(ok) else "다름") if ok else "갈래 안 겹침"
|
||||
elif EX:
|
||||
out["장비"] = "엑셀만"
|
||||
elif OX:
|
||||
out["장비"] = "우리만"
|
||||
else:
|
||||
out["장비"] = "없음"
|
||||
# 자재 — 이름 낱말이 겹치는 것끼리 수량
|
||||
EM = [m for m in e["자재"] if m.get("품")]
|
||||
OM = {k: q for k, q in v["M"].items() if q}
|
||||
if EM and OM:
|
||||
ok = []
|
||||
for m in EM:
|
||||
ws = mat_words(m["이름"])
|
||||
mine = [q for k, q in OM.items() if ws & mat_words(k) or any(a in b or b in a for a in ws for b in mat_words(k))]
|
||||
if mine:
|
||||
x = m["밑품"] if base and m.get("밑품") else m["품"]
|
||||
ok.append(any(near(x, q, t, ru(x) if rnd else 0) for q in mine))
|
||||
out["자재"] = ("같음" if all(ok) else "다름") if ok else "이름 안 겹침"
|
||||
elif EM:
|
||||
out["자재"] = "엑셀만"
|
||||
else:
|
||||
out["자재"] = "없음"
|
||||
core = []
|
||||
if E:
|
||||
core.append(out["직종"] == "같음")
|
||||
elif O_:
|
||||
core.append(False)
|
||||
if out["장비"] in ("같음", "다름"):
|
||||
core.append(out["장비"] == "같음")
|
||||
if out["자재"] in ("같음", "다름"):
|
||||
core.append(out["자재"] == "같음")
|
||||
out["핵심"] = bool(core) and all(core)
|
||||
out["직종만"] = bool(E) and out["직종"] == "같음"
|
||||
return out
|
||||
|
||||
|
||||
# ── 믹서 갈래(C4 MACH 에 없음) — 엑셀 · 우리 둘 다 같은 함수를 쓰므로 여기서 한 번 더함 ──
|
||||
import fp_common as _F # noqa: E402
|
||||
|
||||
for _k in ("믹서", "분무기"):
|
||||
if not any(k == _k for k, _ in _F.MACH):
|
||||
_F.MACH.append((_k, (_k,)))
|
||||
|
||||
# ── 엔진 셈 캐시(명령 사이 이어 씀) ─────────────────────────────────────
|
||||
import pickle # noqa: E402
|
||||
|
||||
CACHE = HERE / "run_cache.pkl"
|
||||
|
||||
|
||||
def load_cache():
|
||||
if CACHE.exists():
|
||||
_RUN.update(pickle.loads(CACHE.read_bytes()))
|
||||
return len(_RUN)
|
||||
|
||||
|
||||
def save_cache():
|
||||
CACHE.write_bytes(pickle.dumps(_RUN))
|
||||
|
||||
|
||||
def plan(code, ex=None):
|
||||
"""(조합들, 어떻게, 움직이는 칸, 안 닿는 칸, 값 묶음).
|
||||
곱이 CAP 이하 = 모두 돌림 → 안 닿는 칸(어떤 자리에서도 품 서명이 안 바뀜) · 수 칸의 같은 값 묶음(같은 표 줄)을 끝에 셈.
|
||||
곱이 넘침 = 기본값 · 마지막 값 두 기준점에서 하나씩 바꿔 안 닿는 칸을 기본값으로 접고 · 같은 품 수 값을 한 값으로 · 남은 칸을 좁힘."""
|
||||
sp = space(code, ex)
|
||||
size = math.prod(len(o) for _, o, _ in sp) if sp else 1
|
||||
if size <= CAP:
|
||||
names = [n for n, _, _ in sp]
|
||||
cb = [dict(zip(names, vals)) for vals in itertools.product(*[o for _, o, _ in sp])]
|
||||
sg = [sig(run(code, c)) for c in cb]
|
||||
active, inert, cls = [], [], {}
|
||||
for n, o, k in sp:
|
||||
if len(o) < 2:
|
||||
continue
|
||||
ctx = {}
|
||||
for c, s_ in zip(cb, sg):
|
||||
key = json.dumps({x: str(y) for x, y in c.items() if x != n}, sort_keys=True, ensure_ascii=False)
|
||||
ctx.setdefault(key, {})[str(c[n])] = s_
|
||||
keys = sorted(ctx)
|
||||
prof = {str(v): tuple(ctx[g].get(str(v)) for g in keys) for v in o}
|
||||
if len(set(prof.values())) == 1:
|
||||
inert.append(n)
|
||||
else:
|
||||
active.append(n)
|
||||
if k != "고르기":
|
||||
ids = {}
|
||||
cls[n] = {v: ids.setdefault(pf, len(ids)) for v, pf in prof.items()}
|
||||
return cb, "모두", active, inert, cls
|
||||
base = {n: o[0] for n, o, _ in sp}
|
||||
alt = {n: o[-1] for n, o, _ in sp}
|
||||
active, inert = [], []
|
||||
for n, o, k in sp:
|
||||
if len(o) == 1:
|
||||
continue
|
||||
s0 = {sig(run(code, {**base, n: v})) for v in o}
|
||||
s1 = {sig(run(code, {**alt, n: v})) for v in o} if len(s0) == 1 else s0
|
||||
if len(s0) > 1 or len(s1) > 1:
|
||||
if k != "고르기": # 같은 표 줄에 드는 수(0 · 35 …)는 한 값으로 — 두 기준점 품이 같으면 같은 값
|
||||
keep, seen = [], set()
|
||||
for v in o:
|
||||
key = (sig(run(code, {**base, n: v})), sig(run(code, {**alt, n: v})))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
keep.append(v)
|
||||
o = keep
|
||||
active.append((n, o, k))
|
||||
else:
|
||||
inert.append((n, o, k))
|
||||
cb, how = combos(active, ex)
|
||||
cb = [{**base, **c} for c in cb]
|
||||
return cb, how, [n for n, _, _ in active], [n for n, _, _ in inert], {}
|
||||
|
||||
|
||||
def vary(hits, act, cls):
|
||||
"""맞은 조합들 사이에서 값이 갈리는 칸 — 수 칸은 같은 값 묶음(같은 표 줄)이면 한 값."""
|
||||
out = []
|
||||
for n in act:
|
||||
vals = {cls.get(n, {}).get(str(g.get(n)), str(g.get(n))) for g in hits}
|
||||
if len(vals) > 1:
|
||||
out.append(n)
|
||||
return out
|
||||
|
||||
|
||||
def add(vs):
|
||||
"""복합 — 몫 벡터 합."""
|
||||
vs = [v for v in vs if v is not None]
|
||||
if not vs:
|
||||
return None
|
||||
out = {"L": {}, "X": [], "M": {}, "S": {}, "mix": {}}
|
||||
for v in vs:
|
||||
for k in ("L", "M", "S"):
|
||||
for a, q in v[k].items():
|
||||
out[k][a] = out[k].get(a, 0.0) + q
|
||||
out["X"] += v["X"]
|
||||
return out
|
||||
|
||||
|
||||
def gap(e, v, base=False):
|
||||
"""닮음 거리(작을수록 가까움) — 직종 묶음 어긋남 1 · 품은 직종마다 |ln 비| 평균 · 장비 시간 같은 갈래끼리."""
|
||||
if v is None:
|
||||
return 99.0
|
||||
E = {k: (x["밑품"] if base else x["품"]) for k, x in e["인력"].items() if x.get("품")}
|
||||
O_ = {k: q for k, q in v["L"].items() if q and q > 0}
|
||||
d = 0.0
|
||||
if E or O_:
|
||||
keys = set(E) | set(O_)
|
||||
d += len(set(E) ^ set(O_)) / len(keys)
|
||||
both = [min(1.0, abs(math.log(E[k] / O_[k]))) for k in set(E) & set(O_)]
|
||||
d += (sum(both) / len(both)) if both else 1.0
|
||||
EX = [m for m in e["장비"] if m.get("시간")]
|
||||
OX = [(set(a), q) for a, _, q in v["X"] if q]
|
||||
if EX and OX:
|
||||
hs = []
|
||||
for m in EX:
|
||||
mine = [q for a, q in OX if a & set(m["갈래"])]
|
||||
h = m["밑시간"] if base and m.get("밑시간") else m["시간"]
|
||||
hs.append(min(1.0, min(abs(math.log(h / q)) for q in mine)) if mine else 1.0)
|
||||
d += 0.5 * sum(hs) / len(hs)
|
||||
return d
|
||||
|
||||
|
||||
def csig(e, v, nd=4):
|
||||
"""엑셀이 보이는 몫만의 품 서명 — 직종 전부 · 엑셀 갈래와 겹치는 장비 시간 · 엑셀 이름과 겹치는 자재 수량."""
|
||||
if v is None:
|
||||
return None
|
||||
r = lambda x: float(f"{x:.{nd}g}") # noqa: E731
|
||||
et = set().union(*[set(m["갈래"]) for m in e["장비"]]) if e["장비"] else set()
|
||||
em = [mat_words(m["이름"]) for m in e["자재"] if m.get("품")]
|
||||
|
||||
def named(k):
|
||||
w = mat_words(k)
|
||||
return any(ws & w or any(a in b or b in a for a in ws for b in w) for ws in em)
|
||||
|
||||
return json.dumps(
|
||||
[
|
||||
sorted((k, r(q)) for k, q in v["L"].items() if q),
|
||||
sorted((tuple(a), r(q)) for a, _, q in v["X"] if q and set(a) & et),
|
||||
sorted((k, r(q)) for k, q in v["M"].items() if q and named(k)),
|
||||
],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
def strong(e, c):
|
||||
"""딱 맞음(강) — 핵심 맞음 + 엑셀 몫을 다 덮음: 장비 갈래가 양쪽 같음(엑셀만 · 우리만 장비 없음) ·
|
||||
자재 다름 없음 · 「보통인부 하나뿐 · 장비 없음」 줄 아님(같은 품 로직이 수십이라 우연 맞음)."""
|
||||
if not c or not c.get("핵심"):
|
||||
return False
|
||||
if c.get("장비") in ("엑셀만", "우리만", "갈래 안 겹침") or any(("엑셀만 장비" in d or "우리만 장비" in d) for d in c.get("덧") or []):
|
||||
return False
|
||||
if c.get("자재") == "다름":
|
||||
return False
|
||||
E = [k for k, x in e["인력"].items() if x.get("품")]
|
||||
if E == ["보통인부"] and not any(m.get("시간") for m in e["장비"]):
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,123 @@
|
||||
"""K5 ① 대상 줄 — 정답 로직이 알려진 엑셀 줄의 지문(C4 꼴: 인력 품 · 밑품 · 장비 시간 · 자재 수량 · 계수 · 조건 · 글).
|
||||
|
||||
사람 정답 126(C1 labels · labels_y — 지문은 C4 excel_fp 그대로) +
|
||||
새 파일 51(C7 wide — 자동 정답 20 · 씨앗 짝 43 · 지문은 C6 STmate 블록 「자원」 L · X · M · S 와 items 품 ·
|
||||
거창은 C4 `north` 식 풀기로 블록 글을 풂).
|
||||
python k5_s1_targets.py → targets.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
import k5_lib as K
|
||||
from fp_common import conds, mtypes, specnums
|
||||
|
||||
import s1_excel_fp as X1 # C4 엑셀 지문(식 풀기 · 북부청 꼴)
|
||||
from labels import L
|
||||
from labels_y import LY
|
||||
|
||||
SP, C4 = K.SP, K.C4
|
||||
EX = {e["id"]: e for e in json.loads((C4 / "excel_fp.json").read_text(encoding="utf-8"))}
|
||||
WIDE = json.loads((SP / "req226/C7/wide.json").read_text(encoding="utf-8"))
|
||||
ITEMS = {r["id"]: r for r in json.loads((SP / "req226/C6/items.json").read_text(encoding="utf-8"))}
|
||||
DATA = {"봉화": "봉화", "울진대흥": "대흥설계", "울진소광": "소광", "영덕계류": "계류보전", "동부청영월": "영월", "거창": "거창"}
|
||||
COMPOSITE = {
|
||||
**{i: ["GC000206", "GC000207"] for i in ("N003", "N019", "N045", "S055", "S059", "Y010")},
|
||||
"N050": ["GC000705", "GC000714", "GC000712"],
|
||||
}
|
||||
_D = {}
|
||||
|
||||
|
||||
def data(f):
|
||||
if f not in _D:
|
||||
_D[f] = json.loads((SP / f"req226/C6/data_{DATA[f]}.json").read_text(encoding="utf-8"))
|
||||
return _D[f]
|
||||
|
||||
|
||||
def stmate(r):
|
||||
d = data(r["파일"])
|
||||
b = d["블록"].get(r["코드"]) or {}
|
||||
fp = X1.empty({**r, "근거": r["근거"]})
|
||||
fp["글"] = [str(t) for t in b.get("글") or []][:60]
|
||||
fp["조건"] = {k: sorted(v) for k, v in conds(" ".join([r["이름"], r["규격"]] + fp["글"][:4])).items()}
|
||||
fp["이름조건"] = {k: sorted(v) for k, v in conds(f"{r['이름']} {r['규격']}").items()}
|
||||
fp["옛판"] = bool(X1._OLD.search(" ".join(fp["글"][:4])))
|
||||
for t in fp["글"]:
|
||||
for k, v in X1._COEF.findall(t):
|
||||
fp["계수"].setdefault(k, float(v))
|
||||
rates = X1.rates_in(f"{r['이름']} {r['규격']}")
|
||||
lab_rate = min(rates) if len(rates) >= 2 else None
|
||||
mach_rate = max(rates) if len(rates) >= 2 else None
|
||||
for t, q in (r.get("품") or {}).items():
|
||||
fp["인력"][t] = {"품": q, "밑품": q / lab_rate if lab_rate else q, "율": [[lab_rate, "섞음"]] if lab_rate else [], "어떻게": ["QTY"]}
|
||||
seen = Counter()
|
||||
for x in b.get("자원") or []:
|
||||
kind, nm, sp, q = x.get("갈래"), x.get("이름") or "", x.get("규격") or "", x.get("품")
|
||||
if not q:
|
||||
continue
|
||||
key = (x.get("코드"), round(q, 9))
|
||||
seen[key] += 1
|
||||
if seen[key] > 1: # 노무비 · 재료비 · 경비 세 줄이 같은 품 — 한 번만
|
||||
continue
|
||||
ts = mtypes(f"{nm} {sp}")
|
||||
if kind == "X" or (kind == "S" and ts):
|
||||
fp["장비"].append({"갈래": sorted(ts), "규격": sorted(specnums(sp) | specnums(nm)), "시간": q,
|
||||
"밑시간": q / mach_rate if mach_rate else q, "율": [], "절": "0", "글": f"{nm} {sp}"}) # fmt: skip
|
||||
elif kind in ("M", "S"):
|
||||
fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": q, "밑품": q})
|
||||
return fp
|
||||
|
||||
|
||||
def geochang(r):
|
||||
b = data("거창")["블록"].get(r["코드"]) or {}
|
||||
item = {**r, "단산글": [b.get("머리") or r["이름"]] + [str(t) for t in b.get("글") or []]}
|
||||
return X1.north(item)
|
||||
|
||||
|
||||
def answers(i):
|
||||
if i in L or i in LY:
|
||||
lab = {**L, **LY}[i]
|
||||
return lab["정답"], lab.get("허용", []), "사람"
|
||||
got = []
|
||||
for d in (WIDE["자동"], WIDE["씨앗"]):
|
||||
for k in d.get(i) or []:
|
||||
if k not in got:
|
||||
got.append(k)
|
||||
kind = "자동+씨앗" if i in WIDE["자동"] and i in WIDE["씨앗"] else "자동" if i in WIDE["자동"] else "씨앗"
|
||||
return got, [], kind
|
||||
|
||||
|
||||
def main():
|
||||
out = []
|
||||
for i, e in EX.items():
|
||||
ans, allow, kind = answers(i)
|
||||
if not ans:
|
||||
continue
|
||||
out.append({**e, "정답": ans, "허용": allow, "무리": kind, "복합": COMPOSITE.get(i)})
|
||||
for i in WIDE["줄"]:
|
||||
if i in EX:
|
||||
continue
|
||||
r = ITEMS[i]
|
||||
fp = geochang(r) if r["파일"] == "거창" else stmate(r)
|
||||
ans, allow, kind = answers(i)
|
||||
comp = ans if set(ans) == {"GC000206", "GC000207"} else None
|
||||
out.append({**fp, "id": i, "파일": r["파일"], "정답": ans, "허용": allow, "무리": kind, "복합": comp, "줄수": r.get("줄수")})
|
||||
for e in out:
|
||||
for one in e["인력"].values():
|
||||
one["품"] = round(one["품"], 6)
|
||||
one["밑품"] = round(one["밑품"], 6)
|
||||
(K.HERE / "targets.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8")
|
||||
c = Counter(e["무리"] for e in out)
|
||||
print("대상", len(out), dict(c))
|
||||
print("인력 있음", sum(1 for e in out if e["인력"]), "· 장비 시간", sum(1 for e in out if any(m.get("시간") for m in e["장비"])),
|
||||
"· 자재 수량", sum(1 for e in out if any(m.get("품") for m in e["자재"])), "· 복합", sum(1 for e in out if e["복합"])) # fmt: skip
|
||||
for e in out:
|
||||
if e["무리"] != "사람":
|
||||
print(" ", e["id"], e["이름"], e["규격"], "| 인력", {k: v["품"] for k, v in e["인력"].items()},
|
||||
"| 장비", [(m["갈래"], round(m["시간"], 4)) for m in e["장비"] if m.get("시간")],
|
||||
"| 자재", [(m["이름"][:14], round(m["품"], 4)) for m in e["자재"] if m.get("품")][:3], "| 정답", e["정답"]) # fmt: skip
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,302 @@
|
||||
"""K5 ② 옵션 거꾸로 찾기 — 정답 로직(복합은 몫 합)의 옵션 조합을 모두(넘치면 엑셀 조건 낱말로 좁혀) 엔진으로 셈 →
|
||||
엑셀 품(직종 · 장비 시간 · 자재 수량)과 같은 조합: 하나만 / 여럿(옵션 못 가름) / 없음 / 엔진 못 셈 · 허용 0.5% · 3%.
|
||||
python k5_s2_answer.py <시작> <끝> → ans_<시작>.json (캐시 run_cache.pkl 이어 씀)
|
||||
python k5_s2_answer.py merge → answer.json
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
from decimal import Decimal
|
||||
|
||||
import k5_lib as K
|
||||
from fp_common import specnums
|
||||
|
||||
TOLS = (0.005, 0.03)
|
||||
MODES = (("0.005", 0.005, False), ("R", 0.005, True), ("0.03", 0.03, False)) # R = 0.5% + 엑셀 자릿수 반올림 허용
|
||||
RANK = {"하나만": 0, "여럿(같은 품)": 1, "여럿(다른 품)": 2, "없음": 3, "못 셈": 4}
|
||||
|
||||
|
||||
def ex_of(e):
|
||||
texts = [e["이름"], e.get("규격") or ""] + [str(t) for t in e.get("글") or []]
|
||||
sp = set()
|
||||
for m in e["장비"]:
|
||||
sp |= set(m.get("규격") or [])
|
||||
sp |= specnums(f"{e['이름']} {e.get('규격') or ''}")
|
||||
return {
|
||||
"글": texts,
|
||||
"글머리": texts[:8],
|
||||
"계수": e.get("계수") or {},
|
||||
"규격수": sp,
|
||||
"조건": e.get("조건") or {},
|
||||
"품값": {Decimal(str(x["품"])) for x in e["인력"].values() if x.get("품")},
|
||||
}
|
||||
|
||||
|
||||
def plans(codes, ex):
|
||||
"""몫마다 (조합, 어떻게, 움직임, 안 닿음) → 복합이면 곱(넘치면 몫마다 하나씩)."""
|
||||
ps = {k: K.plan(k, ex) for k in codes}
|
||||
if len(codes) == 1:
|
||||
k = codes[0]
|
||||
cb, how, act, ine, cls = ps[k]
|
||||
return [({k: c}) for c in cb], how, {k: act}, {k: ine}, {k: cls}
|
||||
pools = [[(k, c) for c in ps[k][0]] for k in codes]
|
||||
size = 1
|
||||
for p in pools:
|
||||
size *= len(p)
|
||||
if size <= 4000:
|
||||
out = [dict(x) for x in itertools.product(*pools)]
|
||||
how = "모두"
|
||||
else: # 몫마다 하나씩(나머지 몫은 첫 조합)
|
||||
first = {k: ps[k][0][0] for k in codes}
|
||||
out = [dict(first)] + [{**first, k: c} for k in codes for c in ps[k][0][1:]]
|
||||
how = "하나씩"
|
||||
return out, how, {k: ps[k][2] for k in codes}, {k: ps[k][3] for k in codes}, {k: ps[k][4] for k in codes}
|
||||
|
||||
|
||||
def vec(given):
|
||||
vs = [K.run(k, c) for k, c in given.items()]
|
||||
if any(v is None for v in vs):
|
||||
return None
|
||||
return vs[0] if len(vs) == 1 else K.add(vs)
|
||||
|
||||
|
||||
def judge(e, codes):
|
||||
ex = ex_of(e)
|
||||
cbs, how, act, ine, cls = plans(codes, ex)
|
||||
rows = []
|
||||
for g in cbs:
|
||||
v = vec(g)
|
||||
rows.append((g, v, K.sig(v)))
|
||||
ran = [r for r in rows if r[1] is not None and (r[1]["L"] or r[1]["X"] or r[1]["M"])]
|
||||
out = {"로직": codes, "조합": len(cbs), "어떻게": how, "움직임": act, "안닿음": ine, "돈 조합": len(ran), "못 돎": sum(1 for r in rows if r[1] is None)}
|
||||
if not ran:
|
||||
out["판정"] = {m: "못 셈" for m, _, _ in MODES}
|
||||
return out
|
||||
for mk, t, rnd in MODES:
|
||||
hit = [(g, v, s) for g, v, s in ran if (K.compare(e, v, t, rnd=rnd) or {}).get("핵심")]
|
||||
lab = [(g, v, s) for g, v, s in ran if (K.compare(e, v, t, rnd=rnd) or {}).get("직종만")]
|
||||
base = [(g, v, s) for g, v, s in ran if (K.compare(e, v, t, base=True, rnd=rnd) or {}).get("핵심")]
|
||||
sigs = {K.csig(e, v) for _, v, _ in hit}
|
||||
vary = sorted({f"{k}.{n}" for k in codes for n in K.vary([g[k] for g, _, _ in hit], act[k], cls[k])})
|
||||
if not hit:
|
||||
verdict = "없음"
|
||||
elif len(sigs) > 1:
|
||||
verdict = "여럿(다른 품)"
|
||||
elif vary:
|
||||
verdict = "여럿(같은 품)"
|
||||
else:
|
||||
verdict = "하나만"
|
||||
out.setdefault("판정", {})[mk] = verdict
|
||||
out[f"맞음{mk}"] = {
|
||||
"조합": len(hit),
|
||||
"서명": len(sigs),
|
||||
"전체서명": len({s for _, _, s in hit}),
|
||||
"갈리는 칸": vary,
|
||||
"직종만 맞음": len(lab),
|
||||
"밑품 맞음": len(base),
|
||||
"보기": [{k: {n: str(x) for n, x in c.items() if n in act[k]} for k, c in g.items()} for g, _, _ in hit[:3]],
|
||||
"기본값 맞음": any(all(all(str(c.get(n)) == str(ex_k) for n, ex_k in _defaults(k).items() if n in act[k]) for k, c in g.items()) for g, _, _ in hit),
|
||||
"덧": sorted({d for _, v, _ in hit[:20] for d in (K.compare(e, v, t, rnd=rnd) or {}).get("덧", [])}),
|
||||
}
|
||||
if len(codes) == 1: # 지금 M02 규격 읽기(spec_inputs)로 낸 입력값 — 같은 품이 나오나
|
||||
k = codes[0]
|
||||
try:
|
||||
vals, unread = K.O.hm.spec_inputs({"이름": e["이름"], "규격": e.get("규격"), "단위": e.get("단위")}, K.O.hm._logic_specs(k))
|
||||
except Exception: # noqa: BLE001
|
||||
vals, unread = {}, ["(읽기 오류)"]
|
||||
v = K.run(k, {**_defaults(k), **vals})
|
||||
out["지금 읽기"] = {"입력": {n: str(x) for n, x in vals.items()}, "못읽음": unread,
|
||||
**{m: bool((K.compare(e, v, t, rnd=rnd) or {}).get("핵심")) for m, t, rnd in MODES}} # fmt: skip
|
||||
if not out["맞음0.03"]["조합"] and not out["맞음R"]["조합"]:
|
||||
out["풀이"] = explain(e, ran, codes, act)
|
||||
best = min(ran, key=lambda r: K.gap(e, r[1]))
|
||||
bb = min(ran, key=lambda r: K.gap(e, r[1], base=True))
|
||||
out["가장 가까움"] = {"입력": {k: {n: str(x) for n, x in c.items() if n in act[k]} for k, c in best[0].items()}, "거리": round(K.gap(e, best[1]), 4),
|
||||
"우리": _show(best[1]), "견줌3": K.compare(e, best[1], 0.03), "밑품거리": round(K.gap(e, bb[1], base=True), 4),
|
||||
"밑품견줌3": K.compare(e, bb[1], 0.03, base=True), "밑품우리": _show(bb[1])} # fmt: skip
|
||||
return out
|
||||
|
||||
|
||||
def _scale(v, kl, kx=None):
|
||||
return {**v, "L": {k: q * kl for k, q in v["L"].items()}, "X": [(a, b, q * (kx if kx is not None else kl)) for a, b, q in v["X"]]}
|
||||
|
||||
|
||||
def _mean(vs, harmonic=False):
|
||||
"""같은 꼴(직종 묶음 · 장비 수 같음) 벡터들의 평균 — 조화 평균은 일 시공량 평균(1 / 평균(1 / 품))."""
|
||||
if len({tuple(sorted(v["L"])) for v in vs}) != 1 or len({len(v["X"]) for v in vs}) != 1:
|
||||
return None
|
||||
f = (lambda xs: len(xs) / sum(1 / x for x in xs) if all(xs) else 0.0) if harmonic else (lambda xs: sum(xs) / len(xs)) # noqa: E731
|
||||
L = {k: f([v["L"][k] for v in vs]) for k in vs[0]["L"]}
|
||||
X = [(a, b, f([v["X"][i][2] for v in vs])) for i, (a, b, _) in enumerate(vs[0]["X"])]
|
||||
return {"L": L, "X": X, "M": vs[0]["M"], "S": vs[0]["S"], "mix": {}}
|
||||
|
||||
|
||||
def explain(e, ran, codes, act, t=0.03):
|
||||
"""「없음」 풀이 — 섞기 전(우리 섞음 비율 풂) · 한 배수 · 옵션 둘 합 · 옵션 평균(산술 · 조화)."""
|
||||
got = {}
|
||||
for g, v, _ in ran: # 우리 로직의 섞음(인력비율 · 기계비율)을 풀면 같나
|
||||
ri, rm = (v.get("mix") or {}).get("인력비율"), (v.get("mix") or {}).get("기계비율")
|
||||
if ri and 0 < ri < 100:
|
||||
u = _scale(v, 100 / ri, (100 / rm) if rm and 0 < rm < 100 else 1.0)
|
||||
if (K.compare(e, u, t) or {}).get("직종만"):
|
||||
got["섞기 전 맞음"] = f"인력 {ri:g}% → 100%"
|
||||
break
|
||||
got.update(ratio(e, ran, act, t))
|
||||
pool = sorted(ran, key=lambda r: K.gap(e, r[1]))[:120]
|
||||
for tt in (0.005, t): # 0.5% 에서 맞으면 그 값(우연 맞음 덜함) · 아니면 3%
|
||||
for (g1, v1, _), (g2, v2, _) in itertools.combinations(pool, 2):
|
||||
if (K.compare(e, K.add([v1, v2]), tt) or {}).get("직종만"):
|
||||
got["옵션 둘 합"] = {"값": [_opt(g1, act), _opt(g2, act)], "허용": tt}
|
||||
break
|
||||
if "옵션 둘 합" in got:
|
||||
break
|
||||
for tt in (0.005, t):
|
||||
_avg(e, ran, codes, act, tt, got)
|
||||
if "옵션 평균" in got:
|
||||
got["옵션 평균"]["허용"] = tt
|
||||
break
|
||||
return got
|
||||
|
||||
|
||||
def _avg(e, ran, codes, act, t, got):
|
||||
if len(codes) == 1:
|
||||
k0 = codes[0]
|
||||
for n in act[k0]:
|
||||
groups = {}
|
||||
for g, v, _ in ran:
|
||||
rest = json.dumps({x: str(y) for x, y in g[k0].items() if x != n}, sort_keys=True, ensure_ascii=False)
|
||||
groups.setdefault(rest, []).append((g, v))
|
||||
for members in groups.values():
|
||||
for size in (2, 3, 4):
|
||||
for sub in itertools.combinations(members, size):
|
||||
for hm_ in (False, True):
|
||||
m = _mean([v for _, v in sub], hm_)
|
||||
if m and (K.compare(e, m, t) or {}).get("직종만"):
|
||||
got["옵션 평균"] = {"칸": n, "값": [str(g[k0].get(n)) for g, _ in sub], "꼴": "조화(일 시공량 평균)" if hm_ else "산술"}
|
||||
break
|
||||
if "옵션 평균" in got:
|
||||
break
|
||||
if "옵션 평균" in got:
|
||||
break
|
||||
if "옵션 평균" in got:
|
||||
break
|
||||
if "옵션 평균" in got:
|
||||
break
|
||||
|
||||
|
||||
def _ratios(e, v):
|
||||
"""엑셀/우리 비 — 직종(묶음 같을 때) · 같은 갈래 장비 시간. 직종 묶음이 다르면 None."""
|
||||
E = {k: x["품"] for k, x in e["인력"].items() if x.get("품")}
|
||||
O_ = {k: q for k, q in v["L"].items() if q}
|
||||
if E and set(E) != set(O_):
|
||||
return None
|
||||
rs = [E[k] / O_[k] for k in E]
|
||||
for m in e["장비"]:
|
||||
if not m.get("시간"):
|
||||
continue
|
||||
mine = [q for a, _, q in v["X"] if q and set(a) & set(m["갈래"])]
|
||||
if mine:
|
||||
rs.append(m["시간"] / mine[0])
|
||||
return rs or None
|
||||
|
||||
|
||||
def ratio(e, ran, act, t=0.03):
|
||||
"""한 배수 — 모든 몫(직종 · 장비 시간)이 같은 배수 k(3% 안 · k ≠ 1)인 조합.
|
||||
k 고르기: 엑셀 글의 수 · 율로 설명되는 k(÷6 · ×1.2 · ×0.1 …) → 몫 수 많은 것 → 1 에 가까운 것."""
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
text = unicodedata.normalize("NFKC", " ".join([e["이름"], e.get("규격") or ""] + [str(x) for x in e.get("글") or []]))
|
||||
divs = {float(n) for n in re.findall(r"[÷/]\s*\(?\s*(\d+(?:\.\d+)?)", text) if float(n) > 0}
|
||||
muls = {float(n) for n in re.findall(r"[×*x]\s*(\d+(?:\.\d+)?)(?!\s*[,\d])", text) if 0 < float(n) < 50}
|
||||
pcts = {float(n) / 100 for n in re.findall(r"(\d+(?:\.\d+)?)\s*%", text) if 0 < float(n) < 100}
|
||||
rates = {float(r[0]) for x in e["인력"].values() for r in x.get("율") or [] if r and r[0]} | pcts
|
||||
cands = []
|
||||
for g, v, _ in ran:
|
||||
rs = _ratios(e, v)
|
||||
if not rs or max(rs) / min(rs) > 1 + t:
|
||||
continue
|
||||
k = sum(rs) / len(rs)
|
||||
if abs(k - 1) <= t:
|
||||
continue
|
||||
why = next((f"÷ {n:g}" for n in divs if abs(k * n - 1) <= 0.01), None)
|
||||
why = why or next((f"× {n:g}" for n in muls if abs(k - n) <= 0.01 * max(k, n)), None)
|
||||
why = why or next((f"율 {r:g}" for r in rates if abs(k - r) <= 0.01 * max(k, r) or abs(k * r - 1) <= 0.01), None)
|
||||
cands.append((why is not None, len(rs), -abs(math.log(k)), k, why, g))
|
||||
if not cands:
|
||||
return {}
|
||||
cands.sort(key=lambda c: c[:3], reverse=True)
|
||||
ok, n, _, k, why, g = cands[0]
|
||||
return {"한 배수": round(k, 4), "몫 수": n, "배수 까닭": why, "배수 입력": _opt(g, act)}
|
||||
|
||||
|
||||
def _opt(g, act):
|
||||
return {k: {n: str(x) for n, x in c.items() if n in act[k]} for k, c in g.items()}
|
||||
|
||||
|
||||
_DEF = {}
|
||||
|
||||
|
||||
def _defaults(k):
|
||||
if k not in _DEF:
|
||||
sp = K.space(k)
|
||||
_DEF[k] = {n: o[0] for n, o, _ in sp}
|
||||
return _DEF[k]
|
||||
|
||||
|
||||
def _show(v):
|
||||
if v is None:
|
||||
return None
|
||||
return {"L": {k: round(q, 5) for k, q in v["L"].items() if q}, "X": [[a, round(q, 5)] for a, _, q in v["X"] if q], "M": {k: round(q, 5) for k, q in v["M"].items() if q}}
|
||||
|
||||
|
||||
def line(e):
|
||||
alts = [e["복합"]] if e.get("복합") else [[k] for k in e["정답"]]
|
||||
got = []
|
||||
for codes in alts:
|
||||
if not all(K.WHOLE.logic(k) for k in codes):
|
||||
got.append({"로직": codes, "판정": {m: "못 셈" for m, _, _ in MODES}, "까닭": "로직 없음"})
|
||||
continue
|
||||
got.append(judge(e, codes))
|
||||
best = {m: min(got, key=lambda g: RANK[g["판정"][m]]) for m, _, _ in MODES}
|
||||
allow = {}
|
||||
for k in e.get("허용") or []:
|
||||
if K.WHOLE.logic(k):
|
||||
j = judge(e, [k])
|
||||
allow[k] = j["판정"]
|
||||
return {"id": e["id"], "무리": e["무리"], "이름": e["이름"], "규격": e.get("규격"), "단위": e.get("단위"), "정답": e["정답"], "갈래": got,
|
||||
"판정": {t: best[t]["판정"][t] for t in best}, "판정로직": {t: best[t]["로직"] for t in best}, "허용판정": allow,
|
||||
"엑셀": {"L": {k: x["품"] for k, x in e["인력"].items()}, "Lb": {k: x["밑품"] for k, x in e["인력"].items()},
|
||||
"X": [[m["갈래"], m.get("시간"), m.get("밑시간")] for m in e["장비"] if m.get("시간")], "M": [[m["이름"], m["품"]] for m in e["자재"] if m.get("품")],
|
||||
"율": [r for x in e["인력"].values() for r in x.get("율") or []], "옛판": e.get("옛판"), "출처": e.get("출처")}} # fmt: skip
|
||||
|
||||
|
||||
def main():
|
||||
T = json.loads((K.HERE / "targets.json").read_text(encoding="utf-8"))
|
||||
if sys.argv[1:] == ["merge"]:
|
||||
out = []
|
||||
for p in sorted(K.HERE.glob("ans_*.json"), key=lambda p: int(p.stem.split("_")[1])):
|
||||
out += json.loads(p.read_text(encoding="utf-8"))
|
||||
(K.HERE / "answer.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8")
|
||||
print("합", len(out))
|
||||
return
|
||||
a, b = int(sys.argv[1]), int(sys.argv[2])
|
||||
n0 = K.load_cache()
|
||||
t0 = time.time()
|
||||
out = []
|
||||
for e in T[a:b]:
|
||||
out.append(line(e))
|
||||
if time.time() - t0 > 70: # 명령 하나 120 초 안 — 남은 줄은 다음 명령
|
||||
break
|
||||
K.save_cache()
|
||||
(K.HERE / f"ans_{a}.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8")
|
||||
print("끝", a, a + len(out), "캐시", n0, "→", len(K._RUN), f"{time.time() - t0:.1f}s")
|
||||
for r in out:
|
||||
print(" ", r["id"], r["이름"][:16], r["판정"], r["판정로직"]["0.03"], [(g["조합"], g["어떻게"]) for g in r["갈래"]])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,236 @@
|
||||
"""K5 ④ 할 일 1 · 2 정리 — 옵션 거꾸로 찾기 표 · 「없음」 나눔.
|
||||
python k5_s4_sum12.py → 표 찍음 · sum12.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
import k5_lib as K
|
||||
|
||||
A = json.loads((K.HERE / "answer.json").read_text(encoding="utf-8"))
|
||||
T = {e["id"]: e for e in json.loads((K.HERE / "targets.json").read_text(encoding="utf-8"))}
|
||||
UNIT = {x["로직"]: x.get("단위") for x in K.O.hm.targets()["로직"]}
|
||||
V = ("하나만", "여럿(같은 품)", "여럿(다른 품)", "없음", "못 셈")
|
||||
import s10_all as C1 # noqa: E402 (사람 정답 줄의 엑셀 줄 수)
|
||||
|
||||
ROWS = {i: C1.BY[i].get("줄수") or 1 for i in C1.BY}
|
||||
|
||||
|
||||
def rows_of(i):
|
||||
return ROWS.get(i) or T[i].get("줄수") or 1
|
||||
|
||||
|
||||
def split_same(r, t):
|
||||
"""여럿(같은 품) 나눔 — 이름만 다름(모든 몫 같음) / 엑셀에 안 보이는 몫(폼타이 · 기계 규격)이 갈림."""
|
||||
g = pick(r, t)
|
||||
m = g.get(f"맞음{t}") or {}
|
||||
return "이름만 다름" if m.get("전체서명", 1) <= 1 else "엑셀에 안 보이는 몫 갈림"
|
||||
COUNT = {"개소", "개", "본", "ea", "조", "매", "대", "기", "공"}
|
||||
HOUR = {"hr", "시간", "h"}
|
||||
|
||||
|
||||
def unit_fit(a, b):
|
||||
if K.O.hm.fits(a, b):
|
||||
return True
|
||||
na = re.sub(r"[\s/]", "", str(a or "")).lower()
|
||||
for nb in [re.sub(r"[\s/]", "", x).lower() for x in str(b or "").split("·")]:
|
||||
if na == nb or (na in COUNT and nb in COUNT) or (na in HOUR and nb in HOUR):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def group(r):
|
||||
if r["무리"] == "사람":
|
||||
return {"N": "사람 · 북부청 임도", "S": "사람 · 북부청 사방", "Y": "사람 · 동부청 영월"}[r["id"][0]]
|
||||
return "새 파일 · " + r["무리"]
|
||||
|
||||
|
||||
def pick(r, t="0.03"):
|
||||
return next(g for g in r["갈래"] if g["로직"] == r["판정로직"][t])
|
||||
|
||||
|
||||
def flat(e):
|
||||
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", " ".join([e["이름"], e.get("규격") or ""] + [str(x) for x in e.get("글") or []])))
|
||||
|
||||
|
||||
def plausible(e, found):
|
||||
"""옵션 합 · 평균이 엑셀 글로 받쳐지나 — 갈리는 칸 값의 한글 낱말(두 자+)이 글에 있음."""
|
||||
text = flat(e)
|
||||
if "칸" in found: # 평균
|
||||
vals = found["값"]
|
||||
else: # 합 — 두 조합에서 다른 값
|
||||
a, b = found["값"]
|
||||
vals = []
|
||||
for k in a:
|
||||
for n in a[k]:
|
||||
if a[k][n] != b[k].get(n):
|
||||
vals += [a[k][n], b[k][n]]
|
||||
if not vals:
|
||||
return False
|
||||
for v in vals:
|
||||
ws = re.findall(r"[가-힣]{2,}", unicodedata.normalize("NFKC", str(v)))
|
||||
if not ws or not any(w in text for w in ws):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# 원문(2026 건설 · 2025 산림) 대조로 확인한 줄 — 까닭 · 근거(어느 쪽이 원문과 같나)
|
||||
CHECKED = {
|
||||
"U012": ("옛 판 값", "건설 6-2-3 조립 Ⅱ-2 시공량 2026 = 1.1 ton(우리) · 엑셀 1.0 ton 옛 값"),
|
||||
"Y014": ("옛 판 값", "건설 7-2-2 찰붙임('26 보완) 35cm이하 30㎡/일 = 우리 · 엑셀 석공 0.05 는 옛 값"),
|
||||
"C016": ("옛 판 값", "건설 2-6-1 동바리('26 보완) 2.5m이하 50공㎥ = 우리 0.06(간격 0.8m 초과 0.054) · 엑셀 0.05 = 옛 60공㎥"),
|
||||
"B031": ("옛 판 값", "건설 2-6-1 동바리('26 보완) = 우리 · 엑셀 0.054 · 0.021 은 2024 파일 옛 값(2-2-4 손율 인용)"),
|
||||
"B032": ("옛 판 값", "건설 2-7-1 비계('26 보완) 10m이하 55㎡ = 우리 · 엑셀 0.05 · 0.02 옛 값"),
|
||||
"N032": ("옛 판 값", "건설 3-5-1 고르기('25 보완) 연질토 250㎡ = 우리 0.032 · 엑셀 0.0276 = 옛 290㎡"),
|
||||
"N040": ("할증 · 횟수 붙임", "건설 1-6-2 A-Type 20㎝ 100㎥(우리 그대로) × 1.2 = 산림 12-6 ⑧ 종단경사 20% 할증을 건설 로직에 붙임"),
|
||||
"S072": ("단위 환산", "건설 6-7-2 원문 본당(우리 그대로 · 관경 1000) ÷ 6 m/본 = 엑셀 m 당"),
|
||||
"S083": ("단위 환산", "건설 6-7-2 본당(관경 500) ÷ 6 m/본"),
|
||||
"Y115": ("단위 환산", "건설 6-7-2 본당(관경 800) ÷ 6 m/본 · 크레인 0.25/6 을 0.04 로 반올림"),
|
||||
"N017": ("직종 다름(더함 · 이름)", "건설 6-3-10 다웰바 형틀목공 0.043 · 보통인부 0.009 = 우리 · 엑셀은 철근 가공(6-2) 철근공을 더함"),
|
||||
"N015": ("자재만 다름", "건설 6-3-4 품 0.07 · 0.03 = 우리 · 엑셀이 문양판 수량에 1/0.72 를 붙임(원문엔 없음)"),
|
||||
"C018": ("옵션을 둘 넘게 섞음(합 · 평균)", "지주 + 표지판 두 옵션 합이 0.5% 안 — 숫자가 북부청 N051 과 같음(글에 「지주」 낱말만 없음)"),
|
||||
"N039": ("묶음(다른 일을 한 줄에)", "잔디붙임(건설 4-1-1) 조경공 0.0067 그대로 + 떼꽂이(산림 5-23-3) 보통인부를 더함"),
|
||||
"S079": ("쪼갬(우리 한 로직을 두 줄로)", "원형돌망태(산림 13-11-1) 한 로직을 조립설치 · 채움 두 줄로 나눔"),
|
||||
"S080": ("쪼갬(우리 한 로직을 두 줄로)", "원형돌망태(산림 13-11-1) 한 로직을 조립설치 · 채움 두 줄로 나눔"),
|
||||
"N020": ("할증 · 횟수 붙임", "산림 13-2-3 큰돌 채집 × 품할증 0.75 × 발생석 50% · 개 → ㎡(4 개/㎡)"),
|
||||
"S094": ("할증 · 횟수 붙임", "산림 13-2-3 큰돌 채집 × 품할증 0.75 · 개 → ㎡"),
|
||||
"U015": ("다른 책 로직 숫자와 같음", "건설 1-6-2 B-Type 20㎝ 4/50 · 2/50 = 0.08 · 0.04 × 두께 0.2 = 0.016 · 0.008 — 씨앗은 산림 GF000162"),
|
||||
"Y027": ("옛 판 값", "건설 6-7-1 플륨관('25 보완) 원문은 굴착기 편성 = 우리 · 엑셀(2024)은 크레인 · 다른 시공량(옛 값 추정)"),
|
||||
"B026": ("옛 판 값", "건설 4-1-1 잔디붙임('24 보완) 평떼 150㎡ = 우리 0.0267 · 엑셀(2024) 0.0291 옛 값"),
|
||||
"B010": ("옛 판 값", "산림 13-3-2(2025) 75mm 미만 0.019 · 굴착기 0.076 = 우리 · 엑셀(2024) 0.018 · 0.07 옛 값"),
|
||||
"B017": ("옛 판 값", "건설 6-1-2 기계비빔 무근 0.15 · 0.46 = 우리(GF000156 같은 숫자) · 엑셀(2024) 0.03 · 0.31 옛 값(추정)"),
|
||||
"Y028": ("옛 판 값", "건설 6-1-1('24 보완) 무근 장비사용 63㎥ = 우리 · 엑셀(2024) 0.09 · 0.02 · 굴착기 0.31 옛 값(추정)"),
|
||||
"C013": ("옛 판 값", "건설 6-1-1('24 보완) 철근 장비사용 55㎥ = 우리 0.0545 · 엑셀 0.07 · 0.02 옛 값(추정)"),
|
||||
}
|
||||
OURC = K.O.hb.ours()
|
||||
VERIFY_NOTE = "원문 확인"
|
||||
|
||||
|
||||
def why_none(r):
|
||||
"""「없음」(3% · R 모두) 한 줄의 까닭(차례대로) + 덧 꼬리표."""
|
||||
e = T[r["id"]]
|
||||
g = pick(r)
|
||||
p = g.get("풀이") or {}
|
||||
best = g.get("가장 가까움") or {}
|
||||
c3 = best.get("견줌3") or {}
|
||||
m3 = g.get("맞음0.03") or {}
|
||||
rates = r["엑셀"]["율"]
|
||||
tags = []
|
||||
if c3.get("장비") == "엑셀만" or any("엑셀만 장비" in d for d in (c3.get("덧") or [])):
|
||||
tags.append("엑셀이 장비 일을 더함")
|
||||
if r["id"] in CHECKED:
|
||||
cat, note = CHECKED[r["id"]]
|
||||
return cat, tags + [VERIFY_NOTE], note
|
||||
allow = [k for k, v in r["허용판정"].items() if v["0.03"] != "없음" or v.get("R") != "없음"]
|
||||
if allow:
|
||||
return "다른 책 로직 숫자와 같음", tags, f"허용 {allow[0]} 이 맞음"
|
||||
if m3.get("밑품 맞음"):
|
||||
notes = {n for _, n in rates}
|
||||
kind = "섞음(엑셀이 인력 % · 기계 % 를 곱함)" if notes & {"섞음", "%"} else "할증 · 횟수 붙임"
|
||||
return kind, tags, f"율 {rates[:2]}"
|
||||
if "섞기 전 맞음" in p:
|
||||
return "섞음 비율을 엑셀이 바꿈(우리 로직 섞음 고정)", tags, p["섞기 전 맞음"]
|
||||
for key in ("옵션 평균", "옵션 둘 합"):
|
||||
if key in p and plausible(e, p[key]):
|
||||
return "옵션을 둘 넘게 섞음(합 · 평균)", tags, json.dumps(p[key], ensure_ascii=False)[:160]
|
||||
E = r["엑셀"]["L"]
|
||||
O_ = (best.get("우리") or {}).get("L") or {}
|
||||
books = {b for b, _ in (r["엑셀"]["출처"] or [])}
|
||||
ours = {(OURC.get(k) or {}).get("책") for k in g["로직"]}
|
||||
if "한 배수" in p and p.get("배수 까닭") and (p.get("몫 수", 0) >= 2 or str(p["배수 까닭"]).startswith("율")):
|
||||
code = g["로직"][0]
|
||||
if not unit_fit(e.get("단위"), UNIT.get(code)) and str(p["배수 까닭"]).startswith("÷"):
|
||||
return "단위 환산", tags, f"배수 {p['한 배수']} ({p['배수 까닭']}) · 단위 {e.get('단위')} ↔ {UNIT.get(code)} · {p.get('배수 입력')}"
|
||||
return "할증 · 횟수 붙임", tags, f"배수 {p['한 배수']} ({p['배수 까닭']}) · {p.get('배수 입력')}"
|
||||
if books and not (books & ours):
|
||||
return "다른 책 숫자(짝 · 정답은 다른 책 로직)", tags, f"엑셀 인용 {sorted(books)} ↔ 우리 {sorted(ours)}"
|
||||
if E and set(E) != set(O_):
|
||||
return "직종 다름(더함 · 이름)", tags, f"엑셀 {sorted(E)} ↔ 우리 {sorted(O_)}"
|
||||
if E and c3.get("직종") == "같음":
|
||||
return "장비 · 자재만 다름", tags, f"장비 {c3.get('장비')} · 자재 {c3.get('자재')}"
|
||||
if not E:
|
||||
return "장비만 줄 — 시간 · 갈래 다름", tags, f"엑셀 {[(a, round(h, 4)) for a, h, _ in r['엑셀']['X'][:2]]} ↔ 우리 {(best.get('우리') or {}).get('X')}"
|
||||
if r["엑셀"]["옛판"]:
|
||||
return "옛 판 값", tags, f"엑셀 글에 옛 판 표시 · 출처 {r['엑셀']['출처']}"
|
||||
if "엑셀이 장비 일을 더함" in tags:
|
||||
return "묶음(다른 일을 한 줄에)", tags, f"엑셀 {E} ↔ 우리 {O_}"
|
||||
return "계수 다름 — 까닭 못 가림", tags, f"엑셀 {E} ↔ 우리 {O_}"
|
||||
|
||||
|
||||
def main():
|
||||
groups = defaultdict(list)
|
||||
for r in A:
|
||||
groups[group(r)].append(r)
|
||||
order = ["사람 · 북부청 임도", "사람 · 북부청 사방", "사람 · 동부청 영월", "새 파일 · 자동", "새 파일 · 자동+씨앗", "새 파일 · 씨앗"]
|
||||
out = {"표1": {}, "없음": [], "반올림": [], "갈리는 칸": {}, "기본값": {}, "지금읽기": {}}
|
||||
print("== 표 1 — 옵션 거꾸로 찾기(정답 로직 · 고유 줄)")
|
||||
rows = [(n, groups[n]) for n in order] + [("사람 126", [r for r in A if r["무리"] == "사람"]), ("새 파일 51", [r for r in A if r["무리"] != "사람"]),
|
||||
("모두 177", A)] # fmt: skip
|
||||
for name, rs in rows:
|
||||
line = {}
|
||||
for t in ("0.005", "R", "0.03"):
|
||||
c = Counter(r["판정"][t] for r in rs)
|
||||
w = Counter()
|
||||
for r in rs:
|
||||
w[r["판정"][t]] += rows_of(r["id"])
|
||||
line[t] = {v: c.get(v, 0) for v in V}
|
||||
line[t + "줄무게"] = {v: w.get(v, 0) for v in V}
|
||||
for t in ("0.005", "R", "0.03"):
|
||||
line[t + "같은품나눔"] = dict(Counter(split_same(r, t) for r in rs if r["판정"][t] == "여럿(같은 품)"))
|
||||
out["표1"][name] = {"n": len(rs), **line}
|
||||
f = lambda d: " · ".join(f"{d[v]}" for v in V) # noqa: E731
|
||||
wt = lambda t: f"{sum(line[t + '줄무게'][v] for v in V[:3])}/{sum(line[t + '줄무게'].values())}" # noqa: E731
|
||||
print(f"{name:<18} n={len(rs):>3} | 0.5% {f(line['0.005'])} | R {f(line['R'])} | 3% {f(line['0.03'])} | 줄무게 맞음 {wt('0.005')} · {wt('R')} · {wt('0.03')} | 같은품 {line['R같은품나눔']}")
|
||||
# 맞은 줄 — 기본값 그대로 맞음 · 지금 M02 규격 읽기로 맞음 · 옵션을 바꿔야 맞음
|
||||
for t in ("0.005", "R", "0.03"):
|
||||
ok = [r for r in A if r["판정"][t] in V[:3]]
|
||||
d = Counter()
|
||||
for r in ok:
|
||||
g = pick(r, t)
|
||||
m = g.get(f"맞음{t}") or {}
|
||||
now = (g.get("지금 읽기") or {}).get(t)
|
||||
d["기본값 그대로 맞음"] += bool(m.get("기본값 맞음"))
|
||||
d["지금 M02 규격 읽기로 맞음"] += bool(now)
|
||||
d["옵션을 바꿔야 맞음(기본값 아님)"] += not m.get("기본값 맞음")
|
||||
d["옵션 바꿔야 + 지금 읽기 못 맞춤"] += (not m.get("기본값 맞음")) and not now
|
||||
out["기본값"][t] = {"맞은 줄": len(ok), **d}
|
||||
print(t, "맞은 줄", len(ok), dict(d))
|
||||
# 여럿 — 갈리는 칸
|
||||
for t in ("0.005", "R", "0.03"):
|
||||
cnt = Counter()
|
||||
for r in A:
|
||||
if r["판정"][t].startswith("여럿"):
|
||||
g = pick(r, t)
|
||||
m = g.get(f"맞음{t}") or {}
|
||||
for n in m.get("갈리는 칸") or []:
|
||||
cnt[n.split(".", 1)[1]] += 1
|
||||
out["갈리는 칸"][t] = dict(cnt.most_common())
|
||||
print(t, "여럿 줄 갈리는 칸", dict(cnt.most_common(20)))
|
||||
# 반올림 — 3% 맞음 · 0.5% 없음
|
||||
for r in A:
|
||||
if (r["판정"]["0.03"] in V[:3] or r["판정"]["R"] in V[:3]) and r["판정"]["0.005"] == "없음":
|
||||
g = pick(r)
|
||||
b = (g.get("가장 가까움") or {}).get("우리")
|
||||
out["반올림"].append({"id": r["id"], "이름": r["이름"], "규격": r["규격"], "로직": g["로직"], "엑셀": r["엑셀"]["L"], "엑셀X": r["엑셀"]["X"][:2], "우리": b})
|
||||
print("반올림(3% 만 맞음)", len(out["반올림"]))
|
||||
for x in out["반올림"]:
|
||||
print(" ", x["id"], x["이름"][:14], x["로직"], x["엑셀"], [(a, round(h, 4)) for a, h, _ in x["엑셀X"]], "| 우리", x["우리"]["L"] if x["우리"] else None, (x["우리"] or {}).get("X"))
|
||||
# 없음 나눔
|
||||
cats = Counter()
|
||||
for r in A:
|
||||
if r["판정"]["0.03"] != "없음" or r["판정"]["R"] != "없음":
|
||||
continue
|
||||
cat, tags, note = why_none(r)
|
||||
cats[cat] += 1
|
||||
out["없음"].append({"id": r["id"], "무리": r["무리"], "이름": r["이름"], "규격": r["규격"], "로직": pick(r)["로직"], "까닭": cat, "덧": tags, "보기": note})
|
||||
print("== 없음(3%) 나눔", sum(cats.values()), dict(cats.most_common()))
|
||||
for x in out["없음"]:
|
||||
print(" ", x["id"], x["무리"][:2], x["이름"][:16], x["로직"], "|", x["까닭"], "|", x["보기"][:110], x["덧"])
|
||||
out["없음셈"] = dict(cats)
|
||||
(K.HERE / "sum12.json").write_text(json.dumps(out, ensure_ascii=False, indent=1, default=str), "utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,121 @@
|
||||
"""K5 ⑤ 맞춤 방법 흉내 — 정답을 모르는 채 (로직 + 옵션) 짝을 엑셀 품 닮음으로 줄 세움 · 금액 안 씀.
|
||||
|
||||
1 단계(거친 순위) = C4 지문 변형(`s3_match.P` · 1,438 로직)을 품 채널(인력 3 · 장비 2 · 자재 1 · 단위 0.5)로만 셈.
|
||||
2 단계(옵션 찾기) = 1 단계 위 K 로직만 K5 옵션 공간(엑셀 조건 낱말 · 수로 좁힘 · CAP)을 엔진으로 돌려
|
||||
0.5% · 반올림 허용(R) · 3% 「딱 맞음」과 거리.
|
||||
C4 지문만(`score_pair` 기본 무게) 점수도 같이 냄 — 견줌 · 섞기용.
|
||||
python k5_s5_rank.py <시작> <끝> → rank_<시작>.json · merge → rank.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
import k5_lib as K
|
||||
import k5_s2_answer as A2
|
||||
|
||||
import s3_match as S # C4 — P(우리 지문) · ex_prep · score_pair (같은 프로세스 · 읽기만)
|
||||
|
||||
TOPK = 15
|
||||
STAGE_CAP = 400
|
||||
QW = {"인력": 3.0, "장비": 2.0, "자재": 1.0, "단위": 0.5}
|
||||
MODES = A2.MODES
|
||||
|
||||
|
||||
def stage_b(e, code):
|
||||
ex = A2.ex_of(e)
|
||||
try:
|
||||
sp = K.space(code, ex)
|
||||
except Exception: # noqa: BLE001
|
||||
return {"수준": 9, "거리": 99.0}
|
||||
import math
|
||||
|
||||
if math.prod(len(o) for _, o, _ in sp) > 20000: # 너무 큰 공간은 좁힌 하나씩만
|
||||
pass
|
||||
old = K.CAP
|
||||
K.CAP = STAGE_CAP
|
||||
try:
|
||||
cb, how, act, ine, cls = K.plan(code, ex)
|
||||
finally:
|
||||
K.CAP = old
|
||||
best = {"수준": 9, "약수준": 9, "거리": 99.0}
|
||||
hits = {m: [] for m, _, _ in MODES}
|
||||
lab3 = 0
|
||||
for g in cb:
|
||||
v = K.run(code, g)
|
||||
if v is None or not (v["L"] or v["X"] or v["M"]):
|
||||
continue
|
||||
d = K.gap(e, v)
|
||||
lvl = weak = 9
|
||||
for i, (m, t, rnd) in enumerate(MODES):
|
||||
c = K.compare(e, v, t, rnd=rnd) or {}
|
||||
if c.get("핵심"):
|
||||
weak = min(weak, i)
|
||||
if K.strong(e, c):
|
||||
hits[m].append((g, K.csig(e, v)))
|
||||
lvl = min(lvl, i)
|
||||
lab3 += bool((K.compare(e, v, 0.03) or {}).get("직종만"))
|
||||
if (lvl, weak, d) < (best["수준"], best["약수준"], best["거리"]):
|
||||
best = {"수준": lvl, "약수준": weak, "거리": round(d, 4), "입력": {n: str(x) for n, x in g.items() if n in act},
|
||||
"견줌": K.compare(e, v, 0.03), "직종같음": set(v["L"]) == {k for k, x in e["인력"].items() if x.get("품")} and bool(v["L"])} # fmt: skip
|
||||
best["맞음수"] = {m: len(h) for m, h in hits.items()}
|
||||
best["서명수"] = {m: len({s for _, s in h}) for m, h in hits.items()}
|
||||
best["갈리는 칸"] = {m: K.vary([g for g, _ in h], act, cls) for m, h in hits.items() if h}
|
||||
best["직종만3"] = lab3
|
||||
best["조합"] = len(cb)
|
||||
best["어떻게"] = how
|
||||
return best
|
||||
|
||||
|
||||
def line(e):
|
||||
ep = S.ex_prep(e)
|
||||
coarse, full = [], {}
|
||||
for o in S.P:
|
||||
s, ch, v = S.score_pair(ep, o, QW)
|
||||
coarse.append((s, o["로직"]))
|
||||
full[o["로직"]] = S.score_pair(ep, o)[0]
|
||||
coarse.sort(key=lambda x: -x[0])
|
||||
top = [k for _, k in coarse[:TOPK]]
|
||||
fp_rank = sorted(full, key=lambda k: -full[k])
|
||||
b = {k: stage_b(e, k) for k in top}
|
||||
return {
|
||||
"id": e["id"],
|
||||
"무리": e["무리"],
|
||||
"정답": e["정답"],
|
||||
"허용": e.get("허용") or [],
|
||||
"복합": e.get("복합"),
|
||||
"거친": [[k, round(s, 3)] for s, k in coarse[:TOPK]],
|
||||
"지문만": fp_rank[:15],
|
||||
"지문점수": {k: round(full[k], 3) for k in set(fp_rank[:15]) | set(top)},
|
||||
"옵션": b,
|
||||
"정답거친차례": next((i + 1 for i, (_, k) in enumerate(coarse) if k in set(e["정답"])), None),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
T = json.loads((K.HERE / "targets.json").read_text(encoding="utf-8"))
|
||||
if sys.argv[1:] == ["merge"]:
|
||||
out = []
|
||||
for p in sorted(K.HERE.glob("rank_*.json"), key=lambda p: int(p.stem.split("_")[1])):
|
||||
out += json.loads(p.read_text(encoding="utf-8"))
|
||||
(K.HERE / "rank.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8")
|
||||
print("합", len(out), len({r["id"] for r in out}))
|
||||
return
|
||||
a, b = int(sys.argv[1]), int(sys.argv[2])
|
||||
n0 = K.load_cache()
|
||||
t0 = time.time()
|
||||
out = []
|
||||
for e in T[a:b]:
|
||||
t1 = time.time()
|
||||
out.append(line(e))
|
||||
print(e["id"], f"{time.time() - t1:.1f}s", flush=True)
|
||||
if time.time() - t0 > 70: # 명령 하나 120 초 안 — 남은 줄은 다음 명령
|
||||
break
|
||||
K.save_cache()
|
||||
(K.HERE / f"rank_{a}.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8")
|
||||
print("끝", a, a + len(out), "캐시", n0, "→", len(K._RUN), f"{time.time() - t0:.1f}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.setrecursionlimit(10000)
|
||||
main()
|
||||
@@ -0,0 +1,191 @@
|
||||
"""K5 ⑥ 할 일 3 · 4 정리 — (로직 + 옵션) 줄 세우기 수치 · 화면 「맞음」 표시 기준 안.
|
||||
python k5_s6_sum34.py → 표 찍음 · sum34.json
|
||||
"""
|
||||
|
||||
import json
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
import k5_lib as K
|
||||
|
||||
import s3_match as S
|
||||
|
||||
R = json.loads((K.HERE / "rank.json").read_text(encoding="utf-8"))
|
||||
T = {e["id"]: e for e in json.loads((K.HERE / "targets.json").read_text(encoding="utf-8"))}
|
||||
A = {r["id"]: r for r in json.loads((K.HERE / "answer.json").read_text(encoding="utf-8"))}
|
||||
C4 = json.loads((K.C4 / "out_match.json").read_text(encoding="utf-8"))["줄"]
|
||||
|
||||
|
||||
def single(e):
|
||||
"""숫자 하나뿐 줄 — 직종 하나 · 장비 없음 또는 장비 시간 하나 · 직종 없음(기계 1 시간 줄은 뺌). 같은 숫자 로직이 많아 우연 맞음."""
|
||||
E = [k for k, x in e["인력"].items() if x.get("품")]
|
||||
X = [m for m in e["장비"] if m.get("시간")]
|
||||
if len(E) + len(X) != 1:
|
||||
return False
|
||||
return not (X and abs(X[0]["시간"] - 1.0) < 1e-9)
|
||||
|
||||
|
||||
def lvl(r, k):
|
||||
b = r["옵션"].get(k)
|
||||
if b is None:
|
||||
return 9
|
||||
return 9 if single(T[r["id"]]) else b["수준"]
|
||||
|
||||
|
||||
def clause_ok(r, code):
|
||||
e = T[r["id"]]
|
||||
our = S.OURC.get(code)
|
||||
theirs = [list(x) for x in e.get("출처") or []]
|
||||
return bool(our and theirs and S.clause_prefix(theirs, our)[0] >= 0.9)
|
||||
|
||||
|
||||
def order_P(r):
|
||||
"""품만 — 2 단계 로직: 딱 맞음(강) 수준 → 약한 맞음 → 거리 → 1 단계 점수."""
|
||||
sc = {k: s for k, s in r["거친"]}
|
||||
b = r["옵션"]
|
||||
return sorted(b, key=lambda k: (lvl(r, k), b[k]["약수준"], b[k]["거리"], -sc.get(k, 0)))
|
||||
|
||||
|
||||
def order_P0(r):
|
||||
return [k for k, _ in r["거친"]]
|
||||
|
||||
|
||||
def order_F(r, lv=1):
|
||||
"""지문 + 딱 맞음 먼저 — 2 단계에서 딱 맞음(강, 수준 ≤ lv) 로직을 앞에(지문 점수 차례) · 나머지는 지문만 차례."""
|
||||
b, fs = r["옵션"], r["지문점수"]
|
||||
first = sorted([k for k in b if lvl(r, k) <= lv], key=lambda k: -fs.get(k, -99))
|
||||
return first + [k for k in r["지문만"] if k not in first]
|
||||
|
||||
|
||||
def order_C(r):
|
||||
return r["지문만"]
|
||||
|
||||
|
||||
def ok_top(order, S_):
|
||||
return bool(order) and order[0] in S_
|
||||
|
||||
|
||||
def metrics(rows, fn):
|
||||
n = len(rows)
|
||||
c = Counter()
|
||||
for r in rows:
|
||||
o = fn(r)
|
||||
Sx, Lx = set(r["정답"]), set(r["정답"]) | set(r["허용"])
|
||||
b = r["옵션"].get(o[0]) if o else None
|
||||
c["1위 엄격"] += ok_top(o, Sx)
|
||||
c["1위 느슨"] += ok_top(o, Lx)
|
||||
c["5안 엄격"] += bool(set(o[:5]) & Sx)
|
||||
c["5안 느슨"] += bool(set(o[:5]) & Lx)
|
||||
opt = ok_top(o, Sx) and b is not None and lvl(r, o[0]) <= 1
|
||||
c["1위 옵션까지(R)"] += opt
|
||||
c["1위 옵션까지(3%)"] += ok_top(o, Sx) and b is not None and lvl(r, o[0]) <= 2
|
||||
c["1위 옵션 하나로"] += opt and not (b.get("갈리는 칸") or {}).get("R")
|
||||
return {k: round(100 * v / n, 1) for k, v in c.items()} | {"n": n}
|
||||
|
||||
|
||||
def display(r, fn=order_F):
|
||||
"""화면 표시 갈래 — 1 위 (로직 + 옵션) 기준."""
|
||||
o = fn(r)
|
||||
b = r["옵션"].get(o[0]) if o else None
|
||||
exact = [k for k in r["옵션"] if lvl(r, k) <= 1]
|
||||
if b and lvl(r, o[0]) <= 1:
|
||||
return "맞음(로직 하나)" if len(exact) == 1 else "맞음(로직 여럿 — 고름)"
|
||||
if b and lvl(r, o[0]) == 2:
|
||||
return "거의 맞음(3% 안)"
|
||||
if b and b.get("직종같음") and clause_ok(r, o[0]):
|
||||
return "다른 점 보임(직종 · 인용 조항 같음 · 계수 다름)"
|
||||
return "후보"
|
||||
|
||||
|
||||
def main():
|
||||
groups = {
|
||||
"사람 126": [r for r in R if r["무리"] == "사람"],
|
||||
" 임도 49": [r for r in R if r["id"][0] == "N"],
|
||||
" 사방 43": [r for r in R if r["id"][0] == "S"],
|
||||
" 영월 34": [r for r in R if r["id"][0] == "Y"],
|
||||
"새 파일 51": [r for r in R if r["무리"] != "사람"],
|
||||
" 자동 정답 20": [r for r in R if "자동" in r["무리"]],
|
||||
" 씨앗 짝 43": [r for r in R if "씨앗" in r["무리"]],
|
||||
}
|
||||
ways = {
|
||||
"C4 지문만(로직만 · 조항 · 이름 덤)": order_C,
|
||||
"1 단계만(C4 변형 · 품 채널만)": order_P0,
|
||||
"품만(로직 + 옵션 · 딱 맞음 먼저)": order_P,
|
||||
"지문 + 딱 맞음(R) 먼저": order_F,
|
||||
"지문 + 딱 맞음(3%) 먼저": lambda r: order_F(r, 2),
|
||||
}
|
||||
out = {"수치": {}, "표시": {}, "표시줄": {}, "다른로직": [], "덮음": {}}
|
||||
print("== 할 일 3 — 1위 엄격/느슨 · 다섯 안 엄격/느슨 · 1위 옵션까지(R/3%) · 옵션 하나로")
|
||||
for g, rows in groups.items():
|
||||
for w, fn in ways.items():
|
||||
m = metrics(rows, fn)
|
||||
out["수치"].setdefault(g, {})[w] = m
|
||||
print(f"{g:<14}| {w:<26}| {m['1위 엄격']:>5}/{m['1위 느슨']:>5} · {m['5안 엄격']:>5}/{m['5안 느슨']:>5} · 옵션 {m['1위 옵션까지(R)']:>5}/{m['1위 옵션까지(3%)']:>5} · 하나로 {m['1위 옵션 하나로']:>5}")
|
||||
# C4 지문만 재현 확인(126) · C4 추천 수치
|
||||
have = [r for r in R if r["무리"] == "사람"]
|
||||
same = sum(1 for r in have if r["지문만"][:5] == C4[r["id"]]["지문만"][:5])
|
||||
print("C4 지문만 다섯 재현", same, "/", len(have))
|
||||
rec = sum(1 for r in have if C4[r["id"]]["추천"] if False) if False else None # noqa: SIM210
|
||||
# 1 단계가 정답을 15 안에 넣었나
|
||||
for g, rows in groups.items():
|
||||
cov = sum(1 for r in rows if set(r["정답"]) & {k for k, _ in r["거친"]})
|
||||
out["덮음"][g] = f"{cov}/{len(rows)}"
|
||||
print("1 단계 15 안 정답", out["덮음"])
|
||||
# 할 일 4 — 표시 갈래
|
||||
print("== 할 일 4 — 표시(지문 + 딱 맞음(R) 먼저 · 1 위)")
|
||||
for g in ("사람 126", "새 파일 51", " 씨앗 짝 43", " 자동 정답 20"):
|
||||
rows = groups[g]
|
||||
cnt = defaultdict(lambda: Counter())
|
||||
for r in rows:
|
||||
d = display(r)
|
||||
o = order_F(r)
|
||||
Sx, Lx = set(r["정답"]), set(r["정답"]) | set(r["허용"])
|
||||
cnt[d]["줄"] += 1
|
||||
cnt[d]["엑셀 줄"] += T[r["id"]].get("줄수") or 1
|
||||
cnt[d]["틀림 엄격"] += not ok_top(o, Sx)
|
||||
cnt[d]["틀림 느슨"] += not ok_top(o, Lx)
|
||||
if d.startswith("맞음"):
|
||||
b = r["옵션"][o[0]]
|
||||
cnt[d]["옵션 하나로"] += not (b.get("갈리는 칸") or {}).get("R")
|
||||
out["표시줄"].setdefault(g, []).append({"id": r["id"], "표시": d, "1위": o[0] if o else None, "정답": r["정답"], "맞음": ok_top(o, Sx)})
|
||||
out["표시"][g] = {d: dict(c) for d, c in cnt.items()}
|
||||
for d, c in sorted(cnt.items()):
|
||||
print(f" {g:<14} {d:<28} {dict(c)}")
|
||||
# C4 딱 맞음(sure)과 견줌 — 126
|
||||
sure_ids = set(json.loads((K.C4 / "out_match.json").read_text(encoding="utf-8"))["딱 맞음"])
|
||||
mine = {r["id"] for r in have if display(r).startswith("맞음")}
|
||||
print("C4 딱 맞음(고유 136 기준)", len(sure_ids & {r['id'] for r in have}), "· K5 맞음", len(mine), "· 둘 다", len(mine & sure_ids), "· K5 만", len(mine - sure_ids), "· C4 만", len((sure_ids & {r['id'] for r in have}) - mine))
|
||||
out["C4딱맞음견줌"] = {"C4": sorted(sure_ids & {r["id"] for r in have}), "K5": sorted(mine)}
|
||||
# 정답 로직이 「없음」인데 다른 로직이 엑셀 품을 그대로 냄
|
||||
for r in R:
|
||||
a = A[r["id"]]
|
||||
if a["판정"]["R"] == "없음" and a["판정"]["0.03"] == "없음":
|
||||
ex = [k for k in r["옵션"] if lvl(r, k) <= 1]
|
||||
if ex:
|
||||
out["다른로직"].append({"id": r["id"], "무리": r["무리"], "이름": T[r["id"]]["이름"], "정답": r["정답"], "딱 맞는 로직": ex,
|
||||
"입력": {k: r["옵션"][k].get("입력") for k in ex}}) # fmt: skip
|
||||
print("정답 로직은 없음인데 다른 로직이 딱 맞음(강 · R)", len(out["다른로직"]))
|
||||
for x in out["다른로직"]:
|
||||
print(" ", x["id"], x["무리"], x["이름"][:16], x["정답"], "→", x["딱 맞는 로직"], [K.WHOLE.logic(k).get("이름")[:16] for k in x["딱 맞는 로직"]], x["입력"])
|
||||
# C4 변형(입력 조합 64 안 · 범위 · 수 칸은 기본값)만으로 1 위 로직의 품이 엑셀과 같나 ↔ K5 옵션 찾기
|
||||
c4opt = Counter()
|
||||
for g, rows in (("사람 126", groups["사람 126"]), ("새 파일 51", groups["새 파일 51"])):
|
||||
for r in rows:
|
||||
e = T[r["id"]]
|
||||
o = order_C(r)
|
||||
if not o or o[0] not in set(r["정답"]):
|
||||
continue
|
||||
ep = S.ex_prep(e)
|
||||
pp = next(x for x in S.P if x["로직"] == o[0])
|
||||
_, _, v = S.score_pair(ep, pp)
|
||||
vv = {"L": v["L"] if v else {}, "X": [(sorted(a), sorted(b_), q) for a, b_, q in (v["X"] if v else [])], "M": v["M"] if v else {}, "S": {}}
|
||||
c = K.compare(e, vv, 0.005, rnd=True) if v and v.get("in") is not None else None
|
||||
c4opt[(g, "C4 변형으로 같은 품")] += bool(c and K.strong(e, c)) and not single(e)
|
||||
c4opt[(g, "K5 옵션 찾기로 같은 품")] += lvl(r, o[0]) <= 1
|
||||
c4opt[(g, "1위 로직 맞음")] += 1
|
||||
out["C4변형옵션"] = {f"{a}|{b}": v for (a, b), v in c4opt.items()}
|
||||
print("C4 변형 대 K5 옵션 찾기(1위 로직 맞은 줄 · R)", dict(c4opt))
|
||||
(K.HERE / "sum34.json").write_text(json.dumps(out, ensure_ascii=False, indent=1, default=str), "utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,77 @@
|
||||
"""K5 들여다보기 — 정답 로직 · 모든 로직의 입력 칸(고르기 · 범위 · 예시 · 기계 목록) 꼴과 조합 수. 읽기만."""
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발")
|
||||
HERE = Path(__file__).resolve().parent
|
||||
SP = HERE.parent.parent
|
||||
for p in (str(ROOT), str(SP / "req225" / "C1")):
|
||||
sys.path.insert(0, p)
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
from labels import L # noqa: E402
|
||||
from labels_y import LY # noqa: E402
|
||||
|
||||
FILES, WHOLE = m01.loaded()
|
||||
EQ = re.compile(r"^\d{4}-\d{4}$")
|
||||
wide = json.loads((SP / "req226/C7/wide.json").read_text(encoding="utf-8"))
|
||||
ans = set()
|
||||
for v in {**L, **LY}.values():
|
||||
ans |= set(v["정답"])
|
||||
for d in (wide["자동"], wide["씨앗"]):
|
||||
for v in d.values():
|
||||
ans |= set(v)
|
||||
codes = [x["로직"] for x in hm.targets()["로직"]]
|
||||
print("돈 로직", len(codes), "· 정답 로직", len(ans))
|
||||
|
||||
|
||||
def kinds(row):
|
||||
sel, eq, rng, other = [], [], [], []
|
||||
for s in row.get("입력") or []:
|
||||
o = s.get("고르기") or []
|
||||
if o and all(isinstance(v, str) and EQ.match(v) for v in o):
|
||||
eq.append((s["이름"], len(o)))
|
||||
elif o:
|
||||
sel.append((s["이름"], len(o)))
|
||||
elif isinstance(s.get("범위"), list):
|
||||
rng.append((s["이름"], s.get("범위"), s.get("예시")))
|
||||
else:
|
||||
other.append((s["이름"], s.get("예시"), s.get("단위")))
|
||||
return sel, eq, rng, other
|
||||
|
||||
|
||||
st = Counter()
|
||||
names = Counter()
|
||||
for c in codes:
|
||||
row = WHOLE.logic(c)
|
||||
sel, eq, rng, other = kinds(row)
|
||||
prod = math.prod(n for _, n in sel) if sel else 1
|
||||
st["고르기 곱 >64"] += prod > 64
|
||||
st["고르기 곱 >512"] += prod > 512
|
||||
st["기계 목록 있음"] += bool(eq)
|
||||
st["범위 있음"] += bool(rng)
|
||||
st["그 밖 수 입력 있음"] += bool(other)
|
||||
for n, _ in sel:
|
||||
names[n] += 1
|
||||
print(dict(st))
|
||||
print("자주 나오는 고르기 이름", names.most_common(40))
|
||||
if "show" in sys.argv:
|
||||
for c in sorted(ans):
|
||||
row = WHOLE.logic(c)
|
||||
if not row:
|
||||
print(c, "없음")
|
||||
continue
|
||||
sel, eq, rng, other = kinds(row)
|
||||
print(c, row.get("이름"), "| 고르기", sel, "| 기계", eq, "| 범위", rng, "| 그 밖", other[:6])
|
||||
if len(sys.argv) > 2:
|
||||
row = WHOLE.logic(sys.argv[2])
|
||||
print(json.dumps(row.get("입력"), ensure_ascii=False, default=str)[:4000])
|
||||
print(json.dumps(row.get("호표"), ensure_ascii=False, default=str)[:3000])
|
||||
print(json.dumps({k: row.get(k) for k in ("이름", "결과단위", "덧줄", "원문번호")}, ensure_ascii=False, default=str)[:2000])
|
||||
@@ -0,0 +1,39 @@
|
||||
"""K5 들여다보기 — 「지역」 · 「보정작업」 고르기가 품(직종 · 장비 시간 · 자재 수량)을 바꾸는 로직이 있나 · 엔진 속도.
|
||||
C4 run_one(같은 엔진 길)으로 기본값 하나와 값 바꾼 것 하나를 견줌. 읽기만."""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
C4 = HERE.parent.parent / "req226" / "C4"
|
||||
sys.path.insert(0, str(C4))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
import s2_ours_fp as O # noqa: E402
|
||||
|
||||
codes = [x["로직"] for x in O.hm.targets()["로직"]]
|
||||
a, b = int(sys.argv[1]), int(sys.argv[2])
|
||||
which = sys.argv[3] if len(sys.argv) > 3 else "지역"
|
||||
t0, runs, changed, err = time.time(), 0, [], 0
|
||||
for code in codes[a:b]:
|
||||
row = O.WHOLE.logic(code)
|
||||
spec = next((s for s in row.get("입력") or [] if s.get("이름") == which and s.get("고르기")), None)
|
||||
if not spec:
|
||||
continue
|
||||
by_name = {}
|
||||
for h in row.get("호표") or []:
|
||||
by_name.setdefault(h.get("이름"), h)
|
||||
times = O.times_of(row)
|
||||
keys = set()
|
||||
for v in spec["고르기"][:: max(1, len(spec["고르기"]) // 6)]:
|
||||
try:
|
||||
got = O.run_one(code, row, {which: v}, times, by_name)
|
||||
runs += 1
|
||||
except Exception: # noqa: BLE001
|
||||
err += 1
|
||||
continue
|
||||
keys.add(O._key(got))
|
||||
if len(keys) > 1:
|
||||
changed.append(code)
|
||||
print(which, "로직", b - a, "· 돌림", runs, "· 오류", err, "· 품 바뀜", len(changed), changed[:20], f"{time.time() - t0:.1f}s")
|
||||
@@ -0,0 +1,59 @@
|
||||
"""K5 들여다보기 — 표시 갈래 조건 바꿔 보기(다른 점 보임 = 직종 같음 + 조항 같음 / 점수 차) · 틀린 줄 목록."""
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
|
||||
import k5_lib as K
|
||||
import k5_s6_sum34 as M
|
||||
import s3_match as S
|
||||
|
||||
OURC = S.OURC
|
||||
T = M.T
|
||||
|
||||
|
||||
def clause_ok(e, code):
|
||||
our = OURC.get(code)
|
||||
theirs = [list(x) for x in e.get("출처") or []]
|
||||
return bool(our and theirs and S.clause_prefix(theirs, our)[0] >= 0.9)
|
||||
|
||||
|
||||
def disp(r, rule):
|
||||
o = M.order_F(r)
|
||||
b = r["옵션"].get(o[0]) if o else None
|
||||
e = T[r["id"]]
|
||||
exact = [k for k, x in r["옵션"].items() if x["수준"] <= 1]
|
||||
fs = r["지문점수"]
|
||||
gap = fs.get(o[0], 0) - (fs.get(o[1], 0) if len(o) > 1 else 0)
|
||||
if b and b["수준"] <= 1:
|
||||
return "맞음(하나)" if len(exact) == 1 else "맞음(여럿)"
|
||||
if b and b["수준"] == 2:
|
||||
return "거의 맞음"
|
||||
same = b and b.get("직종같음")
|
||||
if rule == "직종" and same:
|
||||
return "다른 점"
|
||||
if rule == "직종+조항" and same and clause_ok(e, o[0]):
|
||||
return "다른 점"
|
||||
if rule == "직종+차" and same and gap > 0.3:
|
||||
return "다른 점"
|
||||
if rule == "직종+(조항 또는 차)" and same and (clause_ok(e, o[0]) or gap > 0.3):
|
||||
return "다른 점"
|
||||
return "후보"
|
||||
|
||||
|
||||
for g, rows in (("사람", [r for r in M.R if r["무리"] == "사람"]), ("새", [r for r in M.R if r["무리"] != "사람"])):
|
||||
for rule in ("직종", "직종+조항", "직종+차", "직종+(조항 또는 차)"):
|
||||
c = Counter()
|
||||
for r in rows:
|
||||
d = disp(r, rule)
|
||||
o = M.order_F(r)
|
||||
c[(d, "줄")] += 1
|
||||
c[(d, "틀림")] += not M.ok_top(o, set(r["정답"]))
|
||||
print(g, rule, {d: f"{c[(d, '줄')]}줄 틀림{c[(d, '틀림')]}" for d in ("맞음(하나)", "맞음(여럿)", "거의 맞음", "다른 점", "후보")})
|
||||
if "list" in sys.argv:
|
||||
for r in M.R:
|
||||
d = disp(r, "직종+(조항 또는 차)")
|
||||
o = M.order_F(r)
|
||||
if d.startswith("맞음") and not M.ok_top(o, set(r["정답"])):
|
||||
print("틀린 맞음", r["id"], T[r["id"]]["이름"], o[0], r["정답"], r["옵션"][o[0]].get("입력"))
|
||||
if d == "다른 점" and not M.ok_top(o, set(r["정답"])):
|
||||
print("틀린 다른 점", r["id"], T[r["id"]]["이름"], o[0], r["정답"])
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
# 338 실험 자료 (요구 225~229 · 브레인 스크래치패드 사본)
|
||||
|
||||
옮겨 쓸 알고리즘의 본보기 — 그대로 돌리는 코드 아님(자료 경로 · 중간 파일은 원래 스크래치패드 기준). 저장소 모듈로 옮길 때 읽기만.
|
||||
|
||||
- `C1/` — 정답 표 `labels.py`(북부청 임도 · 사방 · 사람 정답) · `labels_y.py`(영월) · `out_items.json`(줄 id · 단산 번) · 지표 셈 `s04_eval.py` · STmate 흉내 `s09_stmate.py`
|
||||
- `C4/` — 로직 지문: 엑셀 블록 지문 `s1_excel_fp` · `fp_common` · 우리 로직 지문(M01 엔진 셈) · 순위 · 딱 맞음
|
||||
- `C5/` — 조합 풀이: `c01` 나무 · 절 · 몫 · `c05` combine · cover · is_mixed · 호표 조합 맞음
|
||||
- `C6/` — STmate · 거창 읽기 · 노무 목록 이음 · 자동 정답 · 기억 흉내
|
||||
- `C7/` — 겹쳐 잼 · 섞인 줄 몫 먼저 · 가림 시험
|
||||
- `K5/` — 옵션 거꾸로 찾기 · `targets.json`(새 파일 51 줄 정답)
|
||||
- 설계 = `../2026-10-15_338_일위대가맞춤_설계.md`
|
||||
@@ -0,0 +1,566 @@
|
||||
# 요구 229-4 PA4 — 외부 일위대가 맞춤 설계 (연결된 단가산출 → 우리 로직 + 옵션 → 우리 일위대가)
|
||||
|
||||
재료 = 336 조사 셋(목적다시 · 재검토 · 이름맞춤 검토 + 부록3 · 부록4) · 스크래치패드 실험(`req226/C4 · C5 · C6 · C7` · `req227/K5`) · 저장소 코드 HEAD 8a4598c05(M02 맞춤 코드는 510b36eea 뒤 안 바뀜 · 줄 번호 = 지금 파일) · 이번 읽기 확인(`req229/PA4/p01~p05.py` · 읽기만 · 파일마다 1 초 안). 저장소 · git 쓰기 없음. 파일 자리는 따로 적지 않으면 `M02_MasterTemplete/` 아래.
|
||||
- 실험 자료 = 같은 폴더 `2026-10-15_338_실험자료/`(스크래치패드 사본 · 「338_실험자료/C4/…」 꼴로 적음 · 읽기만).
|
||||
|
||||
## 0. 요약
|
||||
|
||||
1. 사용자 말(229) = 엑셀 호표 줄 → 이어진 단가산출 블록(목록 · 식) → 우리 단가산출 로직 + 옵션 → 우리 일위대가 하나 — 336 실측이 이 길을 받침(옵션만으로 엑셀 품 그대로 58% · 엑셀 덧셈 20% · 원문과 다른 우리 로직 0).
|
||||
2. 「하이퍼링크」 실제 — 북부청 · 거창 = 셀 하이퍼링크 0 · HYPERLINK 식 0 · 비고 칸 「단산N참조」 번호 글이 유일한 이음 / STmate = HYPERLINK 식이 있으나 보이는 글이 줄 코드(「D01352 →」)이고 판에 따라 없음(울진대흥 0) → 이음 열쇠 = 블록 번호 · 줄 코드(O 열 · 다섯 파일 모두).
|
||||
3. 만들 것 = 서버 맞춤 엔진(엑셀 지문 · 우리 지문 캐시 · 순위 · 옵션 거꾸로 · 판정 · 조합) + STmate · 거창 읽기 + 별칭 장부 + 확인 창.
|
||||
4. 판정은 숫자 — 「맞음」 = 1위 로직 + 찾은 옵션이 엑셀 품(직종 품 · 장비 시간 · 자재 수량)을 그대로 냄 · 「다른 점」 = 로직은 같음(직종 묶음 + 인용 조항) · 품만 다름 · 그 밖 「후보」 · 이름은 약한 덤 · 금액 안 씀.
|
||||
5. 들어가는 것 = 사용자가 체크하고 [확인하고 사전조합 만들기]를 누른 줄만 · 그때 별칭 장부에 「엑셀 이름 → 로직 + 옵션 + 배」.
|
||||
6. 셈 = 서버 단독(CLAUDE.md 5장 ③) · 올릴 때 뒤 스레드 · 우리 지문 캐시(마스터 판 열쇠 · 처음 ≈ 2 분 · 다음은 읽기만) · 화면 잠금 없음.
|
||||
7. 창 셋 — 가 = 맞춤 핵심 서버(새 다섯 파일 + `M02_Hopyo_Match.py`) · 나 = 읽기(STmate · 거창 · 블록 글 · 인용 조항 손질) · 다 = 확인 · 별칭 장부 · 만들기 서버 + 화면 전부.
|
||||
8. 완료 기준 — 사람 정답 126 줄 1순위 ≥ 92% · 다섯 안 ≥ 96% · 「맞음」 틀림 ≤ 2 · 확인 없이 들어가는 줄 0 · STmate 6 파일 올라감 · master_data 그대로.
|
||||
|
||||
### 0-1. 브레인 이해 — 맞음 / 다름
|
||||
|
||||
| 브레인 이해 | 판정 | 덧 · 고침 |
|
||||
|---|---|---|
|
||||
| 호표 한 줄 = 하이퍼링크 · 「단산N참조」 · STmate 줄 코드(D)로 단가산출 블록 하나에 이어짐 | 반만 맞음 | 엑셀 파일 속 하이퍼링크는 이음이 아님(1 장) · 이음 = 번호 글 · 줄 코드 · 모든 줄이 블록에 이어지지도 않음 — 기계(hr) · 노임 · 자재 · % · 다른 호표 줄은 블록 없음(북부청 임도 415 줄 중 단산 268 · 자재 78 · 별도계상 64 · 기계 3 · % 1 · 호표 1 / STmate 봉화 216 줄 중 단산 103 · 노임 30 · 호표 22) |
|
||||
| 블록의 목록(직종 · 장비 · 자재 · 수량)과 계산식을 읽음 | 맞음 | 북부청 = 식 글을 풂(맨 앞 단가를 떼고 남은 식 · 514 풂 · 32 못 풂) · STmate = 자원 줄 H 열 품이 곧 품(식 안 풂) · 거창 = 글이 한 줄 여러 칸에 흩어져 이어 붙여야 읽힘 · 지금 코드는 블록에서 출처 · 직종만 읽고 글 · 품을 안 남김 |
|
||||
| 우리 단가산출 로직(+ 옵션 = 입력 칸 값)을 찾음 | 맞음 | 차례 = 로직 먼저(지문 = 직종 · 품 · 장비 · 조항 · 조건 · 이름 덤) → 그 로직에서 옵션 거꾸로(엔진) · 품만으로 로직을 고르면 1순위 60% 로 약함(K5) |
|
||||
| 호표 줄을 모아 우리 일위대가(사전조합 = 로직 + 옵션 + 수량 · 줄 별칭 = 엑셀 이름) 하나로 | 맞음 | 덧 — 블록 하나 = 로직 여럿 가능(철근 가공 + 조립 · 섞인 줄 인력 몫) → 사전조합 줄 여럿(같은 별칭) · 자재 줄 = 우리 자재 · 다른 호표 줄 = 그 호표 사전조합 참조 · % 줄 = 안 넣음(우리 덧줄이 셈) · 노임 · 장비를 바로 적은 호표(STmate 49 · 북부청 5 · 거창 5) = 호표 통째를 로직 하나로 |
|
||||
| 이름은 판정에 거의 안 씀 | 맞음 | 이름 = 순위 덤(무게 0.6 / 약 9.5) · 후보 풀 · 별칭 장부 열쇠(엑셀 이름 → 짝)에만 큼 |
|
||||
| 금액 안 씀 | 맞음 | 식 맨 앞 수(노임 · 장비 단가)는 떼고 셈 · 식을 못 풀 때만 금액 ÷ 단가로 품을 되찾음(「역산」 꼬리표) · 금액 비교는 만든 뒤 「참고」 한 줄 |
|
||||
| 확인한 줄만 들어가고 그때 짝이 별칭으로 남음 | 맞음 | 지금 코드는 「같음」 · 「기억」 · 「호표」 줄과 [로직 추가] · 엑셀 보기 창 길이 확인 없이 들어감 → 없앰(4 장) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 연결 — 호표 줄 → 단가산출 블록
|
||||
|
||||
### 1-1. 실측 (이번 · 읽기 전용)
|
||||
|
||||
| 파일(꼴) | 셀 하이퍼링크 | HYPERLINK 식 | 다른 시트 참조 식 | 실제 이음 |
|
||||
|---|---|---|---|---|
|
||||
| 북부청 임도 · 사방 xls | 0(일위대가 · 단가산출 · 총괄표) | 0 | 0 — 일위대가 시트 식 칸 1 개뿐(값만 든 내보내기) | 비고 「단산N참조」 268 / 268 · 139 / 139 · 「일위N참조」 1 · 2 · 빈 비고 146 · 60 |
|
||||
| 거창 xlsx | 0 | 0 | 0 — 일위대가 식 432 = 줄 안 합(「J5+L5+N5」) | 같은 「단산N참조」 21 · 「일위N참조」 12 |
|
||||
| STmate xlsx(봉화 · 영월 · 울진소광) | 2 씩(머리 칸 STmate 안내 풍선) | 일위대가표 249 · 162 · 134 / 단가산출근거 388 · 456 · 237 | — | HYPERLINK = 일위대가표 Z 열 줄마다 「L00004 →」 「D01352 →」(목록표 행으로 감) · B 열 호표 머리 「BDCOD\|B00004 · POSS\|7 · POSE\|9」 · 단가산출근거 G 열 블록 머리 「BDCOD\|D00001 · POSS · POSE」 · Y 열 자원 → 목록표 |
|
||||
| STmate 줄 코드 칸 | — | — | — | O 열(14) 「D00096_1」 다섯 파일 모두(248 · 161 · 133 · 205 · 39) · Z 열 HYPERLINK 글 넷(울진대흥 0) · M 열(12) 비고는 판마다 다름(「N년개정」 · 「D96」 · 「산근N호표」 · 「대가N」 · 「LN」) |
|
||||
| STmate 단가산출근거 | — | — | — | 블록 머리 G 열 「BDCOD\|D…」 다섯 모두 · 자원 코드 M 열 「L00002_1」 다섯 모두 · A 열 「L00002L」 셋 · 열 수 25(봉화 · 대흥 · 소광) / 45~47(영덕 · 영월) |
|
||||
|
||||
### 1-2. 지금 읽기 코드가 이음을 쓰는 법
|
||||
|
||||
- `M02_Hopyo_Excel._REF :34` = 비고 「단산N참조」 · 「일위N참조」만 → 줄 `참조 {종류, 번}` · 블록 = `M02_Hopyo_Basis.read :84-125` 산출근거 열 머리 「N.이름[단위]」 번호 → `read_book :257-261` 이 줄에 `근거 {출처, 직종, 행}`만 붙임(블록 글 · 품 · 장비 · 조건 안 남김).
|
||||
- 시트 = `Excel._sheets :74-102` 가 「일위대가」 · 「일위대가총괄표」 · 「단가산출」 이름만 읽음 → STmate(일위대가표 · 단가산출근거)는 400 「일위대가 시트 없음」.
|
||||
- 거창 = 번호 이음은 되나 `Basis.read` 가 산출근거 한 열만 읽어 블록 글 · 출처 · 직종 0.
|
||||
|
||||
### 1-3. 하이퍼링크를 쓰면 더 튼튼한가 — 아님
|
||||
|
||||
- 북부청 · 거창 — 쓸 하이퍼링크가 없음. 번호 글이 유일한 이음이고 지금도 268 / 268 · 139 / 139 · 21 / 21 이어짐.
|
||||
- STmate — HYPERLINK 의 보이는 글 = 줄 코드와 같은 글 · 대상 주소는 식(`data_only=False`)으로 읽어야 하고 목록표를 한 번 더 거침 · 판에 따라 빠짐(울진대흥) → 줄 코드 칸보다 약함.
|
||||
- 그래서 정함 — 열쇠 = 블록 번호 · 줄 코드 · 하이퍼링크는 안 읽음 · 대신 번호 글 꼴 · 코드 칸 자리를 넓혀 판 차이에 견딤(아래).
|
||||
|
||||
### 1-4. 정함 — 이음 열쇠 (나 창)
|
||||
|
||||
| 꼴 | 줄 → 블록 | 넓힘 |
|
||||
|---|---|---|
|
||||
| 엑셀꼴(「일위대가」 시트) | 비고 `^(단산\|산근\|단가산출)\s*(\d+)\s*(참조\|호표)?$` → 단산 N · `^(일위\|대가\|일위대가)\s*(\d+)\s*(참조\|호표)?$` → 호표 N(빈칸 무시) | 비고가 비었고 노무비 · 경비 단가가 있는 줄 = 줄 품명 + 단위가 블록 머리 이름 + 단위와 같은 블록이 하나뿐이면 이음 · 꼬리표 「이름으로 이음」 |
|
||||
| STmate | 줄 M~Z 열에서 코드 꼴 `^(?:COD\|)?([A-Z]{1,2}\d{5})(?:_\d+)?(?: →)?$` 첫 것 → D = 단산(번 = 코드 숫자 · 코드 남김) · B = 호표(호표 머리 코드 표로 번) | 열 번호에 안 묶임 → 판(25 · 45~47 열)이 달라도 읽힘 |
|
||||
| 블록 머리 | 엑셀꼴 = 산출근거 열 「N.이름[단위]」 · STmate = G 열 「BDCOD\|D…」 + 단가산출근거목록표(이름 · 규격 · 단위) | — |
|
||||
|
||||
---
|
||||
|
||||
## 2. 맞춤 알고리즘 명세
|
||||
|
||||
### 2-1. 흐름
|
||||
|
||||
```
|
||||
올리기 ─ 읽기(나) ─ HP 파일(호표 · 줄 · 블록 글 · STmate 자원) ─ 저장
|
||||
└─ 뒤 셈(가) Fit.ensure(층, 키) ── 블록마다(같은 블록 · 같은 줄 열쇠는 한 번)
|
||||
① 엑셀 지문(Finger) → ② 순위(지문 점수 + β × 인용 조항 차례 덤) → ③ 옵션 거꾸로(위 다섯 · 엔진)
|
||||
→ ④ 판정(맞음 / 다른 점 / 후보) → ⑤ 조합(절 · 몫 · 섞인 줄 몫 먼저) → 맞춤셈 캐시 파일
|
||||
보기(Match.view) = 맞춤셈 캐시 + 별칭 장부 + 갈래 규칙 → 줄 상태 · 체크 제안 (싸게 · 요청마다)
|
||||
고침(Match.guess) = 고른 로직 · 옵션으로 엔진 한 번 → 우리 품 ↔ 엑셀 품 (저장 안 함)
|
||||
확인(다) = [확인하고 사전조합 만들기] → 서버 재검사 · 우리 엔진으로 셈 · 사전조합 · 별칭 장부
|
||||
```
|
||||
|
||||
### 2-2. ① 엑셀 지문 — 새 `M02_Hopyo_Finger.py` (가 · 약 450 줄)
|
||||
|
||||
옮길 것 = C4 `fp_common.py`(식 풀기 · 장비 갈래 · 조건 낱말 · 닮음 도우미) + `s1_excel_fp.north` · `yeongwol` + C5 `c01_trees.sections` · `sections_stm`(몫) — 한 번 도는 고리에서 지문 · 몫을 함께 냄.
|
||||
|
||||
| 함수 | 입력 → 출력 |
|
||||
|---|---|
|
||||
| `excel(block, line) -> dict` | HP 블록(3-1) + 그 줄(이름 · 규격 · 단위) → 지문(아래) |
|
||||
| `machine(line) -> dict` | 블록 없는 기계(hr) 줄 → 지문 `장비 [{갈래, 규격, 시간 1}]` |
|
||||
| `direct(hopyo) -> dict` | 노임 · 장비 줄을 바로 적은 호표 → 줄 수량 합 지문 + 근거글 출처 |
|
||||
| `parse_rhs(rhs) -> {단가, 품, 밑품, 율, 인자} \| None` | 「273,540 × 3인 ÷ 55㎥ × 2 = 29,840」 → 품 0.10909 |
|
||||
| `mtypes(text)` · `specnums(text)` · `conds(text)` | 장비 갈래 · 규격 수 · 조건 낱말(엑셀 · 우리 같은 함수) |
|
||||
|
||||
지문 꼴:
|
||||
|
||||
```
|
||||
{이름, 규격, 단위,
|
||||
인력: {직종: {품, 밑품, 율: [[값, 메모]], 어떻게: "식" | "역산" | "QTY"}},
|
||||
장비: [{갈래: [..], 규격: [수..], 시간, 밑시간, 율, 절, 글}],
|
||||
자재: [{이름, 품, 밑품}], 율: [[바탕, %]], 계수: {q: 0.5},
|
||||
조건: {축: [낱말]}, 이름조건: {축: [낱말]}, 출처: [[책, 번호]], 옛판: bool,
|
||||
몫: [{제목, 절, 노임: {직종: 품}, 장비: {"굴착기|0.6": 시간}}]}
|
||||
```
|
||||
|
||||
규칙(실험 그대로):
|
||||
- 절 = `^\d+\.` 맨 윗 절 · `^[가-하]\.` 작은 절(STmate `^\d+[.)]` · ①~⑩) · 절 제목 장비 낱말 = 지금 장비.
|
||||
- 직종 줄 「직종 : 단가 × 식 = 금액」(직종 = 우리 인력 이름 · 옛이름 — `Basis.trade_names`) → `parse_rhs` = 맨 앞 수(단가) 떼고 식 셈 · 「× 10%」 · 「(품할증)」 · 「× 2회」 = 율(밑품 = 율 뺀 품) · 바꿔 쓴 식(「= 215,907×4×0.026」)은 앞 조각 율을 이어받음 · 율 메모 없는데 맨 인자가 이름 · 절 머리 몫(「9:1」 · 「(90%)」)과 같으면 「섞음」 율 · 못 풀면 금액 ÷ 단가(「역산」 · 품 되찾기만).
|
||||
- 장비 = 「노무비 · 경비 · 재료비 : 단가 ÷ Q × 몫」 + 지금 장비(노무비 > 경비 > 재료비 차례) · 자재 = 「이름 단가 × 수량 = 금액」 · 「: 별도계상」.
|
||||
- 율 「…의 N%」 · 계수 `q|K|f|E|Cm|L|V|T = 수` · 조건 축(토질 · 메찰 · 형 · 무근/철근 · 난이 · 규모 · 방법 · N회 · N개월 · Type) = 이름 · 규격 · 글 앞 다섯 줄 · ※ · 작업조건 줄 · 옛판 = 글 앞 여섯 줄에 2020 이하 해 · 「임도품셈」 · 「사방품셈」.
|
||||
- STmate = 자원 줄 품(H 열)이 곧 품 · L = 노무비목록표 이름 · X = L 몫(시간) 앞섬(L > S > M) · S 중 장비 낱말 = 장비 · 몫 율 = 절 제목 몫 수가 식 글에 곱으로 있으면.
|
||||
- 몫(조합용) = 절마다 노임 · 장비 벡터 — C5 `sections` 결과와 같아야 함(시험).
|
||||
|
||||
### 2-3. 우리 로직 지문 · 캐시 — 새 `M02_Hopyo_Ours.py` (가 · 약 300 줄)
|
||||
|
||||
옮길 것 = C4 `s2_ours_fp.py`(`static` · `combos` · `run_one` · `fingerprint`) + C4 `s3_match.prep`(섞기 전 변형 · 고를 수 있는 장비).
|
||||
|
||||
| 함수 | 하는 일 |
|
||||
|---|---|
|
||||
| `version() -> str` | 마스터 판 열쇠 = `resources/master_data` 첫 층 *.json(밑줄 뺌) 이름 + `M01_MasterData_Store.version_of`(sha256 16) 묶음의 sha256 앞 16 · 파일 자국(`Store_Cache._stamps`)이 같으면 다시 안 셈 |
|
||||
| `table() -> list[dict] \| None` | 우리 지문 1,438(돈 로직 = `Match.targets()["로직"]`) · 준비 전 None |
|
||||
| `ready() -> bool` · `warm() -> Thread` | 디스크 캐시 있으면 읽기만 · 없으면 뒤 스레드 셈(≈ 110 초 · C4) |
|
||||
| `run(code, given) -> {L, X, M, S, mix} \| None` | 엔진 한 번(`mau.values` 자동값 + 입력 → `m01.cm.mf.run` · `Labor_Doc.run_line` 과 같은 길) · 메모리 캐시(5 만 칸 · ponytail) |
|
||||
|
||||
- 정적 = 호표 인력 → 직종 · 「로직(기계경비 …)」 → 장비 갈래 · 규격(EQ) · 재료 이름 · 덧줄 % · 조항(`Basis.ours`) · 이름 · 단위 · 이름 · 고르기 글 조건 낱말 · 기계경비 로직 표시.
|
||||
- 변형 = 고르기 입력 곱 ≤ 64 모두 · 넘으면 하나씩 · 뺌(지역 · 조종원직종 · 보정작업 · 기종 목록 칸) · 줄마다 단위당 L · X · M · S(결과단위 「원/100㎡」 ÷ 100) · 인력비율 · 기계비율 든 로직 = 섞기 전 변형 덧.
|
||||
- 캐시 자리 = `tmp/m02_hopyo/우리지문_<판>.json`(저장소 `tmp/` = git 밖 · 환경 `AISLO_HOPYO_CACHE` 로 바꿈 · 시험은 임시 폴더) · 쓰기 = `atomic_write_json`.
|
||||
- 시동 = `M02_Labor_Router` 불러올 때 `warm()`(디스크 읽기만 · M01 `store.warm()` 과 같은 자리) · 셈은 처음 필요할 때(올리기 · 보기) 뒤에서.
|
||||
- 원본 = `m01.loaded()` 읽기 · 엔진 부르기만 · `resources/master_data` 쓰기 없음.
|
||||
|
||||
### 2-4. ② 순위 — 새 `M02_Hopyo_Fit.py` (가 · 약 450 줄)
|
||||
|
||||
옮길 것 = C4 `s3_match.score_pair` · `lab_sim` · `mach_sim` · `unit_fit` · `sure` · `fuse` + C7 `k0_lib.finger` · k8(딱 맞음이 같음을 이김).
|
||||
|
||||
- 점수(로직마다 가장 닮은 변형) = 3.0 인력 + 2.0 장비 + 1.0 자재 + 0.3 율 + 0.5 단위 + 0.6 조건 + 1.5 조항 + 0.6 이름 · 금액 없음.
|
||||
- 인력 = 0.35 직종 겹침(보통인부 반 무게) + 0.25 꼴(코사인 · 품 · 밑품 큰 쪽) + 0.40 크기(직종마다 ln 비 3% 안 = 1) · 단위가 다르면 크기 덤 없음(단위 별칭 개소 = 개 = 본 · hr = 시간) · 엑셀에 없는 우리 인력 −0.6 · 장비 −0.4.
|
||||
- 장비 = 갈래 겹침 0.6 + 규격 0.2 + 시간 0.2 · 우리에만 있는 갈래는 깎음.
|
||||
- 조항 = `Basis.clause`(같음 1 · 윗마디 0.9 · 앞 마디 0.5) × 옛판 0.5 · 조건 = 엑셀 조건 낱말이 우리 이름 · 고르기 글에 든 비율 − 이름 축 어긋남(토질 · 메찰 · 형) · 이름 = 정리 이름 글자 두 개 묶음 닮음.
|
||||
- 섞기 = 지문 위 40 ∪ B 차례 위 5 → 점수 + β(1) × B 차례 덤(1 · 0.6 · 0.45 · 0.35 · 0.25) → 위 다섯.
|
||||
- B 차례 = `Match.b_order(line, got)` — 지금 이름 후보 + 근거 후보 합침(`Basis.merge`) + 인용 조항 맞대기 손질(접힘 없음 · 「같음」 근거 검사 · 옛 판 글이면 같음 안 냄) · 씨앗(일위대가조합.json 메모)은 이름 같음과 같은 자리(덤만).
|
||||
- 같음 규칙 = B 1위가 이름 같음 · 씨앗(근거 검사 통과)이면 1위 고정 — 단 지문 「딱 맞음」(1위 직종 묶음 같음 · 품 3% 안 · 2위와 점수 차 0.3 넘음 · 보통인부 하나뿐 줄 아님)이 다른 로직을 가리키면 지문 1위.
|
||||
- 기대(사람 126 · 씨앗 켬) = 1순위 88.9 · 다섯 안 96.8(C4 · 336 재검토 ②) · 씨앗 끔 85.7.
|
||||
|
||||
### 2-5. ③ 옵션 거꾸로 — 새 `M02_Hopyo_Option.py` (가 · 약 400 줄)
|
||||
|
||||
옮길 것 = K5 `k5_lib.py`(`excel_numbers` · `space` · `narrow` · `combos` · `plan` · `sig` · `near` · `ru` · `compare` · `gap` · `csig` · `strong` · `vary`) + `k5_s2_answer.MODES` · `ex_of` + `k5_s5_rank.stage_b`.
|
||||
|
||||
| 함수 | 하는 일 |
|
||||
|---|---|
|
||||
| `space(code, ex)` | 입력 칸마다 후보 — 고르기 = 모두 · 기계(EQ) = 엑셀 규격 수와 맞는 기종(없으면 모두) · 범위 = 두 끝 + 범위 안 엑셀 수 · 표 찾기 칸 = 그 표 줄 값(`master_auto._fits`) · 그 밖 수 = 자동값 + 같은 이름 계수 + 칸 이름 · 단위 붙은 엑셀 수(% 칸 = 0 덧 · 「품」 칸 = 엑셀 품 × 결과단위) · 뺌 = 지역 · 보정작업 · 조종원직종 |
|
||||
| `plan(code, ex, cap=400)` | 곱 ≤ cap 모두(안 닿는 칸 · 같은 품 값 묶음) · 넘으면 엑셀 조건 낱말로 고르기 좁힘 · 그래도 넘으면 기준점 둘에서 하나씩 + 고르기 둘씩 |
|
||||
| `compare(efp, v, tol, rnd, base)` | 직종 묶음 같고 품 허용 안 · 같은 갈래 장비 시간 · 이름 낱말 겹치는 자재 수량 → `{직종, 장비, 자재, 핵심, 덧: ["엑셀만 장비 덤프" …]}` |
|
||||
| `strong(efp, cmp)` | 핵심 + 엑셀 몫을 다 덮음(엑셀만 · 우리만 장비 없음 · 자재 다름 없음 · 보통인부 하나뿐 줄 아님) |
|
||||
| `find(code, efp) -> 옵션 결과` | 조합마다 `Ours.run` → 허용 셋(0.5% · R · 3%) 판정 → 아래 꼴 |
|
||||
|
||||
```
|
||||
옵션 결과 = {수준: 0 | 1 | 2 | 9, # 0.5% · R(0.5% + 엑셀 값 자릿수 반올림 폭 · 값의 4% 안) · 3% · 없음
|
||||
판정: "하나만" | "여럿(같은 품)" | "여럿(다른 품)" | "없음",
|
||||
입력: {움직이는 칸: 값}, 갈리는칸: ["측압", "폼타이규격"],
|
||||
견줌: compare(…, 0.03), 우리품: {인력, 장비, 자재}, 거리: 0.0, 섞기전: bool}
|
||||
```
|
||||
|
||||
- 같은 수준이면 거리(직종 어긋남 + 품 ln 비 + 장비 시간 반 무게) 작은 것 · 품에 안 닿는 칸(물빼기파이프 · 모래 · 자갈상태 · 강도 · 슬럼프) = 자동값 · 엑셀 자재 이름 · 규격 글이 고르기 글과 같으면 그 값.
|
||||
- 입력값 출처(칸마다) = 별칭(장부) > 블록(찾은 값이 자동값 · 규격 읽기와 다름) > 규격(`Match.spec_inputs`) > 자동값.
|
||||
- 기대(K5) = 1위 로직이 맞은 103 줄 중 옵션까지 엑셀 품 그대로 56 · 입력값을 옵션 찾기로만 채우는 줄 46(레미콘 「소량」 → 소량감률 50 · 합판 「4회」 → 유형 보통).
|
||||
|
||||
### 2-6. ④ 판정 (Fit)
|
||||
|
||||
| 상태 | 조건(1위 기준) | 체크 제안 | 글 · 꼬리표 |
|
||||
|---|---|---|---|
|
||||
| 맞음 | 옵션 수준 ≤ 1 · `strong` | 체크 | 「맞음 — GC000198 · 장비사용타설 · 철근구조물 · 소량감률 50」 · 딱 맞는 로직이 둘 넘으면 「맞음 둘」 + 둘째를 후보 맨 앞 · 갈리는 칸이면 「측압 · 폼타이규격 못 가름」 |
|
||||
| 다른 점 | 1위 직종 묶음 = 엑셀 · 엑셀 인용 조항 = 그 로직 조항(`Basis.clause` ≥ 0.9) · 수준 2(3% 안) 포함 | 안 함 | 「특별인부 엑셀 0.354 · 우리 0.370」 · 밑품이 같으면 「섞음 — 엑셀이 × 10%」 · 율 「할증 × 1.2」 · 「엑셀만 장비 덤프」 · 가까운 옵션을 입력값으로 |
|
||||
| 후보 | 그 밖 | 안 함 | 후보 다섯 — 후보마다 그 로직 옵션 판정 꼬리표(위 다섯은 다 옵션 찾기를 돌림) |
|
||||
|
||||
- 기준 = K5 4-1 · 기대(사람 126) = 맞음 58 · 틀림 1 · 다른 점 25 · 틀림 1(직종만 보면 39 · 틀림 16 → 인용 조항까지).
|
||||
- 「거의 맞음」 · 「옛 판」 같은 까닭은 자동으로 안 지음 — 숫자 사실(엑셀 · 우리 품)만 · 까닭은 사용자가 [원문 보기]로.
|
||||
|
||||
### 2-7. ⑤ 조합 — 새 `M02_Hopyo_Combine.py` (가 · 약 300 줄)
|
||||
|
||||
옮길 것 = C5 `c03_vec.sim` · `fit` · `dims` · `index` + `c05_solve.combine` · `cover` · `is_mixed` · `machine_of` · `best_hint` · `Prior` · `pool_for` + C7 `k0_lib.solve`(섞인 줄 1순위 바꿈).
|
||||
|
||||
- 할 때 = 블록 몫 둘 넘음 또는 섞인 줄(제목 · 이름 · 규격에 「N%」 · 「a:b」 + 노임만 든 몫 + 장비만 든 몫).
|
||||
- 우리 벡터 = `Ours` 변형의 L · X(같은 캐시 · 따로 안 셈) · 맞음 = 차원마다 작은/큰 가중 평균(보통인부 0.5 · 장비 있음 0.5 + 시간 0.5 · 크기 다르면 × 0.8 · 굴착기 ↔ 굴착기+집게 × 0.8) · 몫 x ∈ {0.1 … 0.9} × 0.95 · 제목 몫 수와 같은 x 면 +0.1.
|
||||
- 섞인 줄 몫 먼저 = 1위가 블록 합을 35% 못 덮고 몫 · 절 로직이 있으면 1위를 인력 몫 로직으로 바꿈(x = 몫 · 고임돌 「인력 10%」 → GF000211 × 0.1) — 지문 × 조합을 그냥 겹치면 조합 맞음 49.4% 로 깎이던 것을 막는 규칙.
|
||||
- 덧 로직 = 1위가 못 덮는(덮음 < 0.6) 절은 절 1위(맞음 ≥ 0.75) · 아니면 몫마다(≥ 0.6) · 장비만 든 몫 = 우리 일 로직이 맞으면 그것 · 아니면 「기계 시간당 기계경비」(GC000267 · GC000268) 후보 — 체크 안 함 · 꼬리표 「시간 = 엑셀 값(우리 셈 아님)」.
|
||||
- 순위 덤 = 지금 차례 0.3 · 인용 조항 같음 0.3 · 같은 책 0.1 · 후보 풀 = 섞기 위 15 + 드문 직종 · 장비 색인(40 개 이하) + 절 제목 이름 후보 8.
|
||||
- 결과 = 대상 목록 `[{로직, 어디(절 제목), 몫, 맞음}]` → 덧 로직마다 그 몫 지문으로 옵션 거꾸로.
|
||||
- 기대(336 재검토 ③) = 1순위 92.9 · 조합 맞음 75.9(덮을 수 있는 83 호표 · 63).
|
||||
|
||||
### 2-8. 블록 없는 줄 · 호표 단위
|
||||
|
||||
| 줄 | 처리 |
|
||||
|---|---|
|
||||
| 기계(hr) 줄 | `Finger.machine` → 같은 순위 · 옵션(기계 칸 = 엑셀 규격 수 기종) → 「기계 시간당 기계경비」 + 기계 · 펌프차 · 크레인 「맞음 둘」 많음 |
|
||||
| 노임 줄(단위 인 + 우리 직종) | 호표에 노임 · 장비 줄이 있으면 직접 줄 호표 → `Finger.direct` → 같은 순위 · 판정 → 호표 단위 제안 「통째 = GF000225 메붙임(굴착기 + 집게)」(C5 (나) 벡터 + 인용 32 / 34) · 통째로 안 맞으면 노임 줄 「노임 · 그릇 없음」 |
|
||||
| 자재 · 별도계상 | 지금 이름 · 규격 같음(`Match._exact` 자재) — 하나 = 맞음 · 체크 / 그 밖 후보 |
|
||||
| % 줄 | 안 들어감(우리 덧줄이 셈) · 체크 없음 |
|
||||
| 다른 호표(일위N) | 그 호표가 만들 층에 확인돼 있으면 체크 · 아니면 「먼저 확인」 |
|
||||
| 블록 없는 그 밖 줄 | B 차례 후보(이름 · 근거 · 씨앗) → 「후보」 · 꼬리표 「숫자 판정 없음」 |
|
||||
|
||||
### 2-9. 상수 한 곳 (`M02_Hopyo_Fit.py` 머리 · 손잡이)
|
||||
|
||||
`W = {인력 3.0, 장비 2.0, 자재 1.0, 율 0.3, 단위 0.5, 조건 0.6, 조항 1.5, 이름 0.6}` · `BETA = 1.0` · `RB = (1, .6, .45, .35, .25)` · `POOL = 40` · `TOP_OPT = 5` · `CAP = 400` · `TOL = (0.005, "R", 0.03)` · `SURE = (0.03, 0.3)` · `MIXED_CUT = 0.35` · `COMBO = (0.85, 0.75, 0.6)` · `COVER = 0.6` · `SHARES = (0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9)` — 값은 336 · K5 실험 그대로(무게를 한 파일로 다시 맞추지 않음 · 336 재검토 2-5).
|
||||
|
||||
### 2-10. 뒤 셈 · 캐시 · 계산 자리
|
||||
|
||||
| 할 것 | 정함 |
|
||||
|---|---|
|
||||
| `Fit.ensure(folder, key) -> {끝, 한, 모두, 까닭}` | HP 파일 sha256 16 + 마스터 판 → 캐시 파일 있으면 끝 · 없으면 일감 줄(작업 스레드 하나 · `queue.Queue`)에 넣음 · 블록 차례로 셈 · 20 블록마다 캐시 파일 덮음 |
|
||||
| `Fit.result(folder, key) -> dict \| None` | 캐시 읽기(보기 · 미리 셈이 씀) |
|
||||
| 캐시 파일 | `tmp/m02_hopyo/맞춤_<HP16>_<판16>.json`(꼴 3-2) · 층 폴더에 안 둠(마스터 층 `resources/master_template/실무자료` 는 git 안 · 파생 값으로 git 소음) |
|
||||
| 부르는 때 | 올리기 길 저장 뒤 · 보기(없거나 판이 바뀜) · 우리 지문 준비 전이면 「우리 로직 준비 중」 |
|
||||
| 셈 단위 | 블록 + 줄 열쇠(`단산:<번>\|<품명\|규격\|단위>`) 한 번 · 블록 없는 줄 `줄:<열쇠>` · 직접 줄 호표 `호표:<번>` |
|
||||
| 계산 자리 | ③ 서버 단독 · 화면은 보이기만 · 옵션 바꿈 = `guess` 를 뒤에서 · 최신 응답만(요청 차례 번호) · 화면 전체 잠금은 [확인하고 사전조합 만들기]에만 |
|
||||
| 무게 | 우리 지문 처음 ≈ 110 초(C4) · 캐시 읽기 몇 초 · 옵션 찾기 줄당 0.1~26 초(K5 · 엔진 셈 캐시로 줄어듦) → 북부청 임도 고유 52 줄 · 블록 95 = 몇 분 뒤에서(측정해 기록) |
|
||||
| 위험 | 엔진을 뒤 스레드에서 돌림 — 요청 스레드 셈과 겹칠 때 값이 그대로인지 시험 · 안 되면 셈 잠금 하나 · `master_auto._uses` · `_fits`(밑줄 함수) 의존은 시험이 잡음 |
|
||||
| ponytail | 일감 줄 하나 · 서버 여럿이면 같은 파일을 둘이 셈 · 블록 글이 같은 다른 파일도 다시 셈 · 캐시 파일 지우기 없음(작음 · `tmp`) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 자료 꼴 — 지금 꼴과 이어지게
|
||||
|
||||
### 3-1. HP 파일(`{층}/실무자료/HP-######.json`)에 더할 것 — 나 창
|
||||
|
||||
```
|
||||
{…지금 칸(키 · 파일 · 올린때 · 올린이 · 시트 · 호표 · 단산 · 검산),
|
||||
꼴: "엑셀꼴" | "STmate", # 새
|
||||
호표: [{번, 이름, 규격, 단위, 금액, 행, 코드?: "B00004", # 코드 = STmate
|
||||
근거글?: ["건설표준품셈 7-2-2(찰붙임)"], # STmate 빈 코드 줄
|
||||
줄: [{…지금 칸, 갈래: + "노임", # 노임 = 단위 인 + 우리 직종 · STmate L
|
||||
참조: {종류: "단산" | "일위", 번: 96, 코드?: "D00096"},
|
||||
이음?: "이름"}]}], # 이름으로 이은 줄만
|
||||
단산: [{번, 이름, 단위, 행, # 지금
|
||||
코드?, 규격?, 출처: [[책, 번호]], 직종: [이름], # 새(지금은 줄 근거에만)
|
||||
글: ["1. 현장가공", " 철근공 : 273,540 × …", …], # 머리 뺀 몸 줄 · 200 줄 · 줄마다 300 자 안
|
||||
자원?: [{갈래: "L"|"X"|"M"|"S", 코드, 이름, 규격, 품, 절}]}]} # STmate
|
||||
```
|
||||
|
||||
- 줄 `근거 {출처, 직종, 행}`은 그대로(지금 화면 · `Basis.candidates` 가 씀).
|
||||
- 옛 HP 파일(글 없음) = 숫자 판정 없음 → 「다시 올리면 숫자 판정」 꼬리표(지금 격자 없음과 같은 길).
|
||||
|
||||
### 3-2. 맞춤셈 캐시 — 가 창 (`tmp/m02_hopyo/맞춤_<HP16>_<판16>.json`)
|
||||
|
||||
```
|
||||
{판, 지문판: 1, 셈: [한, 모두], 끝: bool,
|
||||
블록: {"단산:12|레미콘타설(장비사용)|철근구조물-소량 타설|㎥": {
|
||||
지문: {인력, 장비, 자재, 출처, 조건, 옛판}, # 화면 「엑셀 품」
|
||||
순위: [{로직, 점수, 채널: {인력, 장비, 조항 …}, B: 0}], # 위 다섯
|
||||
옵션: {"GC000198": 옵션 결과(2-5), …}, # 위 다섯 · 조합 덧 로직
|
||||
조합: [{로직, 어디, 몫, 맞음}] | null,
|
||||
판정: {상태: "맞음" | "다른 점" | "후보", 글, 꼬리표: [..]}}}}
|
||||
```
|
||||
|
||||
### 3-3. 맞춤 응답 — 줄 하나 (`GET hopyo/{키}/{번}` · 가 → 다)
|
||||
|
||||
지금 `맞춤 {열쇠, 상태, 대상, 입력값, 못읽음, 환산, 들어감, 후보}`(Api `HopyoMatch :91-100`) → 아래로 바꿈(`들어감` 없앰 → `체크` · `대상` = 목록).
|
||||
|
||||
```
|
||||
맞춤: {열쇠: "품명|규격|단위",
|
||||
상태: "맞음" | "별칭" | "다른 점" | "후보" | "없음" | "뺌" | "호표" | "%" | "노임",
|
||||
체크: bool, # 처음 체크 제안(확인 대기) — 들어감 아님
|
||||
대상: [{로직? | 자재? | 호표?, 이름, 단위,
|
||||
입력값: {칸: 값}, 못읽음: [칸], 출처: {칸: "별칭" | "블록" | "규격" | "자동값"},
|
||||
환산: {배, 갈래}, 몫: 0.1 | null, 배: 0.1 | null, # 배 = 환산 × 몫 · 우리 수량 = 엑셀 수량 × 배
|
||||
어디?: "1. 현장가공"}],
|
||||
후보: [{로직 | 자재, 이름, 단위, 점수, 판정: "맞음" | "다른 점" | "후보", 글, 근거}], # 다섯
|
||||
글: "맞음 — GC000198 · …", 꼬리표: ["옵션 — 블록에서", "근거 건설 6-1-1"],
|
||||
품: {엑셀: {인력: {직종: 품}, 장비: [{갈래, 시간}], 자재: [{이름, 수량}]},
|
||||
우리: {…같은 꼴}, 같음: {"콘크리트공": true, "굴착기": true}},
|
||||
갈리는칸?: ["측압", "폼타이규격"],
|
||||
별칭?: {층, 주인, 때, 누가, 처음파일, 규격다름: bool, 근거다름: bool},
|
||||
뺌?: "우리에 없음" | "안 씀"}
|
||||
```
|
||||
|
||||
- 호표: `{…지금, 맞춤: [체크 제안 수, 줄 수], 통째?: {대상: [..], 글, 체크}, 셈: {끝, 한, 모두, 까닭?}, 만든?: {키, 확인: bool}}` · 파일 보기 `맞춤` 합도 같은 뜻.
|
||||
- 만들 층 = 길 덧 물음 `?to=<층>&to_owner=`(없으면 파일 층) — 호표 참조 줄 체크 · `만든` 은 그 층을 봄([로직 추가] 모달 = 위 층 파일 · 문서 층에 만듦).
|
||||
- 상태 정하는 차례(`Match.match_line` · 가) = ① % ② 호표 참조 ③ 노임 ④ 장부 「우리에 없음 · 안 씀」 → 뺌 ⑤ 장부 짝 → 별칭 ⑥ 맞춤셈 판정 ⑦ 셈 전 · 숫자 없음 → B 후보(「셈 중」 · 「숫자 판정 없음」 꼬리표).
|
||||
- 별칭 체크 = 짝이 있고 다음이 다 아닐 때 — 규격 다름(품명 \| 단위 느슨 열쇠로 찾음) · 근거 다름(엑셀 인용 조항과 짝 로직 조항이 `Basis.clause` < 0.9) · 숫자 판정 「맞음」이 다른 로직.
|
||||
|
||||
### 3-4. 미리 셈 — `POST hopyo/{키}/{번}/guess` (가)
|
||||
|
||||
- 몸 `{차례, 대상: [{로직 | 자재, 입력값}]}` → `{대상: [(3-3 대상 꼴)], 품: {엑셀, 우리, 같음}, 글}` · 저장 안 함 · 엔진 한 번(ms).
|
||||
- 지금 몸 `{차례, 대상: {로직}}`(하나)은 목록으로 바꿈.
|
||||
|
||||
### 3-5. 별칭 장부 한 줄 — 다 창 (`{층}/실무자료/맞춤.json` 키움)
|
||||
|
||||
```
|
||||
{줄: {"<품명|규격|단위 정규화>": {
|
||||
원래: {품명, 규격, 단위},
|
||||
갈래: "짝" | "우리에 없음" | "안 씀",
|
||||
짝: [{로직 | 자재 | (호표 통째 = 로직), 입력값: {}, 배: 1, 몫: null}],
|
||||
누가: {id, 이름}, 때: "2026-10-15T23:10:00", 처음파일: "…xls", 엑셀조항: [[책, 번호]],
|
||||
판: "<마스터 판16>"},
|
||||
"호표|<이름|규격|단위>": {…직접 줄 호표 통째}}}
|
||||
```
|
||||
|
||||
- 열쇠 = 지금 `Match.line_key :68-76` 그대로 · 찾을 때만 품명 \| 단위 느슨 색인(규격 다름 · 체크 안 함).
|
||||
- 옛 줄 `{로직 | 자재 | 빼기, 입력값, 환산, 확정}` = 읽을 때 `{갈래 짝, 짝 [{…, 배: 환산}]}` · 빼기 = 「안 씀」으로 바꿔 읽음(파일은 다음 쓰기 때 새 꼴).
|
||||
- 쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장] 때만 · 확인한 줄은 모두(이름 같음이라도 — 옵션 · 배를 남기려고) · 같은 열쇠는 덮음 · 처음파일은 있던 값 둠.
|
||||
- 층 = 만드는 층에 씀 · 읽는 차례 개인 → 회사 → 마스터(`Match.chain :333-341`) · 아래 층이 이김.
|
||||
|
||||
### 3-6. [확인하고 사전조합 만들기] 요청 — `POST /layers/{층}/hopyo-build` (다)
|
||||
|
||||
```
|
||||
{층, 주인, 키, 번, 새로: false, # 지금 BuildBody 그대로 + 아래
|
||||
줄: [{차례: 3, 대상: [{로직: "GC000198", 입력값: {…}, 배: 1, 몫: null}]},
|
||||
{차례: 4, 대상: [{로직: "GC000206", 입력값: {}, 배: 1}, {로직: "GC000207", 입력값: {}, 배: 1}]},
|
||||
{차례: 5, 뺌: "우리에 없음"},
|
||||
{차례: 6, 대상: [{자재: "PM000123", 배: 1}]},
|
||||
{차례: 7, 대상: [{호표: 12, 배: 1}]}],
|
||||
통째?: {대상: [{로직, 입력값, 배: 1}]}} # 직접 줄 호표
|
||||
→ {참조, 이름, 다시씀, 빠진줄: [{차례, 이름, 규격, 수량, 단위, 금액, 까닭}],
|
||||
비교: {엑셀, 우리, 차이, 까닭}, # 참고 한 줄(판정 아님)
|
||||
장부: {씀: n}}
|
||||
```
|
||||
|
||||
- 서버 재검사(브라우저 값을 받아 적지 않음 · pydantic `extra="forbid"` — 금액 · 품 칸은 400) — 차례가 그 호표 안 · 대상 하나마다 로직 · 자재 · 호표 중 하나 · 로직 = 돈 로직 · 입력값 이름 = 그 로직 입력 · 배 0 넘고 유한 · 몫 (0, 1] · 자재 = M01 자재품목 · 호표 = 만들 층에 확인된 사전조합이 있음(없으면 409 「제N호표 먼저 확인」) · 줄 500 개 안.
|
||||
- 빠진 줄 = 몸에 없는 줄(「확인 안 함」) · 뺌(「뺌 — 우리에 없음」) · % 줄(「% 줄 — 우리 로직 덧줄이 셈」).
|
||||
- `줄` 이 없거나 빈 몸 = 400 「확인한 줄이 없음」(옛 「들어감」 길 없앰).
|
||||
- 같은 본뜬 사전조합이 있으면 덮음(사용자가 확인한 내용이 앞섬) · 응답 `다시씀` true · 화면 글 「있던 SC-… 를 확인한 내용으로 덮음」.
|
||||
|
||||
### 3-7. 사전조합 줄 — 로직 여럿 · 같은 엑셀 이름 (다)
|
||||
|
||||
```
|
||||
{이름: "철근가공조립(현장) Type-Ⅰ", 단위: "ton", 구분: "실무자료", 상세구분: "<파일>",
|
||||
담은로직: [
|
||||
{로직: "GC000206", 입력값: {…}, 별칭: "철근가공조립(현장)", 메모: "실무 1ton × 1 · Type-Ⅰ", 수량: {손: 1}},
|
||||
{로직: "GC000207", 입력값: {…}, 별칭: "철근가공조립(현장)", 메모: "실무 1ton × 1 · Type-Ⅰ", 수량: {손: 1}},
|
||||
{로직: "GF000211", 입력값: {…}, 별칭: "고임돌채집및운반", 메모: "실무 0.5㎥ × 0.1(인력 10%)", 수량: {손: 0.05}},
|
||||
{참조: {층, 주인, 키}, 별칭: "…", 메모, 수량: {손}}],
|
||||
자재: [{코드, 수량: {손}, 별도계상}],
|
||||
본뜬: {층: "실무자료", 주인: null, 키: "HP-000001 제3호표", 파일, 확인: {누가: {id, 이름}, 때}}} # 확인 = 새
|
||||
```
|
||||
|
||||
- 같은 로직 둘째(줄 사이 · 한 줄 안) = 지금처럼 한 줄 사전조합으로 감싸 참조(`Labor_Doc.clean` 「같은 로직 두 번 금지」 · `Build.build :151-161`).
|
||||
- `본뜬.확인` = 「이 호표 확인함」 기록 — 확인 저장소 따로 없음 · 옛 실무자료 사전조합(확인 없음)은 [로직 추가]에서 확인 창으로.
|
||||
- 수량 손 = 엑셀 수량 × 배 · 값은 우리 엔진(`M02_Labor_Ref.Expand`)이 셈.
|
||||
|
||||
### 3-8. 길 표 (`M02_Labor_Router.py` · 다)
|
||||
|
||||
| 길 | 지금 | 바꿈 |
|
||||
|---|---|---|
|
||||
| `POST /layers/{층}/hopyo` | 올리기 | + STmate · 저장 뒤 `Fit.ensure` · DRM 글 |
|
||||
| `GET /layers/{층}/hopyo/{키}` | 호표 맞춤 n/m | + `셈` · n = 체크 제안 수 |
|
||||
| `GET /layers/{층}/hopyo/{키}/{번}` | 줄 맞춤 | 3-3 꼴 · `?to=&to_owner=` 만들 층 |
|
||||
| `POST …/{번}/guess` | 대상 하나 미리 채움 | 3-4 꼴(대상 목록 · 품 비교) |
|
||||
| `POST /layers/{층}/hopyo-memory` | 줄마다 즉시 확정 | **없앰** |
|
||||
| `POST /layers/{층}/hopyo-build` | 들어감 줄로 만듦 | 3-6 몸 · 확인 줄만 · 장부 씀 |
|
||||
| `GET /layers/{층}/hopyo-picks` | 호표마다 n/m | + `확인: {키} \| null`(만들 층에 확인된 사전조합) |
|
||||
| `GET /layers/{층}/hopyo-ledger` | — | 새 · 이 층 + 위 층 줄 `{열쇠, …(3-5), 층, 주인, 고침: bool}` |
|
||||
| `PUT /layers/{층}/hopyo-ledger` | — | 새 · `{줄: {열쇠: 값 \| null}}` · 이 층 쓰기만 · 짝 재검사 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 화면 흐름 (다 창)
|
||||
|
||||
### 4-1. 확인 창 — 새 `M02_MasterTemplete_Hopyo_Check.ts` (본 칸 · 모달 둘 다)
|
||||
|
||||
- 자리 — 실무자료 본 화면 호표 누름(지금 `View.ts` 호표 하나 화면을 바꿔 끼움) · [로직 추가] 「실무 호표」 모달 · 엑셀 보기 창 펼침에서 [확인 창 열기].
|
||||
- 머리 — 호표 이름 · 규격 · 단위 · 「제안 n / 줄 m」 · 셈 중이면 「숫자 판정 셈 중 한/모두」(3 초마다 다시 읽음 · 끝나면 멈춤 · 체크는 사용자가 안 만진 줄만 새 제안으로) · 통째 제안(직접 줄 호표)이면 맨 위 한 줄.
|
||||
- 줄 칸 — [✓] · 갈래 · 엑셀 품명 · 규격 · 수량 · 단위 · 상태(색 — 맞음 초록 · 별칭 파랑 · 다른 점 주황 · 후보 회색) · 우리 로직(이름 · 키 · 몫 · 배 — 여럿이면 칸 안 여러 줄) · 옵션 요약(칸=값 · 출처 꼬리표 · 「못 가름」) · 품 비교(직종 엑셀 → 우리 · 같음 ✓ / 다름 ≠) · 단추.
|
||||
- 줄 단추 — [고르기](고르기 창) · [옵션](`Labor_Input.ts:53 openLaborInput` 다시 씀 → `guess` 로 품 비교 다시) · [원문 보기](엑셀 인용 조항 · 우리 로직 출처 두 갈래 → `/m02/logic-lib/source` · `Router_Combos.py:117`) · [엑셀에서 보기](엑셀 보기 창 · 그 단가산출 블록 행) · [뺌](까닭 「우리에 없음」 · 「안 씀」).
|
||||
- 호표 단추 — [맞음 · 별칭 모두 체크] · **[확인하고 사전조합 만들기]**(체크 한 줄 넘을 때 · 누르면 「넣을 줄 n · 빠질 줄 m(까닭별)」 확인 글 → 서버 → 결과 「SC-… 만듦 / 덮음 · 빠진 줄 표 · 값 비교(참고)」 · 화면 잠금은 이때만).
|
||||
- 캐시 — sessionStorage `m02-hopyo-check:{층}:{주인}:{키}:{번}` = `{체크: {차례: bool}, 대상: {차례: [..]}, 뺌: {차례: 까닭}}` · 조작 즉시 · 만들기 성공하면 지움 · 다시 열면 서버 제안 위에 덮어 보임 · 자동 저장 없음.
|
||||
- 쓰기 단추 = 만들 층 쓰기 권한만(지금 `canWrite`) · 파일 층은 읽기만 있으면 됨 → 위 층 파일도 내 층으로 확인.
|
||||
|
||||
### 4-2. 고르기 창 — 새 `M02_MasterTemplete_Hopyo_Choose.ts` (`View.ts choose` 를 옮김)
|
||||
|
||||
- 위 = 엑셀 블록 품(직종 · 장비 · 자재) · 인용 조항 · [엑셀에서 보기].
|
||||
- 후보 다섯 = 이름 · 키 · 책 · 조항 · 판정 꼬리표(맞음 / 다른 점 / 후보) · 우리 품 · 「근거 맞음」 초록(지금 `basisOk`) · 금액 비 없음.
|
||||
- 고름 = 한 로직 · 또는 [로직 더함]으로 둘 넘게(조합 · 몫 칸) · 고른 뒤 옵션은 서버 `guess` 가 채움.
|
||||
- [다른 로직 찾기] = `openLogicPicker`(`M02_MasterTemplete_Labor_Pick.ts:83`)에 찾기 글 미리 넣음 덧(선택 칸 `query` · 엑셀 인용 조항 번호 → 없으면 품명 첫 뜻 낱말 · M01 찾기는 원문번호로도 걸림) · 다른 부르는 곳은 그대로.
|
||||
|
||||
### 4-3. 엑셀 보기 창 · [로직 추가] · 좌측 판
|
||||
|
||||
- 엑셀 보기 창(`Hopyo_Sheet.ts importPicked :229-278`) — [고른 n개 불러오기] → **[고른 n개 확인하고 만들기]** = ① 고른 호표마다 서버 보기 → 펼침 표(호표 · 넣을 줄 = 체크 제안 · 빠질 줄 · 셈 중 · [확인 창 열기]) ② [이대로 n개 만들기] = 하위 호표부터 차례로 3-6 요청(몸 = 그 호표 체크 줄) · 펼쳐 보이고 한 번 누름 = 사용자 확인(목적다시 7-3).
|
||||
- [로직 추가] 「실무 호표」(`Hopyo_Pick.ts pickHopyo :82-94`) — `확인` 키가 있으면 그 사전조합을 바로 참조(「확인한 사전조합 SC-…」) · 없으면 확인 창 모달 → 만들기 → `lib.pick` 참조 줄(별칭 = 호표 품명).
|
||||
- 좌측 판(`Hopyo_Side.ts`) — 파일 목록 아래 **[별칭 장부]** 한 줄 → 본 칸 장부 화면.
|
||||
|
||||
### 4-4. 별칭 장부 화면 — 새 `M02_MasterTemplete_Hopyo_Ledger.ts`
|
||||
|
||||
- 표 — 엑셀 품명 · 규격 · 단위 | 우리 로직(여럿) · 옵션 · 배 · 몫 | 누가 · 때 · 처음 파일 | 층(내 층 · 회사 · 마스터 — 위 층 회색 읽기만).
|
||||
- 할 일 — 찾기 · 갈래 거름(짝 · 우리에 없음 · 안 씀) · [고치기](고르기 창 · 옵션 창 · 배) · [지우기] · 위 층 줄 [내 층으로 고쳐 씀] · 고친 것은 sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`.
|
||||
- 안 둠 — 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기 단추.
|
||||
|
||||
### 4-5. 없앨 것 (지금 즉시 확정 · 확인 없이 들어감)
|
||||
|
||||
| 자리 | 없앰 |
|
||||
|---|---|
|
||||
| `View.ts` 줄 [확정] `:393-402` | 즉시 서버 쓰기 → 체크(캐시) |
|
||||
| `View.ts` [같음 모두 확정] `:279-282` · [근거 맞음 모두 확정] `:283-291 · 525-541` | → [맞음 · 별칭 모두 체크] |
|
||||
| `View.ts` [사전조합 만들기] `:292-294` | → [확인하고 사전조합 만들기](체크 줄 몸) |
|
||||
| `Hopyo_Sheet.ts importPicked` | 바로 만들기 → 펼침 뒤 한 번 |
|
||||
| `Hopyo_Pick.ts pickHopyo` | 바로 만들기 → 확인된 것 참조 / 확인 창 |
|
||||
| `Api.ts saveHopyoMemory` · 길 `hopyo-memory` | 없앰 |
|
||||
| 서버 `Match.match_line` `들어감` · `Build.build` 의 `들어감` 줄 고름 | `체크` 제안 · 몸 줄만 |
|
||||
|
||||
- 파일 크기 — `View.ts` 614 줄 → 확인 창 · 고르기 창을 새 파일로 빼 400 줄 안 · 새 파일 모두 700 줄 안 · 색 = 공용 토큰.
|
||||
|
||||
---
|
||||
|
||||
## 5. 읽기 넓힘 (나 창)
|
||||
|
||||
### 5-1. STmate — 새 `M02_Hopyo_Stmate.py` (약 350 줄 · C6 `s02_read.read_stmate` + C5 `c01_trees.read_stmate` · `_d_blocks` · `sections_stm` 한 벌)
|
||||
|
||||
| 할 것 | 정함 |
|
||||
|---|---|
|
||||
| 가름 | 시트 이름에 「일위대가표」 · 「단가산출근거」 둘 다 → STmate · `Excel._sheets` 가 이름 목록(openpyxl `sheetnames`)을 먼저 보고 STmate 시트 + 목록표(노무비 · 중기 · 재료비 · 경비 · 단가산출근거목록표 — 있으면)를 읽음 |
|
||||
| 호표 | A 열 「제N호표」 줄 → 그 줄 B~Z 열 「BDCOD\|B…」 = 호표 코드 · 다음 줄 A 이름 · B 규격 · D 단위 |
|
||||
| 줄 | A 이름(빈 · 「합계」 뺌) · B 규격 · C 수량 · D 단위 · 단가 E 계 · G 노무비 · I 재료비 · K 경비(금액 = 다음 열) · M 열 비고 글 · 코드 = 1-4 꼴 첫 것 · T · C 꼴(계 · 소계) 뺌 |
|
||||
| 갈래 | D 단가산출(참조 단산) · B 호표(참조 일위 · 번 = 호표 코드 표) · L 노임 · X 기계 · M 자재(비고 「별산」 · 규격 「별도계상」 · 값 없음 = 별도계상) · S · W 별도계상 · PR · 단위 % = % · 빈 코드 = 근거글(호표 `근거글`) · 그 밖 기타 |
|
||||
| 블록 | 단가산출근거 G 열 「BDCOD\|D…」 머리 → 다음 머리 · 「총계」 앞까지 · 글 = B 열 · 자원 = M 열 「X00000_n」(없으면 A 열 「X00000L」 · G 열 「&X00000L&」) · 품 = H 열 · 이름 · 규격 = X 열 「이름 / 규격」(비면 목록표) · 머리 이름 · 규격 · 단위 = 단가산출근거목록표 · 행 = [머리 행, 끝 행] |
|
||||
| 이음 | 노임 L → 노무비목록표 이름 → 우리 직종(`Basis.trade_names` · 어긋남 0 · C6) · X → 중기목록표 · M → 재료비목록표 · S → 경비목록표 |
|
||||
| 출처 · 직종 | 블록 글 + 호표 근거글 → `Basis.sources` · 직종 = 자원 L 이름 |
|
||||
| 검산 | 안 함 — `검산 {어긋남: [], 총괄표: 0}` · 꼴 「STmate」 |
|
||||
| 결과 | 3-1 꼴(엑셀꼴과 같은 HP) — 맞춤 · 만들기 · 엑셀 보기 창이 꼴을 안 가림 |
|
||||
|
||||
- 기대 = 6 파일 올라감(봉화 호표 45 · 영월 23 · 울진대흥 설계 34 · 착공(설계와 같음) · 영덕 9 · 울진소광 25) · 영월 근거 직종 5 → 33 / 36(C4).
|
||||
|
||||
### 5-2. 거창 꼴 — `M02_Hopyo_Basis.read :84-125` 고침
|
||||
|
||||
- 블록 몸 줄 글 = 산출근거 열 글 · 그 열이 비고 같은 줄 다른 칸에 글이 있으면 산출근거 열 오른쪽 ~ 돈 머리 첫 열 앞(돈 머리 없으면 140 열) 칸을 빈칸으로 이어 붙임(C5 `_row_text` · C6 흩어진 칸) — 북부청은 산출근거 열에 글이 다 있어 그대로.
|
||||
- 블록에 `글` 저장(3-1) · 기대 = 거창 단산 21 줄 출처 17 · 직종 21 · 몫 21(C5).
|
||||
|
||||
### 5-3. 그 밖 읽기 손질
|
||||
|
||||
| 파일 | 할 것 |
|
||||
|---|---|
|
||||
| `M02_Hopyo_Excel.py` | `_REF` 넓힘(1-4) · 이름으로 이음 · `kind_of :132-149` 노임(단위 인 + 우리 직종) · `read_book` 블록 `글` · `꼴` · DRM(파일 머리 「DRMONE」 · PK · OLE 아님) → 400 「문서 보안(DRM)으로 잠긴 파일 — 보안을 푼 사본으로 저장해 올림」 |
|
||||
| `M02_Hopyo_Basis.py` (B · 인용 조항 맞대기 손질) | `_clause :166-176` → 공개 `clause` · 윗마디 0.9(엑셀 「6-2」 ↔ 우리 「6-2-2」 · 「6-2-3」) · `sources :60-71` 조항 오타(「13-114」 → 「13-11-4」) |
|
||||
| `M02_Hopyo_Grid.py` | `SHEETS :22` 에 「일위대가표」 · 「단가산출근거」 · 격자 호표 · 단산 행 = HP 행 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 창 나눔 · 차례
|
||||
|
||||
### 6-1. 파일 경계
|
||||
|
||||
| 파일 | 창 | 새 / 고침 | 몫 |
|
||||
|---|---|---|---|
|
||||
| `M02_Hopyo_Finger.py` | 가 | 새 | 엑셀 지문 · 식 풀기 · 몫 · 기계 줄 · 직접 줄 호표 |
|
||||
| `M02_Hopyo_Ours.py` | 가 | 새 | 우리 지문 · 판 열쇠 · 캐시 · 데움 · 엔진 셈 캐시 |
|
||||
| `M02_Hopyo_Option.py` | 가 | 새 | 옵션 공간 · 거꾸로 찾기 · 견줌 |
|
||||
| `M02_Hopyo_Fit.py` | 가 | 새 | 순위 · 판정 · 블록 셈 · 뒤 셈 · 맞춤셈 캐시 |
|
||||
| `M02_Hopyo_Combine.py` | 가 | 새 | 조합 풀이 · 섞인 줄 몫 먼저 |
|
||||
| `M02_Hopyo_Match.py` | 가 | 고침 | `match_line` 새 상태 · 체크 제안 · `b_order`(접힘 없음 · 같음 근거 검사) · `view` · `guess` 꼴 · 장부 부르기 · `confirm` 없앰 |
|
||||
| `M02_Hopyo_Stmate.py` | 나 | 새 | STmate 읽기 |
|
||||
| `M02_Hopyo_Excel.py` · `M02_Hopyo_Grid.py` | 나 | 고침 | 5-3 |
|
||||
| `M02_Hopyo_Basis.py` | 나 | 고침 | 블록 글 · 거창 이음 · `clause` · 오타 |
|
||||
| `M02_Hopyo_Ledger.py` | 다 | 새 | 장부 읽기(옛 꼴 바꿔 읽음) · 찾기(열쇠 · 느슨) · 쓰기 · 층 · 재검사 |
|
||||
| `M02_Hopyo_Build.py` | 다 | 고침 | 3-6 몸 · 로직 여럿 · 배 · 몫 · 통째 · 장부 씀 · `본뜬.확인` · `confirmed(folder)` |
|
||||
| `M02_Labor_Router.py` | 다 | 고침 | 3-8 길 · 올리기 뒤 `Fit.ensure` · 시동 `Ours.warm()` · picks 에 `확인` |
|
||||
| TS 새 `Hopyo_Check` · `Hopyo_Choose` · `Hopyo_Ledger` / 고침 `Hopyo_View` · `Hopyo_Sheet` · `Hopyo_Pick` · `Hopyo_Api` · `Hopyo_Side` · `Hopyo.css` · `Labor_Pick`(`query` 선택 칸만) | 다 | | 4 장 |
|
||||
|
||||
- 같은 파일은 한 창만 — `Match.py` = 가(B 의 Match 몫 포함) · `Basis.py` · `Excel.py` = 나 · `Labor_Router.py` · `Build.py` = 다.
|
||||
- 한 창이 남의 함수를 불러야 하면 약속(6-2)으로만 — 남의 파일 고침 없음.
|
||||
- 공용 파일(`main.py` · `ui_template/`) 안 고침 · `M02_MasterTemplete_Store` · `M02_Labor_Ref` · `M02_Labor_Doc` · `M02_Hopyo_Store` · M01 · `resources/master_data` 안 고침(HP 덧칸은 `read_book` 결과에 실려 `Store.save` 가 그대로 씀).
|
||||
|
||||
### 6-2. 서로 약속 (자료 꼴은 3 장)
|
||||
|
||||
| 약속 | 낸 창 → 쓰는 창 | 꼴 |
|
||||
|---|---|---|
|
||||
| HP 블록 · 줄 꼴 | 나 → 가 · 다 | 3-1(`단산[].글` · `자원` · `출처` · `직종` · 줄 `참조` · `갈래` 노임) |
|
||||
| `Basis.clause(theirs, our) -> (점수, 조항 글, 꼬리표)` · `Basis.sources` 오타 | 나 → 가 | 윗마디 0.9 · 같음 1 · 앞 마디 0.5 |
|
||||
| `Fit.ensure(folder, key)` · `Fit.result(folder, key)` · `Ours.warm()` | 가 → 다(길) | 2-10 |
|
||||
| `Match.view` · `Match.guess` 응답 | 가 → 다(화면) | 3-3 · 3-4 |
|
||||
| `Ledger.recall(line, folder) -> 장부 줄 \| None`(+ 층 · 규격다름) · `Ledger.key(line)` | 다 → 가 | 3-5 |
|
||||
| `Build.confirmed(folder) -> {본뜬 키: SC 키}` | 다 → 가(호표 참조 줄 체크 · `만든`) · 다(picks) | 3-7 |
|
||||
|
||||
### 6-3. 차례
|
||||
|
||||
| 단계 | 가 | 나 | 다 |
|
||||
|---|---|---|---|
|
||||
| 1(나란히) | Finger → Ours → Option → Fit → Combine · 시험은 북부청 실물(나 1a 전엔 시험 도우미가 단가산출 시트 글을 직접 읽음) | 1a 블록 글 HP · `clause` · 오타(작음 · 먼저 push · 가 바탕) → 1b STmate · 거창 · Grid · DRM | 1a `Ledger.py` 뼈대(recall · key) 먼저 push → Build 새 몸 · 장부 · 길 · 화면(3-3 꼴 대역으로) |
|
||||
| 2 | `match_line` 새 상태 · 뒤 셈 · view · guess 꼴 → push | STmate 실물 끝까지(올림 → 셈 → 상태) · 영월 정답 34 줄 | 화면을 실물 응답에 붙임 · [로직 추가] · 엑셀 보기 창 · 장부 화면 · 옛 길 · 단추 없앰 |
|
||||
| 3(브레인) | 합치기 · 대상 시험 · 골든 · 브라우저 검증(7-4) · 아티팩트 | | |
|
||||
|
||||
- 가 · 다가 가장 큼(새 파일 다섯 · 화면 셋) · 나는 1b 끝나면 가의 골든 시험 · 다의 화면 시험 거듦(브레인 배정).
|
||||
- 손에 걸리는 곳 — 다의 화면은 가의 3-3 꼴이 굳어야 실물로 붙음 → 가는 1 단계 끝에 응답 꼴 대역 JSON(`resources/tester/fixtures/m02_hopyo_view_sample.json`)을 먼저 올림.
|
||||
|
||||
---
|
||||
|
||||
## 7. 시험 · 완료 기준
|
||||
|
||||
### 7-1. 시험 파일
|
||||
|
||||
| 시험 | 창 | 무엇 |
|
||||
|---|---|---|
|
||||
| `test_m02_hopyo_finger.py`(새) | 가 | 식 풀기 일곱 꼴(「273,540 × 3인 ÷ 55㎥ × 2」 → 0.10909 · 「노무비 : 59,020 ÷ 23.1 × 90%」 → 시간 0.03896 · 율 0.9 · 「0.3인/㎥ × 10%」 → 품 0.03 · 밑품 0.3 · 바꿔 쓴 식 · 「× 2회」 · 품할증 · 못 풂 = 역산) · N001 합판 지문(형틀목공 0.1111 · 보통인부 0.0444 · 조건 4회 · 보통 · 조항 건설 6-3-1) · 고임돌 섞음 율 · 몫 = C5 `sections` 와 같음 · STmate 자원 지문 · 기계 줄 |
|
||||
| `test_m02_hopyo_ours.py`(새) | 가 | 판 열쇠 같음 · 캐시 쓰기 · 읽기 · 준비 전 None · GC000211 「유형=보통」 형틀목공 0.112 · 섞기 전 변형 · master_data sha256 그대로 |
|
||||
| `test_m02_hopyo_option.py`(새) | 가 | N000 → GC000198 소량감률 50 「하나만」 · N001 → 유형 보통 · 갈리는 칸 측압 · 폼타이규격 · N013 → R 맞음 · 구조물터파기(인력100%) → 없음 · 다른 점 · 펌프차 41m → 기계 칸 |
|
||||
| `test_m02_hopyo_fit.py`(새) | 가 | 순위(같음 규칙 · 딱 맞음이 같음을 이김) · 판정 셋 · 조합(철근 = GC000206 + GC000207 · 고임돌 → GF000211 × 0.1) · 뒤 셈 캐시 · 「셈 중」 |
|
||||
| `test_m02_hopyo_golden.py`(새 · 느림) | 가(+ 나 영월) | 정답 표 지표(7-3) · 우리 지문 캐시 있으면 빠름 · 없으면 처음 ≈ 2 분 |
|
||||
| `test_m02_hopyo_match.py`(고침) | 가 | 새 상태 · 체크 제안 · 별칭 체크 규칙 셋 · % · 호표 · 노임 · 숫자 없음 · 옛 「들어감」 시험 바꿈 |
|
||||
| `test_m02_hopyo_stmate.py`(새) | 나 | 6 파일 올라감 · 호표 수 · 코드 갈래 · 노임 이름 어긋남 0 · 블록 자원 품 · 이음 열쇠 O 열 · 장수 · 진안 400 글 |
|
||||
| `test_m02_hopyo_excel.py` · `_basis.py` · `_grid.py`(고침) | 나 | 이음 넓힘 · 노임 갈래 · 블록 글 · 거창 출처 17 · 직종 21 · 윗마디 · 오타 · STmate 격자 |
|
||||
| `test_m02_hopyo_ledger.py`(새) | 다 | 옛 꼴 바꿔 읽음 · 새 꼴 · 층 차례 · 느슨 찾기 · 우리에 없음 · 안 씀 · 위 층 읽기만 · PUT 재검사 · 권한 |
|
||||
| `test_m02_hopyo_build.py`(고침) | 다 | 몸 줄만 들어감 · 몸 없음 400 · 빠진 줄 까닭 · 로직 여럿 같은 별칭 · 같은 로직 감쌈 · 재검사 400(없는 로직 · 모르는 입력 · 배 ≤ 0 · 금액 칸) · 하위 호표 409 · 장부 씀 · `본뜬.확인` · 덮음 |
|
||||
| `test_m02_hopyo_screen.py` + `helper_m02_hopyo_check.cjs`(새) | 다 | 확인 창 도우미(체크 → 몸 · 캐시 키 · 최신 응답만) · 옛 단추 · `saveHopyoMemory` 없음 · `buildHopyo(` 는 확인 창에서만 · 700 줄 · `npm run typecheck` |
|
||||
|
||||
- 돌리는 법 = `./venv/Scripts/python.exe -m pytest resources/tester/<고친 시험> -q` · 전체 시험은 사용자가 시킬 때만.
|
||||
|
||||
### 7-2. 정답 표 옮김 — 옮김(브레인 · 1 단계 앞)
|
||||
|
||||
- 자리 = `resources/tester/fixtures/m02_hopyo_golden.json` · 꼴 `[{id, 파일, 이름, 규격, 단위, 단산 | 코드, 무리: "사람" | "자동" | "씨앗" | "자동+씨앗", 정답: [로직], 허용: [로직], 짐작: bool}]`.
|
||||
- 재료 = 스크래치패드 `338_실험자료/C1/labels.py` · `labels_y.py`(사람 136 = 정답 126 + 우리에 없음 10) + `338_실험자료/K5/targets.json`(새 파일 51) + 단산 번(`338_실험자료/C1/out_items.json`) · STmate 코드(`338_실험자료/C6/data_*.json`) · 옮김 스크립트는 스크래치패드에(저장소엔 JSON 만).
|
||||
- 줄 찾기 = 시험이 실물 엑셀을 지금 읽기로 읽어 (파일 · 이름 · 규격 · 단위 · 단산 번 | 코드)로 맞춤 — 엑셀은 저장소 `resources/knowledge/original/실무문서/` (git 안 · 읽기만).
|
||||
- 호표 「조합 맞음」 정답 = 시험이 줄 정답을 호표로 모음(C5 `c07_hopyo` 와 같은 규칙 · 아래에서 위로).
|
||||
|
||||
### 7-3. 기준 수치 (다시 재고 ±1 줄 · 바꾸면 브레인 · 사용자 판단)
|
||||
|
||||
| 지표 | 기준 | 실험 값 |
|
||||
|---|---|---|
|
||||
| 북부청 92 줄 1순위 엄격(씨앗 켬 · 조합 전) | ≥ 88% (81 줄) | 82 줄 — 임도 91.8 · 사방 86.0(C4 지문 + 묶음 ① β=1) |
|
||||
| 사람 126 줄 1순위 엄격(조합 뒤 · 영월 읽기 뒤) | ≥ 92% (116 줄) | 92.9(336 재검토 ③) |
|
||||
| 사람 126 줄 다섯 안 엄격 | ≥ 96% (122 줄) | 96.8 |
|
||||
| 씨앗 끔 1순위 | ≥ 88% (111 줄) | 89.7 (113) |
|
||||
| 「맞음」 표시(사람 126) | ≥ 55 줄 · 틀림 ≤ 2 | 58 · 1(K5) |
|
||||
| 「다른 점」 틀림(사람 126) | ≤ 2 | 25 · 1 |
|
||||
| 옵션까지(1위가 정답인 줄 중 엑셀 품 그대로) | ≥ 50 줄 | 56 / 103 |
|
||||
| 새 파일 51 「맞음」 틀림 | ≤ 3 | 21 · 2 |
|
||||
| 조합 맞음(덮을 수 있는 83 호표 · T1) | ≥ 72% (60) | 75.9 (63) |
|
||||
| 확인 없이 들어가는 줄 | 0 — 몸 없는 만들기 400 · 몸 밖 줄은 사전조합에 없음 · `hopyo-memory` 404 · 화면에 바로 만들기 없음 | — |
|
||||
| STmate 6 파일 · 거창 | 200 · 호표 수 5-1 · 거창 출처 17 · 직종 21 · 장수 · 진안 400 글 | C5 · C6 |
|
||||
| 원본 안 고침 | `resources/master_data` *.json · 일위대가조합.json sha256 시험 앞뒤 같음(지금 `master_hash` 도우미) · M01 쓰기 없음 | — |
|
||||
| 셈 무게 | 우리 지문 캐시 없음 ≤ 180 초(뒤) · 있음 ≤ 3 초 · 북부청 임도 맞춤 셈 시간 기록(목표 ≤ 5 분 · 화면 잠금 없음) | 110 초(C4) |
|
||||
|
||||
- 「검증완료」 한 줄 = 위 시험 통과 + 수치 한 줄(1순위 · 다섯 안 · 맞음 · 틀림) · 검증보고서 파일 없음.
|
||||
|
||||
### 7-4. 브레인 브라우저 검증 목록 (끝에 · ORCA · 검증 층 · 조작한 값 되돌림)
|
||||
|
||||
1. 북부청 임도 올림 → 「숫자 판정 셈 중 n/m」 → 끝 · 화면 안 멈춤.
|
||||
2. 레미콘 소량 줄 = 「맞음 — GC000198 · 소량감률 50」 체크됨 · 품 비교 콘크리트공 0.1091 = 0.1091 · 굴착기 0.29.
|
||||
3. 합판 4회 = 「맞음 + 측압 · 폼타이규격 못 가름」 · 구조물터파기 = 「다른 점 — 특별인부 0.354 · 0.370」 체크 안 됨 · [옵션]으로 토질 고르면 품 비교가 서버 값으로 바뀜.
|
||||
4. 고임돌 = GF000211 × 0.1(인력 10%) · 철근가공조립 = 두 로직 · 같은 별칭.
|
||||
5. [확인하고 사전조합 만들기] → 사전조합 줄 = 체크한 줄만 · 빠진 줄 까닭 · 장부 줄 생김 · 금액은 우리 셈.
|
||||
6. 사방 올림 → 같은 이름 줄 「별칭」 체크됨 · 규격만 다른 줄은 체크 안 됨.
|
||||
7. [로직 추가] 실무 호표 → 확인된 것 = 바로 참조 / 아닌 것 = 확인 창 모달 → 참조 줄.
|
||||
8. 엑셀 보기 창 [고른 3개 확인하고 만들기] → 펼침 → 한 번.
|
||||
9. STmate 영월 올림 200 · 호표 23 · 줄 상태 · 직접 줄 호표 통째 제안.
|
||||
10. 별칭 장부 — 보기 · 고치기 · 지우기 · 위 층 읽기만 · [저장] 때만 씀.
|
||||
11. 옛 단추([확정] · [같음 모두 확정] · [근거 맞음 모두 확정]) 없음 · 확인 없이 들어감 0.
|
||||
12. 되돌림 — 만든 사전조합 · 장부 줄 · 올린 파일 지움(검증 층만).
|
||||
|
||||
---
|
||||
|
||||
## 8. 이번 밖 · 정함
|
||||
|
||||
### 8-1. 이번 밖
|
||||
|
||||
- M01 로직 별칭 칸 엮기(찾기용 · 사람 손) · 발주처 칸 · 별칭 자동 올림(개인 → 회사 → 마스터) · [회사로 올리기] 단추 · 자동 합침 · 쓰인 횟수.
|
||||
- 우리에 없는 로직 만들기(사용자 확인 뒤 시스템 관리자가 M01 [복사해서 만들기]) · 우리 로직 고치기(원문 근거 + 사용자 확인 뒤 따로).
|
||||
- 노임 줄 그릇(336 정할 것 7 — 「노임 · 그릇 없음」 표시만) · 기억 c 열쇠(로직 지문 열쇠) · 같은 열쇠 다른 답 쌓기 · 마스터 판 바뀜 알림 세부 · 사전조합에서 고친 별칭 · 옵션 → 장부 되돌림.
|
||||
- 문서 보안(DRM) 풀기 · STmate STC 직접 읽기 · 외부 AI · 엑셀 하이퍼링크 · HYPERLINK 식 읽기.
|
||||
- 「옛 판」 · 「다른 책」 까닭 자동 글(숫자 사실만 · 까닭은 [원문 보기]).
|
||||
|
||||
### 8-2. 정함 — 브레인 · 사용자가 바꿀 수 있음
|
||||
|
||||
1. 이음 열쇠 = 엑셀꼴 비고 번호 글(넓힘) · STmate 줄 코드(O 열 꼴 찾기) · 하이퍼링크 안 읽음.
|
||||
2. 「맞음」 = 숫자(R 허용 · 엑셀 몫 다 덮음) · 체크 제안 = 맞음 · 별칭(조건 셋) · 나머지 체크 안 함 · 이름 같음 · 씨앗은 상태가 아니라 순위 덤(「같음」 상태 없앰).
|
||||
3. 같은 일 다른 책 = 숫자(품)가 가름 · 숫자가 같으면 인용 책(336 정할 것 1 풀림 그대로).
|
||||
4. 섞인 줄 = 인력 몫 로직 × 몫이 기본 · 기계 몫 = 우리 일 로직이 맞으면 덧 · 아니면 「기계 시간당 기계경비」 후보(체크 안 함 · 「시간 = 엑셀 값」) · 기본 기대는 336 T1(인력 몫만).
|
||||
5. 직접 줄 호표 = 호표 통째 로직 하나 제안(호표 단위 체크).
|
||||
6. 별칭 장부 = `맞춤.json` 키움 · 확인 때만 씀 · 만드는 층 · 옛 기억은 바꿔 읽음.
|
||||
7. 캐시 = 저장소 `tmp/m02_hopyo/`(우리 지문 · 맞춤셈 · 마스터 판 열쇠) · 셈 = 올릴 때 뒤 스레드 하나 · 시동 땐 디스크 읽기만.
|
||||
8. 확인 다시 = 같은 본뜬 사전조합 덮음(`본뜬.확인`) · 옛 실무 사전조합은 [로직 추가]에서 확인 창으로.
|
||||
9. 엑셀 보기 창 여러 호표 = 펼쳐 보인 뒤 한 번 누름 = 사용자 확인.
|
||||
10. 옛 HP 파일 = 숫자 판정 없음 · 다시 올리면 됨(꼬리표 글) · 지금 storage 실무자료 셋 · 맞춤.json 2 줄뿐.
|
||||
11. 정답 표 = 저장소 시험 자료(JSON)로 옮김 · 기준 수치는 7-3(±1 줄).
|
||||
12. 무게 · 문턱 = 336 · K5 값 그대로(2-9) · 한 파일로 다시 맞추지 않음.
|
||||
Reference in New Issue
Block a user