Skip to main content
Back to module
Intro to Data Analysis with Python · Pandas in Anger

Cleaning messy data

Reading1h 29mLesson 2 of 4 in this module

What you'll learn

  • Fix types, missing values and duplicates
  • Normalise text columns with the .str accessor
  • Convert dirty text to numbers with pd.to_numeric and errors="coerce"
  • Wrap cleaning steps in a rerunnable function that leaves the raw data untouched

Fix text, types, missing values and duplicates with a rerunnable pandas cleaning function.

Why it matters

Real data is messy. Survey answers have stray spaces, sensor logs contain "ERR", exports duplicate rows, and somebody typed "unknown" into a number column. Cleaning is often most of the work in an analysis, and mistakes here quietly change every result after it. The goal is a clean table produced by code you can rerun, never by hand edits.

Find the problems first

Python
df.info()                  # dtypes and non-null counts
df.isna().sum()            # missing values per column
df.duplicated().sum()      # fully duplicated rows
df["city"].value_counts()  # spelling and spacing variants

Suppose the data looks like this:

   city      temp_c
0  Boston    21.5
1  boston    22.0
2  Denver    ERR
3  Boston    21.5
4  Austin     30.25

Problems: "boston" and " Boston" differ in case and spacing, temp_c is text because of "ERR" (pandas already reads empty cells, NA and n/a as missing, but not arbitrary codes), and row 3 may duplicate row 0 once the city is cleaned.

Fixing text: the .str accessor

String methods on a whole column live under .str:

Python
df["city"] = df["city"].str.strip().str.title()
# " boston " -> "Boston"

Other useful ones: .str.lower(), .str.replace("$", "", regex=False), .str.contains("lab"), .str.split("-").

Fixing types

pd.to_numeric converts text to numbers. With errors="coerce", anything that cannot be converted becomes NaN (missing) instead of raising an error:

Python
df["temp_c"] = pd.to_numeric(df["temp_c"], errors="coerce")

Strip units or symbols first if needed:

Python
df["price"] = pd.to_numeric(df["price"].str.replace("$", "", regex=False), errors="coerce")

For dates, use pd.to_datetime(df["date"], errors="coerce"). Use .astype(int) only when you are sure there are no missing values.

Handling missing values

NaN means "no value". Most pandas statistics skip it (mean() ignores NaN), but you still have to decide what missing means in your data.

Python
df.dropna(subset=["temp_c"])        # drop rows missing temp_c
df["temp_c"].fillna(0)              # fill with a value (rarely right for measurements)
df["temp_c"].fillna(df["temp_c"].median())   # fill with the median

Rules of thumb:

  • Drop when the row cannot be used without that value and there are few such rows.
  • Fill when a sensible value exists, such as 0 for "no sales recorded", and say so in your notes.
  • Keep as NaN when later steps can skip it and dropping would throw away useful columns.
  • Count before and after. Report how many rows you removed and why.

Removing duplicates

Python
df = df.drop_duplicates()                     # rows identical in every column
df = df.drop_duplicates(subset=["order_id"])  # one row per order_id (keeps the first)

Clean text before removing duplicates, otherwise "Boston" and "boston " look different and survive.

Putting it in a function

Wrap the steps in a function that returns a new DataFrame, so the raw data stays untouched and you can rerun the cleaning:

Python
def clean_temps(raw):
    df = raw.copy()
    df["city"] = df["city"].str.strip().str.title()
    df["temp_c"] = pd.to_numeric(df["temp_c"], errors="coerce")
    df = df.dropna(subset=["temp_c"])
    df = df.drop_duplicates()
    return df.reset_index(drop=True)

reset_index(drop=True) renumbers rows 0, 1, 2, ... after rows were removed.

Common mistakes

  • Editing the original in place without meaning to. Start with df = raw.copy().
  • Assigning to a filtered slice. df[df["x"] > 0]["y"] = 1 may not change df. Use df.loc[df["x"] > 0, "y"] = 1.
  • Filling measurements with 0. A missing temperature is not 0 degrees.
  • Deduplicating before normalising text. Near-duplicates survive.
  • Silently dropping rows. Always know how many rows you lost.

Quick recap

  • Inspect with info, isna().sum(), duplicated().sum() and value_counts().
  • .str.strip().str.title() normalises text; pd.to_numeric(..., errors="coerce") fixes numbers.
  • Decide per column whether to drop, fill or keep missing values, and record the choice.
  • Normalise text first, then drop_duplicates, inside a function that returns a copy.

Try it

In the Do tab you will write clean(df) for a messy city-temperature table: fix the city names, convert temperatures, drop unusable rows and remove duplicates.

Key terms (5)

.str accessor
The pandas attribute that applies string methods such as strip, title and replace to every value in a column.
pd.to_numeric
A pandas function that converts values to numbers; with errors="coerce" anything unconvertible becomes NaN.
NaN
The marker pandas uses for a missing value; most statistics such as mean() skip it.
drop_duplicates
A pandas method that removes repeated rows, comparing all columns or only those given in subset.
reset_index
A pandas method that renumbers rows 0, 1, 2 and so on; drop=True discards the old labels.

Recap

  • Inspect first with info, isna().sum(), duplicated().sum() and value_counts().
  • .str.strip().str.title() normalises text; pd.to_numeric(..., errors="coerce") turns bad values into NaN.
  • Decide per column whether to drop, fill or keep missing values, and record how many rows changed.
  • Normalise text before drop_duplicates so near-duplicates are caught.
  • Clean inside a function that starts from a copy and returns a new DataFrame.
Also in this lesson: