Working with pandas dataframes
Parts 5 and 6 implemented table traversal, grouping and file ingestion directly. From this point onward we will normally use existing analytical libraries for those established operations. Our own code will describe the question, check the expected data shape, connect library results and name the values used in later output.
Orient yourself in a dataframe
pandas.read_csv reads a CSV file into a DataFrame: a table whose rows share named columns.
import pandas as pd
records = pd.read_csv("collections.csv")
print(records.head())
print(records.columns)
print(records.dtypes)Before transforming unfamiliar data, inspect:
- several raw rows with
head(); columns, including spelling and capitalization;dtypes, especially fields which should be numeric or dates;- missing values with
isna().sum().
This is validation by observation. It does not prove that every row is correct, but it often catches a mistaken delimiter, header or type before that mistake reaches a chart.
Read a pandas pipeline from left to right
Pandas operations are often written as a method chain. Each method receives the object returned by the preceding method and returns the value used by the next method:
finnish_titles = (
records[records["language"] == "Finnish"]
.sort_values("year")
.head(10)
)Read this from top to bottom:
- select rows whose language is Finnish;
- sort the selected dataframe by year; and
- keep its first ten rows.
The surrounding parentheses allow the expression to continue across lines. The same flow can be written with named intermediate values:
finnish_records = records[records["language"] == "Finnish"]
ordered_records = finnish_records.sort_values("year")
finnish_titles = ordered_records.head(10)Named intermediates are useful while learning or debugging because each stage can be inspected separately. A short chain is useful when the succession of operations remains clear. Most dataframe methods return a new object; do not assume an operation changed the original dataframe unless its documentation says so.
Pandas also provides the method pipe for inserting one of your own functions into such a chain. The current dataframe is passed as the first argument:
def select_language(dataframe, language):
return dataframe[dataframe["language"] == language]
finnish_titles = (
records
.pipe(select_language, "Finnish")
.sort_values("year")
.head(10)
)Here .pipe(select_language, "Finnish") is equivalent to select_language(records, "Finnish"). pipe is most helpful when a project-specific transformation has a clear name and returns an object suitable for the next library operation. It does not remove the need to understand the input and output of each stage.
Select, group and sort
Suppose the dataframe contains collection, language and documents columns. A boolean condition can select rows:
finnish = records[records["language"] == "Finnish"]Column selection states which fields cross into the next stage:
selected = finnish[["collection", "documents"]]Use the library's grouping operation after you understand the result you want:
summary = (
records.groupby("language", as_index=False)
.agg(documents=("documents", "sum"))
.sort_values("documents", ascending=False)
)Inspect summary.columns, its row count and one hand-calculated group. We test our choice of grouping field and aggregation, rather than retesting pandas' implementation of addition.
Prepare plain results for presentation
A table displayed in a notebook, a text report and a static plot can reuse the same verified result. Keep the analytical result separate from the code which displays it:
table_rows = summary.to_dict(orient="records")
metrics = {
"documents": int(records["documents"].sum()),
"collections": int(records["collection"].nunique()),
}
chart_series = []
for row in table_rows:
chart_series.append({"label": row["language"], "value": row["documents"]})
result_data = {
"metrics": metrics,
"table_rows": table_rows,
"chart_series": chart_series,
}The calls to int turn pandas or NumPy scalar values into ordinary Python values. to_dict(orient="records") turns each dataframe row into a named dictionary. These conversions make the output easy to inspect, test and pass to another component.
This example gives its three results descriptive names:
{
"metrics": {"documents": 120, "collections": 3},
"table_rows": [{"language": "Finnish", "documents": 75}],
"chart_series": [{"label": "Finnish", "value": 75}],
}A project may need different results and names. What matters is that each value has a clear role and can be inspected before presentation:
metricscontains headline values;table_rowscontains named records for detailed inspection;chart_seriescontains tidy observations for plotting.
Present verified chart data with seaborn
Seaborn already knows how to turn tidy observations into statistical graphics:
import pandas as pd
def plot_chart(chart_series: list):
import seaborn as sns
chart_data = pd.DataFrame(chart_series)
return sns.barplot(data=chart_data, x="label", y="value")The seaborn import is inside the plotting function because only presentation needs it. Loading the module and using its pandas transformations therefore do not also require the plotting library. The analytical checks should compare chart_series with hand-calculated records. A lightweight presentation check can then verify that plot_chart returns a Matplotlib axes object, uses the promised axis labels and can save a non-empty image. Comparing image pixels would make the test depend on irrelevant rendering details.
Before plotting, you can also print metrics, table_rows and chart_series. This gives direct evidence about the values passed to the presentation library and helps distinguish a transformation problem from a plotting problem.
When reviewing agent-written analysis, trace one value through five stages:
- Ingest: which file and dataframe column supplied it?
- Validate: where did you inspect its type, missingness or example value?
- Transform: which selection or derived column changed it?
- Summarize: which grouping or calculation produced the result?
- Present: which
metrics,table_rowsorchart_seriesfield exposes it?
The following exercises repeat this trace with dates, JSON, text enrichment, similarity results and joined submission data.
Return to modification evidence at a larger scale
The opening example contained ordinary lists and three function calls. A library-backed program can have the same structural problem even when every individual dataframe operation is correct.
Suppose the existing flow is:
records = load_records("records.csv")
prepared = prepare_records(records)
metrics = summarize_metrics(prepared)
table_rows = make_table(prepared)
chart_series = make_chart_series(prepared)The requirement is: “Add a minimum-year filter which affects every displayed result.” The agent reports that all three outputs now use the filter and supplies this change:
records = load_records("records.csv")
prepared = prepare_records(records)
metrics = summarize_metrics(prepared)
+active = filter_minimum_year(prepared, minimum_year)
-table_rows = make_table(prepared)
-chart_series = make_chart_series(prepared)
+table_rows = make_table(active)
+chart_series = make_chart_series(active)The new library-backed filter may return exactly the intended rows. The remaining defect is about order and consumers: summarize_metrics runs before active exists, so the metric still summarizes prepared. A check covering only the returned table and chart cannot establish the metric requirement.
Use the five stages to inspect this change:
load_recordsandprepare_recordsestablish the ingested and transformed dataframe.filter_minimum_yearcreates the active transformed dataframe for this request.- All summaries and presentation records which the requirement names should consume that active dataframe.
- The changed checks should inspect each affected plain result, not merely confirm that the interface renders.
The smallest consistent structure establishes active before any consumer:
records = load_records("records.csv")
prepared = prepare_records(records)
active = filter_minimum_year(prepared, minimum_year)
metrics = summarize_metrics(active)
table_rows = make_table(active)
chart_series = make_chart_series(active)The agent can explain or audit this flow. Your added capability is being able to compare that explanation with the actual function arguments, order and returned shapes it cites.
You can check your current points from the blue blob in the bottom-right corner of the page.