Devin.KR

파일 읽기와 결측 처리 비교

100분 안팎

학습 목표

with와 csv로 joined.csv를 읽어 삭제·미대체 정책의 행 수와 지역별 결측률을 비교합니다.

개념

읽는 동안 파일을 닫을 책임을 정합니다

앞 모듈의 결과는 메모리 값이 아니라 joined.csv 파일입니다. with Path(path).open(...) as file은 블록을 벗어날 때 파일을 닫도록 관리합니다. CSV에는 newline=""를 주어 CSV 모듈이 줄바꿈을 처리하게 하고 encoding="utf-8-sig"로 BOM이 붙은 UTF-8 헤더도 읽습니다. DictReader는 첫 레코드를 열 이름으로 삼아 이후 레코드를 딕셔너리로 반환합니다. 따옴표로 둘러싼 쉼표를 다루므로 split(",")로 대신하지 않습니다.

이번 미션은 헤더의 이름과 순서가 앞 모듈과 같아야 합니다. 누락된 열이나 추가된 열이 있으면 HEADER로 중단합니다. 각 레코드의 열 수가 다르면 CSV 오류로 중단합니다. 구조가 깨진 파일을 일부 정상으로 읽으면 열이 이동하여 엉뚱한 수치를 만들 수 있습니다. 파일 구조 실패와 개별 값 실패를 구분하고, 구조 검사는 출력 파일을 열기 전에 완료합니다.

정상 결측은 남겨 둡니다

CSV 빈칸은 Python에서 빈 문자열로 읽힙니다. 숫자 변환 전 빈칸을 검사하여 None으로 바꾸고 관측된 0은 숫자 0으로 유지합니다. clean.csv에 쓸 때 None은 빈칸으로 저장합니다. 앞 모듈의 valid_observations가 0이면 통행량 빈칸을 정상 결측으로 보존하고, 관측 수가 있는데 통행량이 비었다면 CONTRACT 오류로 분리합니다. 강수 빈칸은 날씨 행 누락 또는 값 누락 상태와 함께 보존합니다.

정상은 분석에 바로 사용할 수 있는 완전 관측이라는 뜻이 아닙니다. clean_rows에는 구조와 값 계약에 맞는 결측 행도 포함됩니다. complete_rows는 교통과 강수가 모두 존재하는 행의 수입니다. rejected_rows와 결측 행 수를 섞으면 정제 품질을 잘못 해석합니다. 같은 정상 표에서도 어떤 지표를 구하느냐에 따라 포함 조건과 분모가 달라집니다.

두 정책의 같은 점과 다른 점을 셉니다

보존 정책은 6행을 그대로 두고 교통 평균은 유효한 교통 5개만 사용합니다. 합계390대를 5로 나누어 78대입니다. 완전 사례 정책은 교통과 강수가 모두 있는 4행으로 제한하고 교통 평균은 300대를 4로 나누어 75대입니다. 두 평균은 계산 실수 때문에 다른 것이 아니라 포함된 관측이 다르기 때문에 다릅니다. 보존 평균을 강수와의 관계 분석에 그대로 쓰면 짝이 없는 교통 관측을 섞게 됩니다.

지역별 강수 결측률의 분모는 정제 후 해당 지역의 전체 행 수이고 분자는 rain_mm이 None인 행 수입니다. A와 B 모두 3행 중 1행 결측이므로 1/3입니다. 지역별 결측률이 같더라도 제외된 날짜와 통행량이 같지 않습니다. 짧은 기간의 합성 표본으로 편향이 없다고 결론 내릴 수 없습니다. 정책 비교는 영향의 일부를 수치로 보여 주며 결측 원인의 설명을 대신하지 않습니다.

0 대체의 의미를 질문합니다

강수 결측을 0으로 채우면 날씨를 관측하지 않은 날을 무강수 날로 가정하게 됩니다. 교통 결측을 0으로 채우면 통행이 없었다는 관측을 만들어 냅니다. 평균 대체도 지역·날짜별 분포를 바꿀 수 있으므로 이 미션은 자동 대체를 하지 않습니다. 삭제와 미대체를 비교한 후 어떤 정책으로 다음 지표를 계산할지 policy-note.md에 근거와 한계를 작성합니다.

분모가 0인 빈 파일이나 전부 결측인 목록에서는 평균을 None으로 반환합니다. 0으로 반환하면 실제 평균0과 계산할 관측 없음이 구분되지 않습니다. ZeroDivisionError가 나오면 나누기 전에 유효 관측 수를 검사했는지 확인합니다. 지역 표의 행 수도 별도로 남기면 결측률과 평균을 읽는 사람이 표본 크기를 확인할 수 있습니다.

출력과 감사 기록을 함께 전달합니다

미션의 classify는 정상·오류·제거한 완전 중복을 나눕니다. input_rows=clean_rows+rejected_rows+duplicate_rows가 성립해야 합니다. 충돌 키는 완전 중복 수에 넣지 않고 오류로 기록합니다. rejected.csv와 duplicates.csv에는 원본 열, 레코드 번호, 이유가 함께 남습니다. clean-audit.json은 두 정책의 분모·평균·지역별 결측률을 남깁니다. 총 행 수만 맞아도 잘못된 개별 행이 있을 수 있으므로 fixture 값도 검사합니다.

ZIP을 푼 폴더에서 테스트를 실행한 뒤 cleaning.py의 --input joined.csv --out out을 실행합니다. 상대 경로는 명령을 실행한 폴더를 기준으로 찾습니다. FileNotFoundError가 나오면 현재 위치와 입력 경로를 먼저 확인합니다. 입력과 출력이 겹치면 PATH 오류로 중단하며 입력 바이트 보존과 동일 입력 재실행의 출력 일치를 테스트합니다. 여러 출력 파일을 한꺼번에 원자적으로 교체하는 운영 기능은 이후 ETL 설계에서 확장합니다.

