Real datasets have named columns, missing rows, and groups — that is pandas territory. This lesson loads a CSV, cleans it, aggregates by group, and plots the result.

Step 1 — Load and look

import pandas as pd

df = pd.read_csv("results.csv")
print(df.head())
print(df.dtypes)

head() shows the first rows, dtypes the column types. A numeric column that loaded as text (usually a stray unit or comma) is caught right here, before it poisons a plot.

Step 2 — Select and filter

Columns come out by name; rows by a boolean condition:

ok = df[df["quality"] == "good"]           # keep the good runs
signal = ok[["time", "signal", "sample"]]  # just the columns you need
print(f"{len(ok)} of {len(df)} rows kept")

Step 3 — Aggregate by group

groupby is the workhorse — mean and spread per sample in one line each:

stats = ok.groupby("sample")["signal"].agg(["mean", "std", "count"])
print(stats)

The printed table is worth keeping in the notebook output: it is the numbers your figure claims, stated exactly.

Step 4 — Plot from the frame

DataFrames plot directly through matplotlib:

import matplotlib.pyplot as plt

stats["mean"].plot(kind="bar", yerr=stats["std"], capsize=4)
plt.ylabel("Mean signal")
plt.xlabel("Sample")
plt.xticks(rotation=0)

yerr=stats["std"] puts the error bars on for free, straight from the aggregation you just printed.

Step 5 — Export or insert

Export / Insert as always — vector PDF or straight into the paper with a \label. The script (or notebook) is saved as the figure’s source, so the whole load → filter → aggregate → plot chain reruns next time the data changes.

Tip: Filter early and loudly. Printing “kept N of M rows” beside every filter is one line of code and settles every “wait, why is that point missing?” question a co-author will ever ask.