Should I test that?

Should I test data science code?

Verdict

Yes

Yes, give each data science function that a recurring report reruns one test on a small hand-built table whose expected result you worked out by hand.

Why

Yes, test the data science code that runs again, with one test per function that cleans, joins or aggregates data. For pandas functions behind a weekly report that colleagues read, Blast radius is internal and Change frequency is regularly, because new questions add columns about once a month. Detectability is eventually: a join that doubles rows gives a plausible total and raises no error. Reversibility is with-effort, because you rerun the pipeline and reissue the report. Test cost is moderate, because building a small input table and working out its output by hand takes about an hour, and rule R12 gives Test minimally.

When the decision changes
WhenDecisionWhy
The notebook explores a dataset for your own questions, and nobody else sees the resultsDo not test the notebook; look at each result and delete the notebook when you are doneBlast radius falls to none: a wrong number misleads only its author
The analysis answers one question for a stakeholder, and the code will not run againDo not test; before you send the result, check its totals against an independent number such as the finance ledgerChange frequency falls to once: a test in the suite would never run again
The output feeds a customer-facing feature, such as recommendations or a customer dashboardTest the main path and the likeliest edge cases, such as duplicate keys and missing values, in CIBlast radius rises to users: customers see the wrong data
The data science code sets prices, discounts or credit limitsTest mandatory: cover every known failure mode and the boundary values, and have a second person review the testsBlast radius rises to money and Reversibility to costly: wrong prices end in refunds
The data science code produces figures for a regulatory filingTest mandatory, and keep the test evidence that the regulator asks forBlast radius rises to safety-or-legal: a wrong filing breaks a law
The main risk is in the incoming data: a source adds nulls, renames a column or changes unitsTest the incoming data differently: validate it on every run with schema and range checks, and stop the run on a failed checkTest cost rises to heavy: a unit test needs a fixture for every way the source can change, and nobody can list them all

What breaks if you don't test

The common failures in pandas code raise no error. A merge on a key that repeats in the lookup table multiplies rows. A groupby drops rows whose key is missing, because dropna defaults to True. Both failures give believable numbers, so the report goes out. Someone notices weeks later, when a total disagrees with the finance system, after the numbers have been quoted in planning meetings.

What you lose if you over-test

While a question is still open, analysis code changes daily, and a test pinned to each intermediate DataFrame breaks at every new column. Tests for exploratory cells cost an hour each for code you delete the next week. A green suite also proves the code right only on the tables you imagined, and says nothing about the file that arrives on Monday.

How to test

Move the code that runs again from the notebook into a module, and run its unit tests with pytest in CI:

  1. For each function, build an input table of about five rows.
  2. Work out the expected output by hand, not by running the function, and compare with assert_frame_equal.
  3. Pass validate="many_to_one" to each merge with a lookup table, so a repeated key raises an error.
  4. When a bug appears, add a regression test with the row that caused it.

For the incoming data, run schema checks with pandera on every load.

When the answer changes

  • Customers see the output: test the edge cases too. If it sets a price, tests become mandatory.
  • Most failures come from the source data: validate each load instead of adding unit tests.
  • The analysis is for you alone or runs once: check the totals against an independent number instead.

Real incident + Code example

The store that was counted twice

On a retail analytics project I worked on, a weekly revenue report joined orders to a store table on store_id. When a store moved region, someone added a second row for it instead of editing the first. The untested join doubled that store's orders, and the north region read 4% high for five weeks, until a finance analyst compared the report with the ledger. The fix took one argument and one regression test:

import pandas as pd
import pytest

def revenue_by_region(orders, stores):
    joined = orders.merge(stores, on="store_id", how="left", validate="many_to_one")
    return joined.groupby("region", as_index=False)["amount"].sum()

def test_sums_each_order_once():
    orders = pd.DataFrame({"store_id": [1, 1, 2], "amount": [10.0, 5.0, 7.0]})
    stores = pd.DataFrame({"store_id": [1, 2], "region": ["north", "south"]})
    expected = pd.DataFrame({"region": ["north", "south"], "amount": [15.0, 7.0]})
    pd.testing.assert_frame_equal(revenue_by_region(orders, stores), expected)

def test_store_listed_twice_raises():  # regression test for the doubled store
    orders = pd.DataFrame({"store_id": [1], "amount": [10.0]})
    stores = pd.DataFrame({"store_id": [1, 1], "region": ["north", "west"]})
    with pytest.raises(pd.errors.MergeError):
        revenue_by_region(orders, stores)

The expected totals come from adding the amounts by hand. Without validate, the second test fails, because the join returns two rows for the one order and counts 10.0 in both regions.

FAQ

Should data scientists write unit tests?

Yes, data scientists should write unit tests for the functions that a recurring report or model reruns, one test per function on a small hand-built table. Notebooks whose results only you see and analyses that will not run again do not need them; check their totals against an independent number instead.

How do I unit test pandas code?

Build an input DataFrame of about five rows, work out the expected output by hand, and compare with pandas.testing.assert_frame_equal. When a bug appears, add the row that caused it.

Do unit tests replace data validation?

No, unit tests check your code on tables you built, while data validation checks each real load; a recurring pipeline needs both. Schema and range checks with a library such as pandera catch a source that renames a column or changes units.