EduFlow

Section 3 · Cleaning Real Data

Cleaning Text Columns

16 min3 min readPython exercise

Lesson at a glance

Clean text columns with the .str accessor - trim spaces, standardise case, replace characters, split names and search for patterns - and verify the result against the values you expect.

  1. 1The .str accessor
  2. 2Invisible spaces
  3. 3Replace, split, search
  4. 4Prove the column is clean

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Ask the messy export how many countries Cartwheel sells to, and it says 22. The real answer is eight. The other fourteen are the same countries typed as UNITED KINGDOM, united kingdom and so on. Group revenue by country now and the UK’s sales are split across three rows, each one too small. Text cleaning is unglamorous, and it is the difference between a right and a wrong chart.

The .str accessor

A Series of text has a .str attribute that exposes Python’s string methods, applied to every value at once:

Python
import pandas as pd

messy = pd.read_csv("orders_messy.csv")
print(messy["country"].nunique(), "spellings")
print(messy["country"].value_counts().tail(6))

clean = messy["country"].str.strip().str.title()
print(clean.nunique(), "countries")
print(sorted(clean.unique()))

Each .str call returns a new Series, so you chain them: strip the spaces, then normalise the case. 22 spellings become 8, which matches the eight countries Cartwheel actually sells to. Missing values pass through as missing instead of raising an error, which is the main advantage over writing a loop that calls .strip() on each value yourself. Counting distinct values before and after, as the first and third prints do, is the quickest proof that a clean did what you meant.

title() works here because every Cartwheel country is a name made of ordinary words. It would damage values like USA (to Usa) or McDonald (to Mcdonald). When a column has such values, use str.lower() for comparison or a mapping dictionary with map(), as in Lesson 2.5.

Invisible spaces

Thirteen customer names in the export have spaces before or after them. They look identical in a table, so the only way to see them is to measure:

Python
import pandas as pd

messy = pd.read_csv("orders_messy.csv")
padded = messy["customer"] != messy["customer"].str.strip()
print(padded.sum(), "padded names")
print(messy.loc[padded, "customer"].head(3).map(repr).tolist())
messy["customer"] = messy["customer"].str.strip()

repr() shows a string with its quotes, which makes the spaces visible: ' Hassan Hughes '. Untrimmed names break exactly the operations you rely on later: grouping, matching against another table, removing duplicates. Strip every text column you are going to group or join on, even if it looks clean. str.strip() removes more than ordinary spaces: tabs, line breaks and the non-breaking spaces that text copied from web pages and PDFs often carries are all whitespace to Python, so one call handles them all.

Replace, split, search

The export writes the channel as mobile app, while Cartwheel’s clean tables use mobile_app. Matching conventions now saves a failed join in Section 4:

Python
import pandas as pd

messy = pd.read_csv("orders_messy.csv")
messy["channel"] = messy["channel"].str.replace(" ", "_")
print(messy["channel"].value_counts(dropna=False))

names = messy["customer"].str.strip().str.split(" ", expand=True)
names.columns = ["first_name", "last_name"]
print(names.head(3))

coffee = messy["product"].str.contains("coffee", case=False)
print(coffee.sum(), "coffee lines")

str.split(" ", expand=True) returns a DataFrame with one column per piece; without expand, you get a Series of lists, and .str[0] picks the first piece of each. str.contains() returns a boolean mask, ready for filtering as in Lesson 2.4, and case=False ignores capitals.

Plain text or pattern?

str.replace and str.contains can also take regular expressions, a mini-language for text patterns. In pandas 2, str.replace treats the first argument as plain text unless you pass regex=True, and str.contains treats it as a pattern unless you pass regex=False. The difference bites with symbols that have special meaning in patterns, such as $, . and (. Lesson 3.3 shows the $ case, which affects every money column you will ever clean.

Prove the column is clean

Cleaning code that worked on this month’s file may meet a new spelling next month. End with a check that states what clean means:

Python
import pandas as pd

EXPECTED = {"Egypt", "United Arab Emirates", "Saudi Arabia", "Jordan",
            "United Kingdom", "Germany", "United States", "Canada"}

messy = pd.read_csv("orders_messy.csv")
messy["country"] = messy["country"].str.strip().str.title()
unexpected = set(messy["country"]) - EXPECTED
assert not unexpected, f"Unknown countries: {unexpected}"
print("country column OK:", messy["country"].nunique(), "values")

assert condition, message does nothing when the condition is true and stops the script with your message when it is false. Set subtraction lists any cleaned value that is not on the expected list, and the f-string puts them in the error so you know exactly what to fix.

In the exercise you clean all three text columns of the export. Next come the totals: numbers trapped inside strings like $1,234.50.

Key takeaways

  • The .str accessor applies a string method to every value in a column, and missing values stay missing instead of raising errors.
  • str.strip() and a consistent case (str.title(), str.lower()) fix most spelling variants; check them with nunique() before and after.
  • str.replace(old, new) treats old as plain text by default in pandas 2; pass regex=True only when you mean a pattern.
  • str.split(" ", expand=True) turns one text column into several, and str.contains() builds a boolean mask from a pattern.
  • Finish every text clean with a check against the list of values you expect, so a new spelling next month fails loudly.

Further reading

Check your understanding

3 questions

Question 1 of 3

messy["country"] has 22 distinct values for 8 real countries. Which line is the fix you should write?

Question 2 of 3

Why does messy.product.str.contains("Coffee") raise AttributeError: 'function' object has no attribute 'str'?

Question 3 of 3

You clean the country column and then run assert set(clean) <= set(EXPECTED). What is the assertion for?

Practice

Python exercise

Clean the export's text columns

In messy: strip spaces from customer; strip and title-case country; in channel, replace the space with an underscore (mobile app → mobile_app) and fill missing values with "unknown". Add a first_name column holding the first word of the cleaned customer name. Finally set desk_lines to the number of rows whose product contains “desk” in any capitalisation.

Data files: orders_messy.csv

import pandas as pd

messy = pd.read_csv("orders_messy.csv")

# 1. customer: remove leading/trailing spaces

# 2. country: remove spaces and use title case ("UNITED KINGDOM" -> "United Kingdom")

# 3. channel: "mobile app" -> "mobile_app", and missing -> "unknown"

# 4. New column first_name: the first word of the cleaned customer name

# 5. Number of rows whose product name mentions "desk" (any capitalisation)
desk_lines = None
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

(no output)

Checks

  • Customer names have no leading or trailing spaces
  • country holds exactly the eight real countries
  • channel uses web, mobile_app, marketplace and unknown
  • first_name holds one word per row, starting with Daniel
  • desk_lines is 15