Skip to main content
Back to module
Python Fundamentals · Mini Project

Build a grade or sensor-log analyser

Reading48mLesson 1 of 3 in this module

What you'll learn

  • Parse records and report summary statistics
  • Validate each line and skip blank, comment and malformed records
  • Group records by key with a dictionary of lists
  • Format a report in a predictable, sorted order

Parse messy text records, skip bad lines, group by key and report summary statistics.

Why it matters

This lesson pulls the whole course together. Turning raw text records into a clear summary is a task you will meet in almost every technical job: an engineer summarising sensor logs, a lab technician checking instrument output, a teacher totalling grades, a data analyst cleaning an export. The same five steps appear every time: read, parse, validate, group, report.

The problem

A building's sensors write one line per reading:

# date,sensor,value
2026-10-01,temp-a,21.5
2026-10-01,temp-b,19.0
2026-10-02,temp-a,22.5

2026-10-02,temp-b,n/a
2026-10-03,temp-a,20.0
2026-10-03,humid,55
garbage line

Real logs are messy. This one has a comment line, a blank line, a reading of n/a and a broken line. We want a report like:

humid: n=1 min=55.0 max=55.0 mean=55.0
temp-a: n=3 min=20.0 max=22.5 mean=21.3
temp-b: n=1 min=19.0 max=19.0 mean=19.0

The same design works for grades: replace "sensor" with "student" or "assignment" and "value" with "score".

Step 1: parse one line

Start with the smallest piece. A function that takes one line and returns a clean record, or None if the line is not usable:

Python
def parse_line(line):
    line = line.strip()
    if not line or line.startswith("#"):
        return None
    parts = line.split(",")
    if len(parts) != 3:
        return None
    day, sensor, text = parts
    try:
        value = float(text)
    except ValueError:
        return None
    return (day, sensor, value)

Every way a line can be bad is handled explicitly: blank, comment, wrong number of fields, value that is not a number. Returning None for "skip this" keeps the decision in one place.

Test it before moving on:

Python
assert parse_line("2026-10-01,temp-a,21.5") == ("2026-10-01", "temp-a", 21.5)
assert parse_line("# header") is None
assert parse_line("2026-10-02,temp-b,n/a") is None
assert parse_line("garbage line") is None

Step 2: load every line

Python
def load_records(text):
    records = []
    for line in text.splitlines():
        record = parse_line(line)
        if record is not None:
            records.append(record)
    return records

text.splitlines() splits a block of text into lines. If the data were in a file, you would loop over the file object instead, exactly as in the files lesson. Keeping load_records working on a string makes it easy to test, and a one-line wrapper can read a file into that string.

Step 3: group by sensor

This is the dictionary-of-lists pattern from the dictionaries lesson:

Python
def group_by_sensor(records):
    groups = {}
    for day, sensor, value in records:
        groups.setdefault(sensor, []).append(value)
    return groups

groups.setdefault(sensor, []) returns the existing list for that sensor, or creates an empty one first. It is a compact version of the if sensor not in groups: check.

Step 4: summarise each group

Python
import statistics

def summarise(records):
    summary = {}
    for sensor, values in group_by_sensor(records).items():
        summary[sensor] = {
            "count": len(values),
            "min": min(values),
            "max": max(values),
            "mean": statistics.mean(values),
        }
    return summary

The result is a dictionary of dictionaries, easy to test and easy to turn into any output format.

Step 5: format the report

Python
def format_report(summary):
    lines = []
    for sensor in sorted(summary):
        s = summary[sensor]
        lines.append(
            f"{sensor}: n={s['count']} min={s['min']:.1f} "
            f"max={s['max']:.1f} mean={s['mean']:.1f}"
        )
    return "\n".join(lines)

Sorting the sensor names makes the output predictable, which matters for testing and for readers comparing two reports. "\n".join(lines) glues the lines together with newlines between them.

Putting it together

Python
report = format_report(summarise(load_records(LOG_TEXT)))
print(report)

Each function does one job, and the final line reads almost like the plan: load, summarise, format. When something is wrong, you can test each stage separately to see where the data stops looking right.

Extending it

Once the core works, good next steps are:

  • Count what was skipped. Report "3 lines skipped" so bad data does not vanish silently.
  • Flag out-of-range values. Reuse your reading_status function from the conditions lesson.
  • Write the report to a file with with open("report.txt", "w").
  • Group by day as well as sensor, using a tuple (day, sensor) as the dictionary key.
  • Grades version: a CSV of student,assignment,score, with a letter grade per student from their mean.

Common mistakes

  • Writing everything in one big loop. It works for the first version, then becomes impossible to change. Split it into functions.
  • Silently dropping data. Skipping bad lines is right, but count them so you know how much was lost.
  • Testing only the happy path. Include a blank line, a comment, a non-number and a short line in your test data.
  • Unpredictable output order. Sort keys before printing if a person or a test will compare the output.

Quick recap

  • Read, parse, validate, group, report: the shape of most data-summary programs.
  • Parse one line in a function that returns a record or None.
  • Group with a dictionary of lists, then summarise each group.
  • Format the report last, in a predictable order.
  • Test each function on its own with small, deliberately messy inputs.

Try it

The Do tab gives you the log above and the five function names. Implement them so the checks pass, then print the report.

Key terms (5)

parse
To turn raw text into structured values, such as a tuple of date, sensor and float.
validate
To check that a record is usable before keeping it, rejecting blank, comment or malformed lines.
splitlines()
A string method that splits a block of text into a list of lines.
setdefault
A dictionary method that returns the value for a key, inserting a given default first if the key is missing.
happy path
The case where every input is clean; testing only this misses the bugs messy data causes.

Recap

  • Most data-summary programs follow read, parse, validate, group, report.
  • Parse one line in a function that returns a record or None.
  • Group with a dictionary of lists, then summarise each group.
  • Format the report last, sorted so the output is predictable.
  • Test each function on its own with small, deliberately messy inputs.
Also in this lesson: