How-to
Automating Reporting WorkflowsDeep dive

Build a Warranty Claims Report

Analyse warranty claims with pandas the way quality and finance teams need: claims per thousand units sold by product and by month of sale, how long after sale products fail, the failure modes that cost most, a production batch that fails more often than the rest, and an estimate of the claims still to come on units under warranty to support the warranty provision — written to an Excel report.

A warranty claim is a defect that escaped the factory and found the customer. Each one costs a repair or replacement, and the pattern of claims answers two questions that matter to different people. Quality engineers want to know what is failing, when after sale it fails, and whether a particular production batch is worse than the rest. Finance wants to know how much the claims still to come on products already sold will cost, because that figure is the warranty provision on the balance sheet. Both depend on getting the denominator right: a count of claims means little without the number of units that could have produced them, and units sold last month have not had time to fail yet. This page builds a warranty report with pandas from sales and claims data: claim rates per thousand units by product and month of sale, the time from sale to failure, failure modes ranked by cost, a check for a bad batch, and an estimate of future claims for the provision. It belongs to Building Finance and Operations Reports.

Prerequisites

Bash
pip install pandas openpyxl

Two inputs: units sold per product and month, with the production batch they came from, from January 2025 to September 2026; and warranty claims with the product, batch, month of sale, month of claim, failure mode and cost. The warranty runs for 24 months from sale.

Step 1 — Claim rates by product, on the right denominator

Python
import pandas as pd

sold = pd.read_excel("units_sold.xlsx")
claims = pd.read_excel("warranty_claims.xlsx")
for df, cols in ((sold, ["Sale month"]), (claims, ["Sale month", "Claim month"])):
    for c in cols:
        df[c] = pd.PeriodIndex(df[c], freq="M")
asof = pd.Period("2026-09", "M")

mature = sold[(asof - sold["Sale month"]).apply(lambda d: d.n) >= 12]           # sold at least 12 months ago
units = mature.groupby("Product")["Units sold"].sum()
early = claims[claims["Sale month"].isin(mature["Sale month"]) & ((claims["Claim month"] - claims["Sale month"]).apply(lambda d: d.n) < 12)]
rate = (early.groupby("Product").size() / units * 1000).round(1).rename("Claims per 1,000 in first 12 months")
cost_per_unit = (early.groupby("Product")["Cost"].sum() / units).round(3).rename("Claim cost per unit sold")
print(pd.concat([units.rename("Units"), rate, cost_per_unit], axis=1).to_string())

The obvious rate — claims this year divided by units sold this year — is wrong in the same way as returns rates: claims arrive months after sale, so recent sales have not yet had time to fail. A fair comparison counts claims made within the same length of time after sale for every unit. Here that window is the first twelve months, and only units sold at least twelve months ago are included, so every unit in the denominator has had its full twelve months. Measured that way, the blender fails about twice as often as the kettle in its first year, and its claims cost more each. The claim cost per unit sold is the figure product management needs when it sets prices: it is the warranty cost built into every unit, whether or not that unit fails.

The wrong rate and the right one Dividing this year's claims by this year's sales mixes claims on old sales with units too new to have failed. Counting claims within the first twelve months after sale, for units sold at least twelve months ago, compares every unit over the same exposure. Claims this year ÷ sales this year claims on older sales new units not yet exposed rate rises and falls with sales Claims in first 12 months ÷ units sold 12+ months ago same exposure for every unit comparable across products basis for cost per unit

Step 2 — When do products fail?

Python
claims["Months to failure"] = (claims["Claim month"] - claims["Sale month"]).apply(lambda d: d.n)
old = claims[(asof - claims["Sale month"]).apply(lambda d: d.n) >= 18]          # cohorts observed for 18+ months
counts = old.groupby(["Product", "Months to failure"]).size().unstack(fill_value=0).reindex(columns=range(24), fill_value=0)
curve = counts.cumsum(axis=1).div(counts.sum(axis=1), axis=0)
print(curve.loc[:, [0, 2, 5, 11, 17, 23]].round(2).to_string())

The share of each product's claims that arrive within a given number of months of sale is the claims curve. It is built only from sales months that have been in the field for at least eighteen months; including recent cohorts would bias it towards early failures, because their later claims have not happened yet. Most claims come early — about two fifths within the first three months of sale and two thirds within six — and the curve flattens towards the end of the warranty, with over 90% of claims made within the first year. Its shape matters for two reasons. A failure mode concentrated in the first weeks suggests a manufacturing or assembly defect, while one that rises late suggests wear. And the curve is the basis for estimating claims still to come: if 92% of a product's claims usually arrive within the first twelve months, a batch sold a year ago has less than a tenth of its claims still ahead, while last month's sales have most of theirs to come.

Step 3 — Failure modes and a suspect batch

Python
modes = claims.groupby(["Product", "Failure mode"]).agg(claims=("Claim", "size"), cost=("Cost", "sum")).sort_values("cost", ascending=False)
print(modes.round(0).head(6).to_string())

