AI 업무 활용

AI로 CSV 데이터 정제 규칙을 만들고 변경 셀을 모두 검토하기

AI 채팅 도우미에게 명확한 데이터 정제 규칙을 제안하게 한 뒤, 승인한 규칙만 CSV 사본에 적용하고 셀 단위 전후 변경 기록을 만듭니다. 예제는 모든 변환을 직접 확인할 수 있도록 만든 합성 데이터입니다.

목차 보기

이런 분께AI의 도움으로 반복 가능한 CSV 정제 규칙을 만들되, AI가 데이터를 임의로 바꾸지 못하도록 직접 검토하려는 사람을 위한 글입니다.

준비사항
  • Python 3.12
  • 텍스트 편집기와 AI 채팅 도우미

01작은 합성 CSV로 시작하기

이 예제의 사람 이름과 이메일 주소는 모두 합성 데이터이며 실제 인물이 아닙니다. 목표는 AI에게 파일을 직접 정제해 달라고 맡기는 것이 아닙니다. 대신 AI가 규칙을 설명하게 하고, 사용자가 그 규칙을 검토하고 승인한 뒤 결정적으로 적용하는 방식입니다.

다음 내용을 sample_people.csv로 저장합니다. 앞뒤 공백, 이메일 대소문자, 부서명 대소문자, 결측값 표기, 숫자 표기가 서로 일관되지 않습니다.

text
row_id,name,email,department,hours
1, alice kim ,ALICE.KIM@example.com, sales ,8
2,Bob Lee,bob.lee@example.com,Engineering,7.5
3,Carol Park, carol.park@example.com ,SALES, 8.0
4,Dan Choi,,engineering,NA
5,Eve Han,eve.han@example.com, Sales,9

02AI에게 파일 수정보다 규칙 제안을 요청하기

AI에게 각 열의 의미와 대표 값을 알려주고, 의미를 바꾸지 않는 안전한 형식 정제와 해석이 필요한 변환을 구분해 달라고 요청합니다.

그럴듯해 보인다는 이유만으로 규칙을 승인하면 안 됩니다. 예를 들어 모든 이름을 Title Case로 바꾸면 원래 의도한 대소문자가 훼손될 수 있습니다. hours의 빈 값을 모두 0으로 바꾸는 규칙 역시 원본에 없던 정보를 새로 추가하는 것입니다.

03AI 답변을 명시적인 규칙표로 바꾸기

이 예제에서 보수적인 AI 답변은 다음과 같은 규칙을 제안할 수 있습니다. 코드를 작성하기 전에 각 항목을 먼저 검토합니다.

승인한 규칙이유
name앞뒤 공백만 제거대소문자는 바꾸지 않고 불필요한 공백만 제거
email공백 제거 후 비어 있지 않은 값만 소문자로 변환결측값을 새로 만들지 않으면서 예제 이메일 형식을 통일
department공백 제거 후 대소문자를 무시하고 sales는 Sales, engineering은 Engineering으로 매핑예제에서 확인 가능한 제한된 목록만 사용
hours공백 제거, NA는 빈 값으로 변경, 유효한 소수 표기를 정규화선택한 결측값 표기와 숫자 표현을 일관되게 만듦
row_id변경하지 않음레코드 식별자 유지

이 규칙들은 의도적으로 범위를 좁게 잡았습니다. 알 수 없는 부서 값은 추측해서 바꾸지 않고 그대로 둡니다. 빈 이메일은 빈 상태로 유지합니다. 행 삭제나 중복 제거도 수행하지 않습니다.

04실행 전에 예상 변경값 직접 계산하기

작은 예제라면 실행 전에 예상 결과를 직접 계산할 수 있습니다. 이렇게 만든 기준표를 나중에 스크립트 결과와 비교합니다.

row_id변경 전변경 후
1name alice kim alice kim
1emailALICE.KIM@example.comalice.kim@example.com
1department sales Sales
3email carol.park@example.com carol.park@example.com
3departmentSALESSales
3hours 8.08
4departmentengineeringEngineering
4hoursNA
5department SalesSales

