← Teacher guide

Clean it, test it

Students clean a messy survey with a function that returns a number or None, check it with assert test cases, then tidy genres, skip repeated rows and chart the cleaned genres.

Data detective · Charts and checking · about 50 minutes · Open the student lesson

Classroom resources

Load these into Waypoints Prepare: choose Load URL and paste the copied address, or download the file and import it. In Prepare, the worksheet's Teacher edition shows the answers; the student copy hides them.

Students will be able to

Timing

Predict and Run 10 min · Investigate 15 min · Modify 15 min · Make 10 min

Before class

Read the unit primer first. survey.csv is made up (no real students) with deliberate mess: capitals and spaces in genres, a blank genre, a blank hours answer, "three", -1, 400 (a week has 168 hours) and a repeated row. clean_hours returns a number, or None ("no value") when the answer cannot be used; hours is None checks for it (is is the usual way to compare with None). try / except ValueError is introduced here in one sentence (the Predict): try a line, and if it causes that error, run the except lines instead of stopping. Deeper Python's errors lesson goes further. Test cases are assert lines: silent when true, and the program stops with AssertionError when false. The bug task joins the two range checks with and, which can never be true; the tests catch it. Talk about honesty: a report says how many answers were used.

Running the lesson

  1. Predict: float() on each of the four answers, on the board.
  2. Run: have students break a test on purpose (168 to 16) and read the AssertionError.
  3. Investigate: ask for a number that is below 0 and above 168 at the same time.
  4. Modify: write the test case before the function, then make it pass.
  5. Make: compare the chart with the raw file: what would the chart show without clean_genre?

The hard part (on purpose)

Writing a test case before the fix. When a student is stuck, ask what the function should give for the failing input, and have them write that assert first.

When students get stuck

What you will seeA question to askThe fix
AssertionError on a test line.What does the function give for that input, and what does the test expect?Print clean_hours("400") to see what it gives, then fix the function, not the test.
ZeroDivisionError at the average.Did any row get counted?Check that count goes up in the else, for the rows that are kept.
The repeated student is still counted twice.When is the student added to seen?seen.append(...) when a row is used, and check if row["student"] in seen first.

Answers to the checks

  1. Predict: What total does it print at the end?
    9.5. float("") and float("three") both cause a ValueError, so except skips them; 5 + 4.5 is 9.5.
  2. What gets skipped: Which hours answers does clean_hours turn into None?
    The blank, "three", "-1" and "400"
    Blank and "three" are not numbers (ValueError). -1 and 400 are numbers, but impossible: a week has 168 hours.
  3. What assert does: What does assert clean_hours("three") is None do when clean_hours works?
    Nothing: the program carries on
    assert is silent when its test is true. It stops the program with AssertionError when the test is false, so a broken function is caught straight away.
  4. Spot the bug: The tests stop with AssertionError on the line that checks "400". Click the line that is wrong.
    line 6
    No number is below 0 and above 168 at the same time, so the if is never true. Either is enough to reject it: hours < 0 or hours > 168.
  5. Where this leads: Your cleaning skipped 4 of 21 answers. What should a report about the survey say?
    The average, and that it comes from 17 usable answers out of 21
    Honest data work says what was left out and why. Turning a blank into 0 would change the average.

Solutions

One possible solution for each task. Other correct programs are fine.

Modify › Tidy genres

import csv


def clean_hours(text):
    try:
        hours = float(text)
    except ValueError:
        return None
    if hours < 0 or hours > 168:
        return None
    return hours


def clean_genre(text):
    return text.strip().lower()


assert clean_hours("5") == 5
assert clean_hours("4.5") == 4.5
assert clean_hours("three") is None
assert clean_hours("") is None
assert clean_hours("-1") is None
assert clean_genre(" Hip Hop ") == "hip hop"
assert clean_genre("POP") == "pop"
print("All tests pass")

with open("survey.csv") as f:
    rows = list(csv.DictReader(f))

for row in rows:
    print(row["student"] + ": " + clean_genre(row["genre"]))

Modify › Repeated rows

import csv


def clean_hours(text):
    try:
        hours = float(text)
    except ValueError:
        return None
    if hours < 0 or hours > 168:
        return None
    return hours


assert clean_hours("5") == 5
assert clean_hours("4.5") == 4.5
assert clean_hours("three") is None
assert clean_hours("") is None
assert clean_hours("-1") is None
print("All tests pass")

with open("survey.csv") as f:
    rows = list(csv.DictReader(f))

total = 0
count = 0
seen = []
for row in rows:
    hours = clean_hours(row["hours"])
    if row["student"] in seen:
        print("Skipped " + row["student"] + ": a repeat")
    elif hours is None:
        print("Skipped " + row["student"] + ": '" + row["hours"] + "'")
    else:
        total = total + hours
        count = count + 1
        seen.append(row["student"])

print("Average: " + str(round(total / count, 1)) + " hours of music a week, from " + str(count) + " answers")

the class chart

import csv


def clean_genre(text):
    return text.strip().lower()


assert clean_genre(" Hip Hop ") == "hip hop"
assert clean_genre("POP") == "pop"
print("All tests pass")

with open("survey.csv") as f:
    rows = list(csv.DictReader(f))

counts = {}
for row in rows:
    genre = clean_genre(row["genre"])
    if genre != "":
        if genre in counts:
            counts[genre] = counts[genre] + 1
        else:
            counts[genre] = 1

for genre in counts:
    print(genre.ljust(12) + "#" * counts[genre] + " " + str(counts[genre]))

Stretch and support

Stretch: Write clean_year(text) for the year_level column (only 7 to 12 allowed) with test cases, and print the average hours for each year level.

Support: Give students clean_genre written out, and ask them to write the two test cases for it.

Exit ticket

What does a test case such as assert clean_hours("400") is None prove?

That the function rejects an impossible answer; if someone breaks the function later, the program stops on that line and says so.

What each step is for

Where it leads

Next lesson puts the data behind a Gradio app with pandas, which has its own cleaning tools. Test cases come back in every later course.