“Large” here means a household that gave $1,000 or more in a single year. This chapter tracks how many such households are active each year, how many donors come back year to year, and how many once-active donors have gone quiet. Definitions and the retention method are in Methodology; the size tiers are introduced in How has giving by donation size changed over time?.
Code
import pandas as pdimport plotly.express as pxfrom hh.io import read_parquetfrom hh.clean.accounts import clean_accountsfrom hh.clean.donations import clean_donationsfrom hh.analytics.donors import ( annual_gifts, donor_retention, donors_above_threshold_by_year, size_tier, TIER_LABELS, LARGE_DONOR_MIN,)from hh.viz import style, BLUE, pctag = annual_gifts(clean_donations(accounts=clean_accounts()))hh = read_parquet("processed", "households_summary.parquet")max_year =int(ag["year"].max()) # current, partial yearcomplete_below = [y for y insorted(ag["year"].unique()) if y < max_year]# (a) households reaching the large-donor threshold each yearbig = donors_above_threshold_by_year(ag, min_amount=LARGE_DONOR_MIN)big_plot = big[big["year"].isin(complete_below)]big_ytd =int(big.loc[big["year"] == max_year, "donors"].sum())# (b) year-over-year retention. Drop the row whose "next year" is the partial year.ret = donor_retention(ag)ret_plot = ret[ret["year"] < max_year -1].copy()# (c) lapsed donors: ever gave, but not in the last two complete yearsrecent_complete = [max_year -1, max_year -2]ever = hh[hh["is_donor"]].copy()recent_ids =set(ag.loc[ag["year"].isin(recent_complete), "id"])ever["lapsed"] =~ever["id"].isin(recent_ids)lapsed = ever[ever["lapsed"]]n_lapsed =len(lapsed)lapsed_large =int((lapsed["total_donations"] >= LARGE_DONOR_MIN).sum())lapsed["tier"] = lapsed["total_donations"].map(size_tier)lapsed_by_tier = lapsed.groupby("tier")["id"].count().reindex(TIER_LABELS).fillna(0).astype(int)
Figure 4.1: Households giving $1,000 or more in a year
4.2 Do donors come back?
Of the donors active in one year, the share who also gave the next:
Code
fig = px.line(ret_plot, x="year", y="retention", markers=True)fig.update_traces(line_color=BLUE, marker_color=BLUE, hovertemplate="%{x}: %{y:.0%} returned<extra></extra>")fig.update_layout(xaxis_title="Year (gave in …)", yaxis_title="Share who gave again next year")fig.update_yaxes(tickformat=".0%", range=[0, 1])style(fig).show()
Figure 4.2: Year-over-year donor retention
4.3 Who has gone quiet?
Of the 1,332 households that have ever given, 827 have not given in either of the last two complete years (2024 or 2025). Of those lapsed households, 70 were once large donors (gave $1,000+ in a year at some point). By lifetime giving tier:
2026 is year-to-date, so it is left out of the trends. So far in 2026, 6 households have reached the $1,000+ threshold; the full-year count will be higher. Retention is an upper bound on voluntary churn — a donor who has died also appears as “not retained.”