따라서 변경되는 셀은 총 9개여야 합니다. row 2는 아무 값도 바뀌지 않아야 하며, 출력 결과에는 원래 순서 그대로 5개 행이 모두 남아 있어야 합니다.

05승인한 규칙만 사본에 적용하기

다음 표준 라이브러리 기반 스크립트는 예상 열을 먼저 검증하고, 각 행을 정제하면서 값이 실제로 바뀐 모든 셀을 기록합니다. outputs 폴더가 이미 존재하면 실행을 중단하므로 이전 검토 결과를 실수로 덮어쓸 가능성을 줄입니다.

python
import csv
import sys
from decimal import Decimal, InvalidOperation
from pathlib import Path

INPUT = Path("sample_people.csv")
OUTPUT_DIR = Path("outputs")
CLEANED = OUTPUT_DIR / "sample_people_cleaned.csv"
CHANGES = OUTPUT_DIR / "changes.csv"

EXPECTED_FIELDS = ["row_id", "name", "email", "department", "hours"]
DEPARTMENT_MAP = {
    "sales": "Sales",
    "engineering": "Engineering",
}


def clean_hours(value):
    trimmed = value.strip()
    if trimmed == "NA":
        return "", "NA converted to blank"
    if trimmed == "":
        return "", "trim whitespace"

    try:
        number = Decimal(trimmed)
    except InvalidOperation:
        return trimmed, "trim whitespace"

    if number == number.to_integral():
        normalized = str(number.quantize(Decimal("1")))
    else:
        normalized = format(number.normalize(), "f")
    return normalized, "normalize numeric notation"


def clean_cell(column, value):
    if column == "row_id":
        return value, "preserve row_id"

    if column == "name":
        return value.strip(), "trim name whitespace"

    if column == "email":
        trimmed = value.strip()
        return trimmed.lower() if trimmed else "", "trim and lowercase email"

    if column == "department":
        trimmed = value.strip()
        normalized = DEPARTMENT_MAP.get(trimmed.lower(), trimmed)
        return normalized, "trim and normalize known department"

    if column == "hours":
        return clean_hours(value)

    return value, "no rule"


def main():
    if not INPUT.is_file():
        sys.exit(f"Input file not found: {INPUT}")

    if OUTPUT_DIR.exists():
        sys.exit(f"Stop: {OUTPUT_DIR} already exists. Review or rename it first.")

    OUTPUT_DIR.mkdir()

    cleaned_rows = []
    changes = []

    with INPUT.open("r", encoding="utf-8", newline="") as source:
        reader = csv.DictReader(source)
        if reader.fieldnames != EXPECTED_FIELDS:
            sys.exit(
                f"Unexpected columns. Expected {EXPECTED_FIELDS}, got {reader.fieldnames}"
            )

        for row in reader:
            cleaned = {}
            for column in EXPECTED_FIELDS:
                before = row[column]
                after, rule = clean_cell(column, before)
                cleaned[column] = after

                if before != after:
                    changes.append(
                        {
                            "row_id": row["row_id"],
                            "column": column,
                            "before": before,
                            "after": after,
                            "rule": rule,
                        }
                    )

            cleaned_rows.append(cleaned)

    with CLEANED.open("x", encoding="utf-8", newline="") as target:
        writer = csv.DictWriter(target, fieldnames=EXPECTED_FIELDS)
        writer.writeheader()
        writer.writerows(cleaned_rows)

    with CHANGES.open("x", encoding="utf-8", newline="") as target:
        writer = csv.DictWriter(
            target,
            fieldnames=["row_id", "column", "before", "after", "rule"],
        )
        writer.writeheader()
        writer.writerows(changes)

    print(f"Rows written: {len(cleaned_rows)}")
    print(f"Changed cells: {len(changes)}")
    print(f"Cleaned copy: {CLEANED}")
    print(f"Change log: {CHANGES}")


if __name__ == "__main__":
    main()

