Fundamentals

Cleaning CSV Data with Pandas for AI: A Step-by-Step Script

Copy-paste Pandas script to clean CSV files for AI. Fix missing values, drop duplicates, set types, normalize text, and export a model-ready CSV.

This guide shows you how to turn a messy CSV file into a clean, model-ready dataset with Pandas in under fifteen minutes. You will load a raw file, fix missing values, drop duplicates, set the right column types, normalize text, and export a file you can hand straight to an AI step. Replace the example column names with your own headers, run the script, and you get a predictable dataset every time.

Pandas is the standard Python library for working with table-shaped data (rows and columns, like a spreadsheet). A DataFrame is the in-memory table Pandas builds from your file. If those two terms are new, that is fine, the steps below explain each line as you go.

Raw exports cause more AI problems than most people expect. Inconsistent casing turns Acme and acme into two different things. Trailing spaces hide in text fields. Empty cells trigger API validation errors. A clean file fixes all of that before the data ever reaches a model, which is why data hygiene is a foundation skill covered across Data Cleaning for AI.

Prerequisites

This guide assumes you already have Python 3.10 or newer and a working virtual environment. If you do not, set one up first with Create a Python Virtual Environment for AI, then return here.

Install the one dependency you need:

pip install pandas

You also need a CSV file to clean. The examples below use a file named input.csv with columns called company, prompt_text, category, and price. Swap these for your real headers as you follow along. To see your actual column names, run a quick check first:

import pandas as pd

df = pd.read_csv("input.csv")
print(df.columns.tolist())
print(df.shape)  # (rows, columns)

df.shape prints a pair like (1200, 4), meaning 1,200 rows and 4 columns. Note that starting number, you will compare it against the cleaned count at the end to confirm nothing went missing by accident.

Step 1: Load the CSV and inspect it

Always look at the data before you change it. Loading and inspecting takes seconds and saves you from cleaning the wrong column.

import pandas as pd

df = pd.read_csv("input.csv", encoding="utf-8-sig")  # utf-8-sig strips Excel's hidden BOM

print(df.info())   # column names, non-null counts, and types
print(df.head())   # the first five rows