blender = claims[claims["Product"] == "Blender B3"]
early_b = blender[blender["Months to failure"] < 3]
per_batch = (early_b.groupby("Batch").size() / sold[sold["Product"] == "Blender B3"].set_index("Batch")["Units sold"] * 1000).dropna()
typical = per_batch.median()
suspects = per_batch[per_batch > 2 * typical].round(1)
print(f"blender claims per 1,000 in first 3 months: median {typical:.1f}; suspect batches: {suspects.to_dict()}")
print(blender[blender["Batch"].isin(suspects.index)]["Failure mode"].value_counts().to_dict())

Ranking failure modes by total cost points the engineers at the defects that matter most — for the blender, the motor. The batch check compares each production batch's early claim rate, over the first three months after sale so that recent batches can be included, with the typical rate for that product. Two blender batches, from September and October 2025, have 51.8 and 60.3 early claims per thousand units against a median of 25 — and their claims are dominated by seal leaks rather than the motor failures seen elsewhere. That pattern — a specific mode, in specific batches — points to a component or process change at the time, which quality can trace through the batch records, and it might justify contacting customers who bought from those batches before more units fail.

Early claim rates by blender batch Blender batches typically have about 25 claims per thousand units in the first three months after sale. The September 2025 batch has 51.8 and the October 2025 batch 60.3, two to two and a half times the usual rate, and their claims are mainly seal leaks. A B C D 1 Batch Early claims per 1 0 Main failure mode 2 Other blender batches ≈ 25 (median) Motor 3 B3-2509 51.8 Seal leak 4 B3-2510 60.3 Seal leak

Step 4 — Claims still to come, for the provision

Python
def expected_remaining(product):
    s = sold[sold["Product"] == product].set_index("Sale month")["Units sold"]
    rate24 = claims[claims["Product"] == product].groupby("Sale month").size().reindex(s.index, fill_value=0) / s
    lifetime_rate = rate24[(asof - rate24.index).map(lambda d: d.n) >= 20].mean()      # nearly complete cohorts
    share_by_age = curve.loc[product]
    rows = []
    for month, units_sold in s.items():
        age = (asof - month).n
        if age >= 24:
            continue
        done = share_by_age.get(min(age, 23), 1.0)
        rows.append(units_sold * lifetime_rate * (1 - done))
    avg_cost = claims.loc[claims["Product"] == product, "Cost"].mean()
    return round(sum(rows)), round(sum(rows) * avg_cost, 2)

provision = pd.DataFrame({p: expected_remaining(p) for p in sold["Product"].unique()}, index=["Claims to come", "Estimated cost"]).T
print(provision.to_string())
print("total estimated cost:", provision["Estimated cost"].sum().round(2))

The provision estimate combines three things for each product: a lifetime claim rate per unit, taken from sales cohorts old enough to have had nearly their whole warranty; the claims curve, which says what share of claims a cohort of a given age has already made; and the number of units still under warranty from each month of sale. For each month, the expected remaining claims are units sold × lifetime rate × the share of the curve still to come; summing over months and multiplying by the average claim cost gives the estimated cost. Here the estimate comes to about 1,560 claims still to come, costing £43,621, of which the blender accounts for more than half.

Estimated claims still to come For units still under warranty, the kettle is expected to produce about 319 more claims costing 6,010 pounds, the toaster 569 costing 13,195 pounds and the blender 668 costing 24,416 pounds, a total of 43,621 pounds. A B C 1 Product Claims to come Estimated cost 2 Kettle K1 319 £6,010 3 Toaster T2 569 £13,195 4 Blender B3 668 £24,416 5 Total 1,556 £43,621

This is a simple, explainable method that auditors recognise; it assumes the future follows the past, so a known problem such as the suspect batches justifies an extra amount on top, which should be stated separately.

Common pitfalls

  • Claims divided by sales in the same period. Compare claims over the same time after sale.
  • Including units too new to have failed. Restrict to units with the full observation window.
  • Ranking failure modes by count. Rank by cost; an expensive failure matters more.
  • Batch rates on long windows. Use an early window so recent batches can be compared.
  • A provision that ignores known problems. Add and disclose an amount for identified issues.

Conclusion

A warranty report compares products on claims per thousand units over the same time after sale, using only units old enough to have had that time, and turns the timing of claims into a claims curve. Ranking failure modes by cost directs engineering effort, comparing early claim rates by batch finds production problems, and combining a lifetime claim rate with the curve and the units still under warranty gives an explainable estimate of the claims still to come for the warranty provision.

Frequently asked questions

How do I calculate a warranty claim rate? Divide the claims made within a fixed period after sale, such as twelve months, by the units sold that have been in the field at least that long.

What is a warranty claims curve? The cumulative share of a product's claims that arrive within each number of months after sale.

How do I find a bad production batch from warranty data? Compare each batch's claim rate over the first few months after sale with the typical rate for the product and look for outliers with a distinctive failure mode.

How do I estimate claims still to come for a warranty provision? For each sales month, multiply units sold by the lifetime claim rate and by the share of the claims curve not yet reached, then by the average claim cost.

More on report layouts & templates

More on report layouts & templates →