Agentic Programming with Pandas
Four small programs. One common goal. A school wants an accurate examination report. Instead of building one giant script, give each program a clearly defined job, connect them with CSV files, and let a coordinator run the whole workflow.
A Principal's Mission: Result Day
One teacher has entered student marks in input/student_marks.csv. Four independently runnable programs must work together. Each program has an input contract, an output contract and a specific responsibility. Their outputs become the inputs of downstream programs.
First understand the input CSV
Open the complete student_marks.csv file. These are fictional records, so students can freely edit a copy and rerun the programs.
| StudentID | Name | Maths | Science | English | Outcome |
|---|---|---|---|---|---|
| S004 | Kabir | 98 | 96 | 99 | PASS |
| S006 | Dev | 40 | 41 | 40 | PASS: 40 counts |
| S008 | Rahul | 70 | 35 | 80 | FAIL: Science |
| S014 | Neha | 39 | 39 | 85 | FAIL: Maths and Science |
| S019 | Vivek | 30 | 32 | 20 | FAIL: All three subjects |
Vital rule: A student is passed only if Maths ≥ 40 AND Science ≥ 40 AND English ≥ 40. A good total or average can never compensate for a failed subject.
import pandas as pd
students = pd.read_csv("input/student_marks.csv")
subjects = ["Maths", "Science", "English"]
passed_mask = students[subjects].ge(40).all(axis=1)
passed = students.loc[passed_mask]
failed = students.loc[~passed_mask]
print("Passed:", len(passed), "Failed:", len(failed))ge(40) means greater than or equal to 40. all(axis=1) means all subject conditions must be true for each row. The opposite mask identifies failures.
Program 1: Gatekeeper: Split the result files
Read the original marks sheet, validate all scores and student IDs, then create exactly two CSV files. A failing student goes in the failed file only once, even if several subject marks are below 40.
Validation matters: an absent score, an impossible score of 150 or duplicate StudentID is invalid input—not an automatic academic failure. The program stops with a clear error rather than silently misclassifying a student.
Open or save the complete program1_split.py source
"""Program 1: read student marks and produce passed.csv + failed.csv.
Rule: a student passes only if EVERY subject mark is >= 40.
Invalid data is rejected rather than being treated as a failure.
"""
from pathlib import Path
import pandas as pd
BASE = Path(__file__).resolve().parent
SUBJECTS = ("Maths", "Science", "English")
COLUMNS = ("StudentID", "Name", "City", *SUBJECTS)
def split_students(source: pd.DataFrame):
"""Return independent passed/failed DataFrames, without mutating source."""
missing = sorted(set(COLUMNS) - set(source.columns))
if missing:
raise ValueError("Missing columns: " + ", ".join(missing))
df = source.loc[:, list(COLUMNS)].copy()
for col in ("StudentID", "Name", "City"):
df[col] = df[col].astype("string").str.strip()
if df[col].isna().any() or df[col].eq("").any():
raise ValueError(f"{col} cannot be missing or blank")
if df["StudentID"].duplicated().any():
raise ValueError("Duplicate StudentID: review the input file")
for subject in SUBJECTS:
df[subject] = pd.to_numeric(df[subject], errors="coerce")
if df[subject].isna().any():
raise ValueError(f"{subject} has missing/non-numeric marks")
if not df[subject].between(0, 100).all():
raise ValueError(f"{subject} must be between 0 and 100")
if not df[subject].mod(1).eq(0).all():
raise ValueError(f"{subject} marks must be whole numbers")
df[subject] = df[subject].astype(int)
passed_mask = df[list(SUBJECTS)].ge(40).all(axis=1)
passed = df.loc[passed_mask].reset_index(drop=True)
failed = df.loc[~passed_mask].reset_index(drop=True)
assert len(passed) + len(failed) == len(df)
return passed, failed
def main(base=None):
base = Path(base) if base is not None else BASE
input_file = base / "input" / "student_marks.csv"
students = pd.read_csv(
input_file, dtype={"StudentID": "string", "Name": "string", "City": "string"}
)
passed, failed = split_students(students)
output = base / "output"
output.mkdir(parents=True, exist_ok=True)
passed.to_csv(output / "passed.csv", index=False)
failed.to_csv(output / "failed.csv", index=False)
print(f"Program 1: {len(students)} students → {len(passed)} passed, {len(failed)} failed")
return passed, failed
if __name__ == "__main__":
main()
Think before running
Can someone with 100 in Maths and 100 in Science but 39 in English pass? Why?
Hint: all three comparisons must return True.
Program 2: Merit Maker: Calculate rank
Read only passed.csv, calculate Total, Percentage and Rank, then create meritlist.csv. Failed students must never reach this file.
Rank policy: higher totals rank first. Tied totals receive the same competition rank (1, 2, 2, 4). For consistent display order between tied students, sort alphabetically by name and then StudentID.
The sample deliberately includes Lata and Meera, both scoring 262 with Rank 6. This makes tie handling visible, not merely theoretical.
Open or save the complete program2_merit.py source
"""Program 2: read passed.csv, calculate scores and publish meritlist.csv.
Tied totals receive the same competition rank (1, 2, 2, 4).
Names and student IDs resolve display order within a tied rank.
"""
from pathlib import Path
import pandas as pd
BASE = Path(__file__).resolve().parent
SUBJECTS = ("Maths", "Science", "English")
def build_meritlist(source: pd.DataFrame) -> pd.DataFrame:
required = {"StudentID", "Name", *SUBJECTS}
if missing := sorted(required - set(source.columns)):
raise ValueError("passed.csv is missing: " + ", ".join(missing))
df = source.copy()
if df["StudentID"].isna().any() or df["StudentID"].duplicated().any():
raise ValueError("passed.csv has invalid StudentID values")
for subject in SUBJECTS:
df[subject] = pd.to_numeric(df[subject], errors="coerce")
if df[subject].isna().any() or not df[subject].between(40, 100).all():
raise ValueError("passed.csv contains incomplete or failing marks")
df["Total"] = df[list(SUBJECTS)].sum(axis=1)
df["Percentage"] = (df["Total"] / len(SUBJECTS)).round(2)
df["Rank"] = df["Total"].rank(ascending=False, method="min").astype(int)
return df.sort_values(
["Total", "Name", "StudentID"],
ascending=[False, True, True], kind="mergesort",
).reset_index(drop=True)
def main(base=None):
base = Path(base) if base is not None else BASE
passed = pd.read_csv(
base / "output" / "passed.csv",
dtype={"StudentID": "string"},
)
merit = build_meritlist(passed)
merit.to_csv(base / "output" / "meritlist.csv", index=False)
print(f"Program 2: {len(merit)} passed students → meritlist.csv")
return merit
if __name__ == "__main__":
main()
Think before running
What is the merit rank of the student immediately after two people tie at rank 6?
Competition ranking skips the following rank; the next rank is 8.
Program 3: Topper Finder: Select exactly ten
Read meritlist.csv and create top_ten.csv. Never calculate rankings again from the original marks file.
Using head(10) after a validated, deterministic sort gives ten rows when there are at least ten successful students. If several people tie at the tenth position, the tie-breaking order is Name, then StudentID; this exercise deliberately chooses exactly ten, rather than including all candidates tied for tenth.
Here Sahil and Uma share Rank 10 and both have 251 marks. Sahil appears in top_ten.csv, Uma remains on the full merit list.
Open or save the complete program3_top_ten.py source
"""Program 3: read meritlist.csv and produce exactly ten rows at most.
If the 10th position is tied, we keep exactly 10 records and break
ties alphabetically by Name then StudentID (not all tied candidates).
"""
from pathlib import Path
import pandas as pd
BASE = Path(__file__).resolve().parent
def choose_top_ten(source: pd.DataFrame) -> pd.DataFrame:
required = {"StudentID", "Name", "Total", "Rank"}
if missing := sorted(required - set(source.columns)):
raise ValueError("meritlist.csv is missing: " + ", ".join(missing))
df = source.copy()
if df["StudentID"].isna().any() or df["StudentID"].duplicated().any():
raise ValueError("Invalid meritlist StudentID")
df["Total"] = pd.to_numeric(df["Total"], errors="coerce")
df["Rank"] = pd.to_numeric(df["Rank"], errors="coerce")
if df["Total"].isna().any() or df["Rank"].isna().any():
raise ValueError("Merit rank or total is missing or invalid")
expected_ranks = df["Total"].rank(ascending=False, method="min")
if not df["Rank"].eq(expected_ranks).all():
raise ValueError("Merit ranks are inconsistent with total marks")
return df.sort_values(
["Total", "Name", "StudentID"], ascending=[False, True, True],
kind="mergesort"
).head(10).reset_index(drop=True)
def main(base=None):
base = Path(base) if base is not None else BASE
merit = pd.read_csv(
base / "output" / "meritlist.csv", dtype={"StudentID": "string"}
)
top = choose_top_ten(merit)
top.to_csv(base / "output" / "top_ten.csv", index=False)
print(f"Program 3: top {len(top)} students → top_ten.csv")
return top
if __name__ == "__main__":
main()
Think before running
Would a student with rank 10 always appear in a strictly ten-row list?
Not necessarily: ties at the cutoff must be resolved by the stated display policy.
Program 4: Remedial Analyst: Find weak subjects
Read failed.csv, detect the subject or subjects where each student scored below 40, and create separate CSV files for Maths, Science and English.
Every output includes a Shortfall column, calculated as 40 - subject mark. The student with the largest shortfall appears first. A student failing two subjects is included in both subject files. This helps teachers plan focused revision classes instead of merely publishing a single failure list.
For example, Vivek appears in all three subject files. The sum of subject-failure counts can therefore exceed the number of failed students.
Open or save the complete program4_subject_failures.py source
"""Program 4: read failed.csv and create one CSV per failed subject.
A student failing two or three subjects intentionally appears in two
or three subject CSVs. Each CSV includes shortfall = 40 - subject mark.
"""
from pathlib import Path
import pandas as pd
BASE = Path(__file__).resolve().parent
SUBJECTS = ("Maths", "Science", "English")
def failures_by_subject(source: pd.DataFrame):
missing = sorted(set(SUBJECTS) - set(source.columns))
if missing:
raise ValueError("failed.csv is missing: " + ", ".join(missing))
df = source.copy()
for subject in SUBJECTS:
df[subject] = pd.to_numeric(df[subject], errors="coerce")
if df[subject].isna().any() or not df[subject].between(0, 100).all():
raise ValueError("failed.csv contains invalid marks")
if not df[list(SUBJECTS)].lt(40).any(axis=1).all():
raise ValueError("failed.csv contains a student who passed all subjects")
result = {}
for subject in SUBJECTS:
part = df.loc[df[subject].lt(40)].copy()
part["Shortfall"] = 40 - part[subject]
part = part.sort_values(
["Shortfall", "Name", "StudentID"],
ascending=[False, True, True], kind="mergesort",
).reset_index(drop=True)
result[subject] = part
return result
def main(base=None):
base = Path(base) if base is not None else BASE
failed = pd.read_csv(
base / "output" / "failed.csv", dtype={"StudentID": "string"}
)
reports = failures_by_subject(failed)
for subject, report in reports.items():
name = f"failed-in{subject.lower()}.csv"
report.to_csv(base / "output" / name, index=False)
print(f"Program 4: {subject} → {name} ({len(report)} students)")
return reports
if __name__ == "__main__":
main()
Think before running
If 10 students failed overall, can there be more than 10 entries across the three subject-failure files?
Yes. Count the unique StudentIDs for overall failures; count rows for subject-wise remedial demand.
One command runs the complete workflow
The coordinator knows the dependency graph. It runs Program 1 first; Program 2 can then read passed.csv, and Program 4 can independently read failed.csv; Program 3 waits for Program 2's meritlist.csv. The implementation uses a simple sequential schedule and checks that no records disappeared.
View run_all.py — the coordinator
from program1_split import main as split
from program2_merit import main as merit
from program3_top_ten import main as top_ten
from program4_subject_failures import main as failures
passed, failed = split() # one input → two branches
meritlist = merit() # passed.csv → meritlist.csv
top = top_ten() # meritlist.csv → top_ten.csv
subject_lists = failures() # failed.csv → three CSVs
print("Pipeline complete")python -m pip install pandas
cd python/pandas/projects/csv-pipelines/agentic-student-merit
# Run each program separately:
python program1_split.py
python program2_merit.py
python program3_top_ten.py
python program4_subject_failures.py
# Or let the coordinator run all four:
python run_all.py
# Verify the results:
python -m unittest discover -s . -p "test_*.py"A failed upstream stage should stop the workflow before downstream programs consume stale data. In production, add a run ID, checksums and atomic file replacement for stronger protection.
Watch data change from one program to another
Choose an agent-like program and its output. You will see the real committed CSV files side by side. Run the Python scripts locally to regenerate them and compare.
Input CSV
Open input fileOutput CSV
Open output fileThe browser inspector is a read-only demonstration; it does not execute Python or change any student marks.
Downloadable inputs and seven outputs
The exact filenames from this problem are included. These snapshots let students see the expected results before they write code.
Expected output checks
| CSV file | Number of students | Meaning |
|---|---|---|
| passed.csv | 21 | All marks ≥ 40 |
| failed.csv | 10 | At least one mark below 40 |
| meritlist.csv | 21 | Passed students ranked by total marks |
| top_ten.csv | 10 | Strict top ten with stated tie-break |
| failed-inmaths.csv | 5 | Maths below 40 |
| failed-inscience.csv | 5 | Science below 40 |
| failed-inenglish.csv | 7 | English below 40 |
Subject-wise counts total 17 but involve only 10 distinct failed students, because some learners need help in multiple subjects.
Challenges: Turn this into a smarter system
- Beginner: Change the sample so a student scores exactly 40 in every subject. Which file receives them?
- Boundary test: Change one subject mark to 39 while keeping the other two at 100. Does this student remain on the merit list?
- Fairness: Explain why a tied rank does not guarantee appearing in a list limited to exactly ten rows.
- Remedial planning: Add a new output
failed-in-two-or-more-subjects.csvto identify students needing additional attention. - New branch: Create an extra program that produces
city-wise-summary.csvfrom passed.csv. Connect it to the coordinator without modifying Programs 2, 3 or 4. - Reliability: Add a manifest recording every file's input row count, output row count, modification time and checksum.
Show the key idea for the two-subject challenge
import pandas as pd
failed = pd.read_csv("output/failed.csv")
subjects = ["Maths", "Science", "English"]
failure_count = failed[subjects].lt(40).sum(axis=1)
needs_extra_help = failed.loc[failure_count >= 2].copy()
needs_extra_help["SubjectsFailed"] = failure_count[failure_count >= 2]
needs_extra_help.to_csv(
"output/failed-in-two-or-more-subjects.csv", index=False
)
print(needs_extra_help[["Name", "SubjectsFailed"]])From workflow programming to truly agentic systems
What these four programs do
Use a fixed goal, a known sequence of tasks, explicit file contracts, predictable code and auditable outputs. This is a deterministic multi-component workflow.
What an AI agent might add
Observe outputs, choose the next tool or action based on the situation, explain unusual errors, and propose remediation. Any AI decisions must be authorised and checked; an AI must not invent marks, change grades or make unreviewed pass/fail decisions.
Key lesson: divide a difficult goal into small jobs, define inputs and outputs, coordinate dependencies and verify results. This architecture is a useful foundation for agentic software even when no AI model is involved.
Continue with the other multilevel CSV projects or Pandas merging and reshaping.