feat(scripts): dosage 순서 매칭으로 체중구간 자동 매핑

제품명에 사이즈 라벨이 없지만 dosage 컬럼으로 구분 가능한 제품
(하트웜 솔루션, 지마스터, 넥스포인트 등) 처리 추가.
- 고유 dosage 수 == 체중구간 수 일 때 오름차순 매칭
- 작은 용량 = 작은 체중 원칙 적용
- 결과: 146건 → 189건으로 커버리지 증가 (+43건)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
thug0bin 2026-03-08 18:08:19 +09:00
parent 90f88450be
commit 0bcae4ec72

View File

@ -356,6 +356,7 @@ def main():
'total_items': len(items),
'updated': 0,
'matched_by_name': 0,
'matched_by_dosage_order': 0,
'matched_single': 0,
'skipped_no_parse': 0,
'skipped_livestock': 0,
@ -402,6 +403,8 @@ def main():
if wr['size']:
size_to_weight[wr['size']] = (wr['min'], wr['max'])
# 먼저 제품명 라벨로 매칭 시도
unmatched_rows = []
for row in apc_rows:
size = detect_size_from_product_name(row.product_name)
if size and size in size_to_weight:
@ -412,10 +415,74 @@ def main():
if args.verbose:
print(f' 적용 (제품명 {size}): {row.product_name}{wmin}~{wmax}kg')
else:
stats['skipped_multi_no_label'] += 1
if args.verbose:
print(f' SKIP (다중구간+라벨없음): {row.product_name} '
f'(감지={size}, 가용={list(size_to_weight.keys())})')
unmatched_rows.append(row)
# ── 제품명 매칭 실패한 것들 → dosage 순서 매칭 시도 ──
if unmatched_rows:
# dosage 값이 있는 APC만 추출 (NaN 제외)
rows_with_dosage = [r for r in unmatched_rows
if r.dosage and r.dosage != 'NaN']
rows_no_dosage = [r for r in unmatched_rows
if not r.dosage or r.dosage == 'NaN']
if rows_with_dosage and len(weight_ranges) >= 2:
# dosage에서 첫 번째 숫자 추출하여 정렬 키로 사용
def dosage_sort_key(dosage_str):
nums = re.findall(r'(\d+\.?\d+)', dosage_str)
return float(nums[0]) if nums else 0
# 고유 dosage 값 추출 (순서 유지)
unique_dosages = sorted(
set(r.dosage for r in rows_with_dosage),
key=dosage_sort_key
)
# 체중 구간도 min 기준 정렬 (이미 정렬됨)
sorted_ranges = sorted(weight_ranges, key=lambda x: x['min'])
if len(unique_dosages) == len(sorted_ranges):
# 개수 일치 → 순서 매칭 (작은 용량 = 작은 체중)
dosage_to_weight = {}
for d, wr in zip(unique_dosages, sorted_ranges):
dosage_to_weight[d] = (wr['min'], wr['max'])
for row in rows_with_dosage:
if row.dosage in dosage_to_weight:
wmin, wmax = dosage_to_weight[row.dosage]
updates.append((row.apc, wmin, wmax, row.product_name,
f'dosage순서→{wmin}~{wmax}'))
stats['matched_by_dosage_order'] += 1
stats['updated'] += 1
if args.verbose:
print(f' 적용 (dosage순서): {row.product_name} '
f'dosage={row.dosage}{wmin}~{wmax}kg')
else:
stats['skipped_multi_no_label'] += 1
if args.verbose:
print(f' SKIP (dosage매칭실패): {row.product_name}')
# dosage 없는 APC (대표 품목 등)
for row in rows_no_dosage:
stats['skipped_multi_no_label'] += 1
if args.verbose:
print(f' SKIP (다중구간+dosage없음): {row.product_name}')
if args.verbose and dosage_to_weight:
print(f' dosage 매핑: {dict((d, f"{w[0]}~{w[1]}kg") for d, w in dosage_to_weight.items())}')
else:
# 개수 불일치 → SKIP
for row in unmatched_rows:
stats['skipped_multi_no_label'] += 1
if args.verbose:
print(f' SKIP (dosage수≠구간수): {row.product_name} '
f'(dosage {len(unique_dosages)}종 vs 구간 {len(sorted_ranges)}개)')
else:
# dosage 없는 APC만 남음
for row in unmatched_rows:
stats['skipped_multi_no_label'] += 1
if args.verbose:
print(f' SKIP (다중구간+라벨없음): {row.product_name} '
f'(감지={detect_size_from_product_name(row.product_name)}, '
f'가용={list(size_to_weight.keys())})')
# ── 결과 출력 ──
print('\n' + '=' * 60)
@ -425,6 +492,7 @@ def main():
print(f' 업데이트할 APC: {stats["updated"]}')
print(f' - 단일구간 적용: {stats["matched_single"]}')
print(f' - 제품명 라벨 매칭: {stats["matched_by_name"]}')
print(f' - dosage 순서 매칭: {stats["matched_by_dosage_order"]}')
print(f' SKIP - 파싱 불가: {stats["skipped_no_parse"]}')
print(f' SKIP - 축산용 (>60kg): {stats["skipped_livestock"]}')
print(f' SKIP - 다중구간+라벨없음: {stats["skipped_multi_no_label"]}')