Q&A library Python

How do I clean messy text values in a pandas column?

Python Intermediate cleaningnormalisationstrings

Short answer

Chain the str accessor methods to strip whitespace and normalise case, and strip out stray characters before converting to numbers. Inconsistent whitespace and case are the usual cause of failed joins and duplicate groups.

The short version

Chain .str methods to normalise text: strip to remove stray whitespace, then lower or title to make case consistent. Do this before joining or grouping, because " Leeds " and "LEEDS" are different values to pandas, which silently splits what should be one group and breaks joins.

The standard clean

import pandas as pd

raw = pd.Series(["  Leeds ", "BATH", "york  "])

clean = raw.str.strip().str.title()
print(clean.tolist())   # ['Leeds', 'Bath', 'York']

strip removes leading and trailing whitespace, and title gives consistent capitalisation. Chaining reads as a small pipeline, and both operate on the whole column at once.

Why this matters so much

Untidy text is the most common reason a merge loses rows or a groupby produces two entries for the same thing.

pd.Series(["Leeds", "leeds ", "LEEDS"]).nunique()   # 3, but they are one city

Pandas compares strings exactly, so a trailing space or a capital letter makes two values different. Normalising first collapses them into one, which fixes phantom duplicates and failed joins at a stroke.

Removing unwanted characters

Numbers stored as text often carry separators or symbols that block conversion.

money = pd.Series(["1,200", "3,400"])
numbers = pd.to_numeric(money.str.replace(",", "", regex=False))
print(numbers.tolist())   # [1200, 3400]

Strip the offending characters first, then convert. Passing regex=False makes replace treat the comma literally, which is both faster and safer when you are not using a pattern.

Handling bad values during conversion

If some entries will never parse, errors="coerce" turns them into missing values instead of raising, so one bad row does not stop the whole column.

pd.to_numeric(pd.Series(["1", "2", "bad"]), errors="coerce")   # [1.0, 2.0, NaN]

You can then count or inspect the NaN values to see exactly which rows were unparseable, which is a good way to audit incoming data.

Case-insensitive matching without changing the data

If you need to match without permanently altering the column, use case=False in contains, or compare lowercased copies inside the condition, leaving the stored values as they are.

df[df["city"].str.contains("leeds", case=False, na=False)]

Passing na=False stops missing values raising in the filter, which is a frequent stumble.

Do the cleaning once, early

Normalise text as soon as the data enters your pipeline, not at each point of use. Cleaning in one place means every later join, group and count sees consistent values, whereas cleaning ad hoc leaves some steps working on the raw, inconsistent version.

Enjoying these?

Go from piecing together answers to real fluency.

Our courses take you from looking things up to doing the work with confidence, step by step on real data.

Browse the courses