날짜와 단위 정규화
75분 안팎
학습 목표
입력 JSON의 날짜를 ISO 날짜로 바꾸고 강수량 단위와 오류를 명시적으로 반환합니다.
개념
날짜가 같아야 같은 관측입니다
교통과 날씨를 지역·날짜로 결합했으므로 날짜 표기는 키의 일부입니다. 2026/09/01과 2026-09-01은 같은 하루를 가리킬 수 있지만 원문 문자열로 비교하면 다릅니다. 이번 계약은 네 자리 연도, 두 자리 월과 일, 하이픈 또는 슬래시 구분만 허용합니다. 변환 결과는 YYYY-MM-DD입니다. 날짜 객체로 달력의 유효성을 검사하고 isoformat으로 출력하면 표기와 의미를 함께 정리할 수 있습니다.
문자열을 단순 replace로 바꾸는 것은 형식 변환일 뿐 달력 검사가 아닙니다. 2026/02/30도 하이픈으로 바뀌지만 존재하는 날짜가 되지 않습니다. datetime.strptime는 지정한 형식으로 해석하면서 월·일의 유효성을 검사합니다. ValueError가 나면 다음 형식을 시도하거나 DATE 오류를 반환합니다. 넓은 예외로 빈 문자열을 반환하면 날짜 키가 없는 정상 행처럼 남습니다.
모호한 표기는 추측하지 않습니다
01/09/2026이 1월 9일인지 9월 1일인지는 작성 지역과 원본 명세에 달려 있습니다. 이 실습에서는 지원하지 않는 표기로 거부합니다. 입력의 첫 몇 행을 보고 자동으로 월일 순서를 추측하지 않습니다. YYYY-MM-DD 또는 YYYY/MM/DD만 정규식으로 형태를 검사한 다음 strptime으로 파싱합니다. Python 파서가 두 자리 미만의 월을 허용하더라도 계약 검사에서 먼저 막습니다.
윤년인 2024년 2월 29일은 허용하고 2026년 2월 29일은 거부합니다. 날짜 앞뒤 공백은 strip으로 제거하지만 내부 공백은 수정하지 않습니다. timestamp나 시간대가 들어온 입력은 이 날짜 전용 계약 밖입니다. 날짜만 있으면 하루 경계가 어느 시간대인지 자동으로 알 수 없으므로 실제 자료에서는 출처의 일별 집계 기준을 확인해야 합니다.
단위는 열 이름과 같이 넘깁니다
강수량 1이 mm인지 cm인지에 따라 해석은 열 배가 달라집니다. 브라우저 실습은 date,rain,unit 세 키를 받고 unit이 mm이면 그대로, cm이면 열 배 해서 rain_mm으로 반환합니다. 허용 단위는 이 두 가지이며 대소문자나 다른 기호를 임의로 추측하지 않습니다. 숫자는 비음수이며 유한해야 합니다. float로 변환이 되더라도 NaN과 infinity는 측정값 계약에 맞지 않습니다.
float는 근사 실수이므로 계산 뒤 마지막 자릿수가 달라질 수 있습니다. 과제는 소수점 한 자리로 표현할 수 있는 입력만 제공하고 출력은 round(value,1)로 고정합니다. 이것은 이 교육 과제의 표시 계약이며 실제 강수 자료를 한 자리로 반올림하라는 일반 규칙은 아닙니다. 원본의 해상도와 집계 규칙에 맞춰 정밀도를 정합니다. 미션 joined.csv는 이미 mm 단위이므로 열 배 변환을 다시 하면 안 됩니다.
누락과 변환 실패를 분리합니다
브라우저 실습의 rain은 문자열 숫자이고 빈 문자열 또는 null은 결측으로 보존합니다. 결측이어도 날짜와 단위는 검사합니다. 단위가 없으면 정상 결측으로 받아들이지 않고 UNIT 오류를 반환합니다. 날짜→단위→강수 순서로 검사하여 여러 오류가 있을 때 첫 이유를 고정합니다. 정상 출력에는 error:null, 오류 출력에는 date:null과 rain_mm:null, 그리고 오류 코드를 둡니다.
각 행이 독립적으로 변환되므로 한 행이 잘못되어도 나머지 결과를 반환합니다. rows 전체가 올바른 JSON 목록이라는 가정과 개별 값 오류는 구분합니다. 이 방식은 입력 번호와 같은 순서로 결과를 대조하기 쉽습니다. 모듈 미션에서는 성공 행과 오류 행을 별도 CSV로 쓰고 원문을 남기지만, 브라우저에서는 반환 객체 목록으로 판단을 연습합니다.
검사 순서를 코드와 테스트로 고정합니다
from datetime import datetime은 표준 라이브러리의 파서를 가져옵니다. 함수 normalize 안에서 날짜 형태를 확인하고 두 형식을 순서대로 시도합니다. 성공하면 반복을 끝내고 날짜 문자열을 저장합니다. return은 함수 결과를 호출자에게 돌려줍니다. 출력은 함수 밖에서 json.dumps로 만들면 파일이나 테스트에서도 같은 변환 함수를 재사용할 수 있습니다.
오류 코드 DATE는 입력 날짜 문자열과 원본 계약을 비교하라는 뜻입니다. UNIT은 숫자가 맞더라도 단위 해석이 정해지지 않았음을 뜻합니다. RAIN은 형식 오류·음수·비유한 값을 포함합니다. 실제 진단에서는 더 상세한 이유가 필요할 수 있으므로 원문과 번호를 함께 저장합니다. 계약을 바꿀 때는 정상·결측·윤년·잘못된 달력·단위 오류를 포함한 테스트를 먼저 수정합니다.
이 레슨의 프로젝트 적용과 검토
앞 모듈은 날씨 관측소의 하루 강수량을 지역·날짜별 유효 관측소의 같은 가중치 평균으로 계산했습니다. joined.csv의 rain_mm은 그 결과이므로 관측소 수로 다시 나누거나 하루 누적량을 여러 날 합친 수치로 읽지 않습니다. 브라우저의 cm 입력은 단위 변환을 연습하기 위해 별도로 만든 값입니다. 두 실습의 입력 계약을 구분해야 미션에서 mm를 또 변환하는 오류를 피할 수 있습니다.
날짜 정규화 함수의 출력은 문자열이지만 내부 검사에서는 날짜 객체를 사용합니다. 그래서 유효성 검사를 통과한 결과는 키에 다시 사용할 수 있습니다. 날짜가 오류인 행은 seen에 넣기 전에 거부해야 같은 지역의 뒤 정상 행이 살아남습니다. 2026/09/01과2026-09-01의 중복은 정규화 후에야 확인할 수 있습니다. 이번 실습은 입력과 같은 순서로 결과 객체를 내보내므로 어느 날짜가 실패했는지 원소 위치로 대조합니다.
아직 값이 없는 강수량은 단위 변환을 수행할 숫자가 없습니다. rain_mm을 None으로 둔 채 날짜와 단위가 유효한지 먼저 확인합니다. 단위 m처럼 계약 밖의 문자열은 강수 결측이어도 UNIT으로 반환합니다. 나중에 실제 자료에 센티미터 외 단위가 추가되면 분기와 환산 계수뿐 아니라 테스트·출처 메타데이터를 같이 수정합니다. 단위를 자동 추정하는 선택은 이 교육 과제의 모범 답안에 포함하지 않습니다.
에러 객체에서 date도 null로 두는 것은 이번 반환 형식을 단순하게 고정한 선택입니다. 잘못된 강수라도 원래 날짜 문자열은 입력 JSON에 남아 있습니다. 실제 파이프라인에서는 정상화에 성공한 필드와 원문을 더 자세히 보존할 수 있습니다. 미션은 오류 CSV에 원문을 써서 이런 추적을 제공합니다. 성공 객체와 오류 객체의 키를 같게 두면 후속 코드에서 error를 먼저 확인하고 같은 필드 구조로 처리할 수 있습니다.
일별 데이터라고 시간대를 자동 변환하지 않습니다. 시각이 없는 날짜는 시간대 변환의 기준이 부족합니다. 실제 공개 교통·날씨 자료를 받으면 하루 시작과 끝, 지역 시간 기준, 일별 누적과 관측소 평균의 차이를 출처에서 확인합니다. 이 과제는 그 설명을 생략한 채 현실의 다른 표를 안전하게 결합할 수 있다고 주장하지 않습니다. 문자열 형식 정리와 관측 기간의 의미 확인을 구분하여 다음 분석자에게 인계합니다.
따라하기
날짜 정규화
아래 코드를 Python 3에서 실행합니다. 각 단계는 독립 예제입니다.
from datetime import datetime
print(datetime.strptime('2026/09/01','%Y/%m/%d').date().isoformat())실행 결과
2026-09-01
달력 오류 읽기
아래 코드를 Python 3에서 실행합니다. 각 단계는 독립 예제입니다.
from datetime import datetime
for text in ['2024-02-29','2026-02-29']:
try:
print(datetime.strptime(text,'%Y-%m-%d').date().isoformat())
except ValueError:
print('DATE:',text)실행 결과
2024-02-29 DATE: 2026-02-29
단위와 오류 반환
아래 코드를 Python 3에서 실행합니다. 각 단계는 독립 예제입니다.
import json, math, re, sys
from datetime import datetime
def normalize(row):
text = row['date'].strip()
result_date = None
for fmt, pattern in [('%Y-%m-%d',r'[0-9]{4}-[0-9]{2}-[0-9]{2}'),('%Y/%m/%d',r'[0-9]{4}/[0-9]{2}/[0-9]{2}')]:
if re.fullmatch(pattern, text):
try:
result_date = datetime.strptime(text,fmt).date().isoformat()
except ValueError:
pass
if result_date is None:
return {'date':None,'rain_mm':None,'error':'DATE'}
if row['unit'] not in ['mm','cm']:
return {'date':None,'rain_mm':None,'error':'UNIT'}
raw = row['rain']
if raw is None or raw.strip() == '':
rain = None
else:
try:
rain = float(raw)
except ValueError:
return {'date':None,'rain_mm':None,'error':'RAIN'}
if not math.isfinite(rain) or rain < 0:
return {'date':None,'rain_mm':None,'error':'RAIN'}
rain *= 10 if row['unit'] == 'cm' else 1
rain = round(rain,1)
return {'date':result_date,'rain_mm':rain,'error':None}
print(json.dumps([normalize(row) for row in json.load(sys.stdin)]))
실행 결과
[{"date": null, "rain_mm": null, "error": "RAIN"}, {"date": null, "rain_mm": null, "error": "RAIN"}, {"date": null, "rain_mm": null, "error": "UNIT"}]
확인 문제
실습
입력은 date,rain,unit을 가진 JSON 객체 목록입니다. date는 문자열, rain은 문자열 또는 null, unit은 문자열입니다. 날짜는 공백 제거 후 YYYY-MM-DD 또는 YYYY/MM/DD만, 단위는 mm 또는 cm만 허용합니다. 강수는 유한 비음수이며 빈칸·null은 결측입니다. cm는10배 후 소수 한 자리로 round합니다. 날짜→단위→강수 순서로 검사합니다. 정상은 {"date":"ISO 날짜","rain_mm":숫자 또는 null,"error":null}, 오류는 date와 rain_mm을 null로 두고 error를 DATE·UNIT·RAIN 중 첫 코드로 반환합니다. 입력 순서 객체 목록을 JSON 한 줄로 출력합니다. 숫자 fixture는 소수 한 자리로 표현 가능한 범위입니다.
모범 답안
import json, math, re, sys
from datetime import datetime
def normalize(row):
text = row['date'].strip()
result_date = None
for fmt, pattern in [('%Y-%m-%d',r'[0-9]{4}-[0-9]{2}-[0-9]{2}'),('%Y/%m/%d',r'[0-9]{4}/[0-9]{2}/[0-9]{2}')]:
if re.fullmatch(pattern, text):
try:
result_date = datetime.strptime(text,fmt).date().isoformat()
except ValueError:
pass
if result_date is None:
return {'date':None,'rain_mm':None,'error':'DATE'}
if row['unit'] not in ['mm','cm']:
return {'date':None,'rain_mm':None,'error':'UNIT'}
raw = row['rain']
if raw is None or raw.strip() == '':
rain = None
else:
try:
rain = float(raw)
except ValueError:
return {'date':None,'rain_mm':None,'error':'RAIN'}
if not math.isfinite(rain) or rain < 0:
return {'date':None,'rain_mm':None,'error':'RAIN'}
rain *= 10 if row['unit'] == 'cm' else 1
rain = round(rain,1)
return {'date':result_date,'rain_mm':rain,'error':None}
print(json.dumps([normalize(row) for row in json.load(sys.stdin)]))
더 읽기
면접 질문
- 결측값을 삭제할지 채울지 판단하는 과정을 설명해 주시면 됩니다.