Ask AI for CSV cleaning rules and review every changed cell
Use an AI chat assistant to propose explicit data-cleaning rules, apply only approved rules to a copy of a small CSV, and create a cell-level before/after log. The example is synthetic so you can inspect every transformation yourself.
Content checked 2026.09.21Example code included
Show contents
Who this is forFor people who want AI help defining repeatable CSV-cleaning rules without letting the AI silently rewrite their data.
What you need
Python 3.12
A text editor and an AI chat assistant
01Start with a tiny synthetic CSV
This example uses synthetic people and addresses; none of the records are real. The goal is not to ask AI to clean the file directly. Instead, ask it to describe rules that you can inspect, approve, and implement deterministically.
Save the following as sample_people.csv. Notice the inconsistent spaces, email capitalization, department capitalization, missing-value marker, and numeric formatting.
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
02Ask AI for rules, not a rewritten file
Give the AI the column meanings and a few representative values. Ask it to separate safe formatting changes from transformations that could change meaning.
Do not approve a rule merely because it sounds reasonable. A recommendation such as converting every name to title case could corrupt valid capitalization. A rule such as replacing every blank hours value with zero would add information that the source did not contain.
03Turn the AI answer into an explicit rule table
A conservative AI answer for this example might suggest rules like these. Review them before writing code.
Column
Approved rule
Reason
name
Trim leading and trailing whitespace only
Removes accidental spacing without changing capitalization
email
Trim whitespace, then lowercase non-empty values
Normalizes the example addresses without inventing missing values
department
After trimming, map sales to Sales and engineering to Engineering case-insensitively
Uses a closed list visible in the example
hours
Trim whitespace; change NA to blank; normalize valid decimal notation
Makes the chosen missing marker and numeric representation consistent
row_id
Do not change
Preserves record identity
These rules are intentionally narrow. Unknown department values should remain unchanged rather than being guessed. Blank email addresses remain blank. No rows are deleted or deduplicated.
04Predict the changes before running anything
For a tiny example, work out the expected result manually. This gives you a reference against which to review the script output.
row_id
Column
Before
After
1
name
alice kim
alice kim
1
email
ALICE.KIM@example.com
alice.kim@example.com
1
department
sales
Sales
3
email
carol.park@example.com
carol.park@example.com
3
department
SALES
Sales
3
hours
8.0
8
4
department
engineering
Engineering
4
hours
NA
5
department
Sales
Sales
There should therefore be 9 changed cells. Row 2 should be unchanged, and the script should still output all 5 rows in their original order.
05Apply only the approved rules to a copy
The following standard-library script validates the expected columns, cleans each row, and records every cell whose value changes. It refuses to run if outputs already exists, reducing the chance of silently replacing an earlier review.
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()
Run it with python clean_csv.py after placing clean_csv.py and sample_people.csv in the same folder. For this example, the expected summary is 5 rows written and 9 changed cells.
06Review every changed cell
Open outputs/changes.csv before treating the cleaned file as usable. Each line should explain one transformation using row_id, column, original value, replacement value, and the rule that caused it.
Confirm that the change log contains exactly 9 rows for this synthetic example.
Check that row_id values themselves never appear as changed cells.
Compare the 9 entries with the manually predicted table above.
Confirm that row 2 does not appear in the change log.
Open outputs/sample_people_cleaned.csv and confirm that it still contains 5 data rows in the original order.
Investigate any additional change rather than assuming the AI-generated rule was correct.
07Common mistakes and limits
Do not ask the AI to infer missing hours, departments, or email addresses unless you have a separate justified method for doing so.
Do not use broad text transformations such as title-casing every name without checking whether legitimate values would be changed.
Do not delete duplicates merely because two rows look similar. Deduplication requires an explicit record-matching rule.
Do not overwrite the source CSV. Keep both the untouched input and the reviewed output.
Do not rely only on a total such as 9 changes. Review which cells changed and why.
For large or sensitive datasets, test rules on a representative sample and add domain-specific validation before applying them widely.
AI is useful here as a rule-drafting assistant, not as the final authority on what your data means. Column definitions, valid categories, missing-value conventions, and acceptable transformations should come from the dataset owner or another authoritative source.
Console output matched the article exactly: 5 rows written and 9 changed cells.
The change log contained exactly 9 changes: 3 in row 1, 3 in row 3, 2 in row 4, and 1 in row 5.
The nine changes exactly matched the predicted table: row 1 name/email/department; row 3 email/department/hours; row 4 department/hours; row 5 department.
Row 2 did not appear in the change log.
The cleaned CSV retained all 5 rows in their original order and preserved every row_id.
The cleaned values were as intended, including 8.0 -> 8, NA -> blank, department normalization, whitespace trimming, and lowercasing non-empty email addresses.
sample_people.csv was byte-for-byte unchanged after execution.
A second run stopped with 'Stop: outputs already exists. Review or rename it first.' and did not overwrite the previous outputs.
Verification limits
The test covers only the supplied synthetic CSV and approved rules; it does not validate broader department mappings, malformed CSV input, unusual encodings, multiline fields, or domain-specific data rules.
The Python environment emitted an unrelated spreadsheet-runtime warmup warning on stderr; it did not affect the article script or its results.
References
The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.
Turn “automate this” into an executable task description. Attach a synthetic sample with no sensitive data and a hand-checked expected result to complete a request for a script that totals work logs by team.
Check an aggregation function with four rows you can calculate by hand and 12 unit tests. Verify not only normal values but also empty input, zero, decimals, and invalid input.
Check an AI report against its source data, recalculate totals and rates, and use a copyable review prompt to flag unsupported claims and overstated causes before submission.
Treat an AI-generated regex as a draft, not a finished rule. Build a small synthetic test table, compare expected and actual matches, revise the pattern, and save a review report before using it on real data.