clean_csv.py와 sample_people.csv를 같은 폴더에 둔 뒤 python clean_csv.py로 실행합니다. 이 예제에서 예상되는 요약은 5 rows written과 9 changed cells입니다.

06변경된 모든 셀 검토하기

정제된 파일을 사용하기 전에 outputs/changes.csv부터 확인합니다. 각 행에는 row_id, 열 이름, 원래 값, 변경 값, 적용된 규칙이 기록되어 있어 한 번의 변환을 설명할 수 있어야 합니다.

  1. 이 합성 예제에서 변경 기록이 정확히 9행인지 확인합니다.
  2. row_id 자체가 변경된 셀로 기록되지 않았는지 확인합니다.
  3. 9개 변경 항목을 앞에서 직접 계산한 표와 비교합니다.
  4. row 2가 변경 기록에 나타나지 않는지 확인합니다.
  5. outputs/sample_people_cleaned.csv를 열어 원래 순서 그대로 5개 데이터 행이 남아 있는지 확인합니다.
  6. 예상하지 못한 변경이 하나라도 있으면 AI가 제안한 규칙이 맞다고 가정하지 말고 원인을 확인합니다.

07흔한 실수와 한계

  • 별도의 근거가 없다면 AI에게 누락된 hours, department, email 값을 추정하게 하지 않습니다.
  • 정상적인 값까지 바뀔 수 있으므로 모든 이름을 일괄 Title Case로 변경하는 식의 광범위한 텍스트 변환은 피합니다.
  • 두 행이 비슷해 보인다는 이유만으로 중복을 삭제하지 않습니다. 중복 제거에는 명시적인 레코드 매칭 규칙이 필요합니다.
  • 원본 CSV를 덮어쓰지 않습니다. 수정하지 않은 입력 파일과 검토한 출력 파일을 모두 보관합니다.
  • 9 changes 같은 총합만 보지 말고 어떤 셀이 왜 바뀌었는지 확인합니다.
  • 대규모 데이터나 민감한 데이터에서는 대표 샘플로 규칙을 먼저 시험하고, 전체 적용 전에 도메인별 검증 절차를 추가합니다.

여기서 AI는 데이터 의미를 최종적으로 판단하는 주체가 아니라 정제 규칙 초안을 만드는 보조 수단입니다. 열 정의, 유효한 범주, 결측값 규칙, 허용 가능한 변환은 데이터 소유자나 다른 신뢰할 수 있는 기준에서 확인해야 합니다.

실행·검증 기록

2026-09-21 · hand-checked example · Python 3.12

  • 합성 CSV와 승인한 변환 규칙을 직접 확인했습니다.
  • 예상되는 셀 단위 변경은 총 9개이며, row 1에서 3개, row 3에서 3개, row 4에서 2개, row 5에서 1개입니다.
  • row 2는 예상 변경이 없습니다.
  • 정제 후에도 총 5개 행과 원래 행 순서, row_id 값이 유지되는 것으로 계산했습니다.
  • 스크립트는 outputs/sample_people_cleaned.csv와 outputs/changes.csv에만 쓰며 sample_people.csv는 덮어쓰지 않습니다.
  • outputs 폴더가 이미 존재하면 실행을 중단하도록 구성되어 있습니다.
검증 범위의 한계
  • Python 코드는 직접 실행하지 않았으며, 작은 예제와 예상 결과를 손으로 검토했습니다.
  • department 매핑은 의도적으로 Sales와 Engineering만 포함하며 일반적인 부서명 정규화 규칙이 아닙니다.
  • 이메일 소문자 변환은 이 튜토리얼의 명시적인 예제 규칙입니다. 실제 조직 데이터에 적용하기 전에는 해당 조직의 데이터 정책을 확인해야 합니다.
  • 이 예제는 잘못된 CSV 형식, 특수 인코딩, 여러 줄 필드, 스키마 변경, 중복 레코드 판정, 도메인별 유효성 규칙까지 다루지는 않습니다.

사이트 전체의 작성·검증 원칙

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.