df.info() is the most useful single command here. It lists every column, how many non-null (non-empty) values it holds, and the type Pandas guessed for it. If a column you expect to be numbers shows up as object (Pandas's label for text), that is an early warning that the column contains stray text you will fix in Step 3.

A BOM (byte order mark) is an invisible character some programs write at the start of a file. Reading with encoding="utf-8-sig" removes it so it does not contaminate your first column header.

Those few lines involve three separate things worth keeping apart in your head: a file sitting on disk, a parser that decides how bytes become text, and a table in memory that can then describe itself.

How read_csv turns a file on disk into an inspectable DataFrame A left-to-right flow in which the raw input.csv file is read by pd.read_csv with utf-8-sig encoding, producing a DataFrame held in memory, which then answers the two inspection calls df.info and df.head. input.csv raw export on disk pd.read_csv() encoding=utf-8-sig DataFrame table in memory df.info() types + null counts df.head() first five rows
Reading the file is one step; the DataFrame it produces is what answers every later question about shape, types, and gaps.

Step 2: Fix missing values

Missing values are the most common cause of AI API errors, because most endpoints reject empty or null text. Handle required fields and optional fields differently.

df = df.dropna(subset=["prompt_text", "category"])   # no text, no row
df["company"] = df["company"].fillna("unknown")      # keep the row, mark the gap

remaining_gaps = df[["prompt_text", "category"]].isnull().sum().sum()
print(f"Remaining gaps in required columns: {remaining_gaps}")

The difference matters. dropna deletes rows, so you only want it where a missing value makes the row worthless. fillna keeps the row and replaces the gap, which preserves data you can still use. Print the gap count to confirm the required columns are now fully populated.

The choice is easier to apply consistently if you reduce it to a single question you ask once per column, before you write either line.

Deciding whether to drop a row or fill a missing value A decision tree starting from a missing value found in a column, asking whether the AI step requires that field, and branching to dropna for required fields or fillna with a placeholder for optional ones. Missing value found in a column Required field? for your AI step yes, required no, optional Drop the row dropna(subset=...) Keep the row fillna('unknown')
Ask the question once per column: only fields your AI step genuinely reads justify deleting a row, everything else gets an explicit placeholder.

Step 3: Remove duplicates and fix types

Duplicate rows inflate costs (you pay to process the same text twice) and skew any counts or analysis. Wrong types cause crashes the moment you call a text method on a number.

before = len(df)
df = df.drop_duplicates()                            # exact matches across every column
print(f"Removed {before - len(df)} duplicate rows")

df["prompt_text"] = df["prompt_text"].astype(str)    # .str methods need real strings
df["category"] = df["category"].astype(str)

df["price"] = pd.to_numeric(df["price"], errors="coerce")  # "N/A" becomes NaN, not a crash

drop_duplicates() with no arguments removes rows that match across all columns. If two rows should count as duplicates based on one key column only, pass subset=["prompt_text"] to compare just that field.

Note the limit of this approach: it only catches rows that are identical character for character. Two support tickets that say the same thing in different words survive it untouched. When that kind of near-duplicate is the problem you actually have, Remove Duplicate Records with Embeddings in Python shows the meaning-based approach that catches them.

pd.to_numeric(..., errors="coerce") is the safe way to convert text to numbers. The errors="coerce" setting turns anything it cannot parse into NaN (Pandas's missing-value marker) instead of crashing. After this line you can fill or drop those new gaps the same way you did in Step 2.

Step 4: Normalize text fields

Normalizing means forcing text into one consistent shape so identical values actually match. This is the step that most improves prompt accuracy and embedding quality.

df["prompt_text"] = (
    df["prompt_text"]
    .str.strip()                               # remove leading/trailing spaces
    .str.replace(r"\s+", " ", regex=True)      # collapse runs of whitespace
    .str.replace(r"[\r\n]+", " ", regex=True)  # flatten line breaks
)

df["category"] = df["category"].str.strip().str.lower()  # "Sales" and "SALES" become one value

Each method in the chain does one job. .str.strip() trims the outer spaces. .str.replace(r"\s+", " ", regex=True) collapses double and triple spaces into single ones. The line-break replacement flattens hidden \r and \n characters that break CSV and JSON payloads. Lowercasing labels is what lets later grouping treat Sales and sales as the same category.

Be deliberate about which columns you lowercase. Lowercasing a category label is helpful; lowercasing a sentence you plan to show a user later may not be. Apply it only where consistent matching matters more than the original casing.

Seeing one value travel through the chain makes the division of labour obvious: each call fixes exactly one defect and leaves the others alone.

Three text defects and the string method that removes each one A before and after comparison with three rows: padded text fixed by strip, runs of whitespace collapsed by a regex replace, and mixed casing folded to one label by lower. Raw value After cleaning Outer spaces ' Acme Corp ' .str.strip() Trimmed 'Acme Corp' Runs of spaces double and tab gaps .str.replace(regex) Single spaces one space between Mixed casing Sales, sales, SALES .str.lower() One label sales
Each call in the chain targets one defect, which is why the order rarely matters but the combination does.

Step 5: Validate and export a clean CSV

Before you trust the file, run three quick checks, then write it to a new name so your raw data stays intact.

assert df[["prompt_text", "category"]].isnull().sum().sum() == 0   # 1. no gaps left
print(f"Final rows: {len(df)}")                                    # 2. expected row count
print(df.sample(min(5, len(df))))                                  # 3. eyeball a few rows

df.to_csv("clean_output.csv", index=False, encoding="utf-8")  # index=False drops the row numbers
print("Saved clean_output.csv, ready for your AI step.")

Writing to clean_output.csv instead of overwriting input.csv means you can re-run with different settings if you spot a problem. The cleaned file is now ready to feed into an AI workflow, whether you are sending each row to a model or building embeddings from it. For where that data goes next, see Understanding LLM APIs.

Two things are worth checking before you start sending rows. If any single cleaned cell is longer than a model will accept in one request, break it up first using Split Long Text into Chunks for AI APIs. And if the file is large enough that the bill matters, price the run up front with Count Tokens in Python Before You Send rather than discovering the cost afterwards.

Key-parameter quick reference

ParameterMethodDefaultEffect
encodingpd.read_csv / to_csvplatform defaultSet to "utf-8-sig" to strip Excel's BOM on read; "utf-8" on write keeps accents and emoji intact.
subsetdropna / drop_duplicatesall columnsLimits the check to the listed columns, so you only act on the fields that matter.
errorspd.to_numeric"raise"Set to "coerce" to turn unparseable values into NaN instead of crashing the script.
indexto_csvTrueSet to False to avoid writing an extra unnamed row-number column to your output file.

Troubleshooting

  1. UnicodeDecodeError: 'utf-8' codec can't decode byte — Cause: the file is not UTF-8, often it is Latin-1 from an older system. Fix: pass the matching encoding, for example pd.read_csv("input.csv", encoding="latin-1").
  2. AttributeError: Can only use .str accessor with string values — Cause: you called a .str method on a column that holds numbers or mixed types. Fix: cast it first with df["col"] = df["col"].astype(str) before the normalization chain.
  3. KeyError: 'prompt_text' — Cause: the column name in your code does not match the file, often due to a trailing space or different casing in the header. Fix: run print(df.columns.tolist()) and copy the exact name, or normalize headers with df.columns = df.columns.str.strip().
  4. Cleaned file shows garbled symbols in Excel — Cause: Excel expects a BOM to read UTF-8 correctly. Fix: write with df.to_csv("clean_output.csv", index=False, encoding="utf-8-sig") so Excel renders accents and emoji properly.

If you hit an error that is not on this list, the fastest route is to read the failure report Python prints rather than guessing; Read a Python Traceback in Five Minutes walks through how to find the offending line.

When to use this vs. alternatives

  • Use this Pandas script when your data fits in memory (up to a few hundred thousand rows on a normal laptop) and you want full control over each cleaning rule. This is the right default for almost every creator, marketer, or founder project.
  • Reach for a database query instead when the file is too large to load at once or already lives in a SQL system. Filtering and deduplicating in SQL before exporting a smaller, cleaner CSV avoids loading the whole thing into Python.
  • Skip dedicated cleaning code when the work is genuinely one-off and tiny, say a dozen rows you can fix by hand in a spreadsheet. A script pays off when you will repeat the job or need the result to be exactly reproducible. If this cleaning is part of a recurring pipeline, wrap it in a scheduled run as shown in Automating Repetitive Tasks with Python.

Back to Data Cleaning for AI.

Frequently asked questions

Why does messy CSV data break AI prompts and embeddings?

Inconsistent casing, stray whitespace, and missing values make a model treat identical concepts as different inputs. That splits them into different tokens, wastes context window, and lowers embedding quality. Cleaning the data first makes results consistent and cheaper.

Do I need to remove every row with a missing value?

No. Only drop rows that are missing the fields your AI step actually needs, such as the text column you send to the model. For other columns, filling a placeholder like 'unknown' usually keeps more usable data.

Why am I getting an AttributeError when I run .str.lower() on a column?

Pandas only allows .str methods on text columns. If the column mixes numbers and text, cast it first with .astype(str). The error means at least one value is not a string.

How do I keep accented characters and emoji intact while cleaning?

Read and write the file with utf-8 encoding (use encoding='utf-8-sig' for Excel exports). Avoid forcing ASCII. Pandas preserves Unicode text by default as long as the encoding matches the source file.

Is it safe to overwrite my original CSV with the cleaned version?

Write to a new filename instead, such as clean_output.csv. Keeping the raw file means you can re-run the script with different settings if you spot a mistake, without losing the source data.