NOETRION

Choose a topic

LESSON 9 OF 9 · Python & data foundations
DATA WORKFLOW · 5 MIN READ

Python data capstone: audit a CSV and write a defensible summary

Combine CSV reading, validation, grouping, and statistics in a complete small project with a reconciliation check.

Learning lesson · Reviewed October 2, 2026. Allow additional time for the practice. Prerequisites: all earlier Python & data lessons. Python 3; standard library only. This is a small-data teaching project.

What you will learn
  • Run a complete analysis on a reproducible fictional CSV.
  • Report accepted and rejected counts alongside a summary.
  • State what the result cannot establish.

Your task is to summarize fictional assessment submissions by course. Each record represents one submission, not necessarily one unique learner. Scores must be whole numbers from 0 to 100. A blank score is missing, and every rejected submission must retain a reason. The project succeeds when its result can be traced back to the input and its limitations are clear.

Begin with an inspectable dataset

The embedded CSV lets you run the entire example without creating a file or installing packages. It is deliberately tiny so you can verify the result by hand. There are six records: four valid scores, one missing score, and one score above the allowed range.

import csv
import io
from statistics import mean, median

csv_text = """submission,course,score
S01,Python,80
S02,Python,0
S03,Data,70
S04,Data,
S05,Data,90
S06,Python,105
"""

def parse_score(text):
    cleaned = text.strip()
    if cleaned == "":
        return None, "missing"
    try:
        score = int(cleaned)
    except ValueError:
        return None, "not an integer"
    if score < 0 or score > 100:
        return None, "outside 0-100"
    return score, None

rows = list(csv.DictReader(io.StringIO(csv_text)))
groups = {}
rejected = []
for row_number, row in enumerate(rows, start=2):
    score, reason = parse_score(row["score"])
    if reason is not None:
        rejected.append((row_number, reason))
        continue
    course = row["course"]
    if course not in groups:
        groups[course] = []
    groups[course].append(score)

accepted_count = sum(len(values) for values in groups.values())
assert accepted_count + len(rejected) == len(rows)
print("Input:", len(rows), "Accepted:", accepted_count,
      "Rejected:", len(rejected))
for course, values in sorted(groups.items()):
    print(f"{course}: n={len(values)}, mean={mean(values):.1f}, "
          f"median={median(values):.1f}")
print("Rejected:", rejected)
# Input: 6 Accepted: 4 Rejected: 2
# Data: n=2, mean=80.0, median=80.0
# Python: n=2, mean=40.0, median=40.0
# Rejected: [(5, 'missing'), (7, 'outside 0-100')]

CSV fields arrive as text, so the parser runs before statistics. The rejection numbers correspond to lines in this simple example with one header line and no multiline fields. For general CSV files, a record number and a physical file line number can differ. The code creates a group only after accepting a score, avoiding an empty list passed into a summary function.

CourseAll submissionsAccepted scoresRejectedMean / median of accepted
Data370, 901 missing80.0 / 80.0
Python380, 01 out of range40.0 / 40.0

Audit the result manually

Data has two valid scores: (70 + 90) / 2 = 80. Python has two: (80 + 0) / 2 = 40. For two observations, each median is also their midpoint. The two rejections explain the gap between six source records and four accepted values. Keeping zero is essential: silently dropping it would change the Python mean to 80.

Read
6 records
Validate
4 accepted + 2 rejected
Group
2 courses
Explain
Counts + limitations
The report includes the records that did not contribute to the means.

Write the report with its scope attached

A defensible summary is: “In this fictional six-submission sample, four scores met the validation rules. The mean among accepted scores was 80 for Data and 40 for Python, with two observations in each group. One submission was missing a score and one exceeded the permitted range. These results describe accepted submissions only; they do not establish course quality or represent all learners.”

That wording links the outcome to the sample, rule, and denominator. A headline such as “Data learners perform twice as well” would add a claim the project did not investigate. The assessments may differ, rejected values may not be random, and repeated learners are not checked.

Adapt to a file carefully

For your own small CSV, replace the embedded reader with the pattern below. Confirm column names, expected encoding, delimiter, and row structure first. This project assumes the documented schema; it is not a general-purpose ingestion system. For broader input, add checks for missing or extra columns, blank course names, duplicate submission IDs, and unexpectedly large files.

# Replace the embedded reader with this for a compatible local file:
with open("scores.csv", encoding="utf-8", newline="") as handle:
    rows = list(csv.DictReader(handle))

Practice: add a valid boundary score

Add S07,Python,100 to the embedded CSV. Predict the new input, accepted, and rejected counts, then calculate the Python mean and median. Keep the Data result unchanged. Explain why an unchanged rejection count is still worth reporting.

Show the worked solution

There are seven inputs, five accepted scores, and two rejected records. Python now has [80, 0, 100]: its mean is 60 and its median is 80. Data remains at 80 for both summaries. The missing and invalid records still limit interpretation even though another accepted observation was added.

Completion checklist

Save the input, validation policy, code, output, and a short limitations paragraph together. Run the boundary checks from the previous lesson when you change the parser. You have completed a small reproducible workflow; the next step is practicing on a dataset whose schema and provenance you can explain.

Sources and further reading

Official sources checked October 2, 2026. Examples, exercises, and rubrics are original Noetrion teaching material; fictional datasets are identified below.

Check your understanding

Six input submissions produce four accepted scores and two rejections. Which report is most defensible?

Choose one answer