이 레슨의 프로젝트 적용과 검토

로컬 ZIP에는 이전 모듈의 원본·SQL·계약 검사와 새 cleaning.py가 함께 있습니다. 이전 파일을 새 정제 규칙에 맞추어 바꾸는 것이 과제는 아닙니다. 기존29개 테스트가 계속 통과하는지 확인하고 새12개 테스트로 정제 분류와 정책 계산을 점검합니다. starter는 구조와 변환 도구를 제공하되 두 정책 함수가 미완성입니다. 실패한 새 테스트 이름을 읽으면 중복 분류와 완전 사례 선택 중 어느 부분을 고쳐야 하는지 알 수 있습니다.

파일 검사에서 BOM과 줄바꿈을 명시하는 것은 운영체제 차이에 따른 해석을 줄이기 위한 선택입니다. 출력은 UTF-8과 일정한 줄바꿈으로 저장하며 감사 JSON의 키 순서도 고정합니다. 같은 입력으로 다시 실행하면 출력이 달라지지 않는지 바이트로 비교합니다. 다만 실행 중 여러 출력 파일 중 일부만 교체된 상황까지 복구하는 것은 이 스크립트가 제공하지 않습니다. 로컬 학습용 결과와 운영용 저장 흐름의 책임을 혼동하지 않습니다.

실제 따라하기에서는 숫자 목록을 코드 안에 둔 작은 예제로 정책을 확인하고, ZIP에서는 joined.csv를 읽어 같은 계산을 수행합니다. 전자는 파일 위치와 무관하게 분모 계산을 확인하기 위한 예제이고 후자는 입출력과 감사 기록까지 검증하는 실습입니다. 값이 다르면 원본6행과 clean.csv를 먼저 대조하고, 다음으로 유효 교통 수와 완전 사례 수를 확인합니다. 평균만 비교하면 제외 조건의 오류를 찾기 어렵습니다.

policy-note.md는 테스트 통과 여부와 별개로 사람이 읽는 제출물입니다. 입력이 합성 표본임을 밝히고, 보존6행·교통 유효5개·완전 사례4행이라는 수를 함께 씁니다. A와 B의 강수 결측 비율이 같아도 날짜별 대표성이 같다고 단정하지 않습니다. 다음 모듈에서 지표의 분모를 정의할 때 어떤 제외 조건을 적용할지 제안하고, 자료가 더 들어오면 다시 검토할 조건도 적습니다. 선택 근거 없이 파일 두 개만 내는 것으로 판단 과제가 끝나지 않습니다.

작업이 끝나면 out 폴더의 네 결과 파일을 다음 단계에 전달할 수 있습니다. 오류 CSV가 헤더만 가진 빈 표라면 이번 입력에서 거부 행이 없다는 뜻이며 검사 코드가 불필요하다는 뜻은 아닙니다. tests의 잘못된 날짜·숫자·충돌 fixture가 실제로 거부되는지 확인합니다. 원본행 수 보존 등식을 감사 파일에서 대조한 뒤, 같은 입력의 재실행 결과와 원본 해시를 비교하면 정제에 따른 변화의 범위를 설명할 수 있습니다.

따라하기

CSV에서 빈칸과 0 읽기

아래 코드를 Python 3에서 실행합니다. 각 단계는 독립 예제입니다.

import csv, io
text = 'region,rain_mm\nA,0\nB,\n'
with io.StringIO(text, newline='') as file:
    for row in csv.DictReader(file):
        value = None if row['rain_mm'] == '' else float(row['rain_mm'])
        print(row['region'], value)

실행 결과

A 0.0
B None

삭제와 보존 평균 비교

아래 코드를 Python 3에서 실행합니다. 각 단계는 독립 예제입니다.

rows = [(100,3),(80,0),(None,None),(120,10),(0,0),(90,None)]
traffic = [v for v,r in rows if v is not None]
pairs = [v for v,r in rows if v is not None and r is not None]
print(len(rows), len(traffic), sum(traffic)/len(traffic))
print(len(pairs), sum(pairs)/len(pairs))

실행 결과

6 5 78.0
4 75.0

지역별 결측률과 빈 분모

아래 코드를 Python 3에서 실행합니다. 각 단계는 독립 예제입니다.

rows = [('A',3),('A',0),('A',None),('B',10),('B',0),('B',None)]
for region in ['A','B']:
    values = [rain for name,rain in rows if name == region]
    print(region, sum(v is None for v in values), len(values), round(sum(v is None for v in values)/len(values),3))
values = []
print(sum(values)/len(values) if values else None)

실행 결과

A 1 3 0.333
B 1 3 0.333
None

확인 문제

실습

미션 starter ZIP을 풀고 README-cleaning.md를 읽습니다. cleaning.py의 classify와 compare를 완성합니다. 정상 결측을 보존하고 오류·완전 중복·충돌 중복을 분류합니다. python3 -m unittest discover -s tests -v 후 python3 cleaning.py --input joined.csv --out out을 실행합니다. out/clean-audit.json의 두 평균78대·75대와 지역별 결측률1/3을 대조합니다. policy-note.md에 두 정책의 분모, 제외 행, 지역·날짜 편향 가능성과 선택 근거를 작성합니다. 판단 문서는 사람이 검토합니다.

시작 코드·테스트 내려받기

실행 명령

python3 -m unittest discover -s tests -v

기대 결과

기존29개와 새12개, 총41개 테스트 통과. starter는 새 정책 테스트 일부 실패.

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 결측값을 삭제할지 채울지 판단하는 과정을 설명해 주시면 됩니다.