How far do Hubbard Hall’s donors live from the hall? Distances are straight-line (haversine) from the venue (25 E Main St, Cambridge NY) to each donor household, bucketed into half-open mile bands. Donors whose only address is a PO box are placed at their ZIP’s centroid and flagged “approximate” (excluded from the tight 1-mile count so it stays honest).
Code
import pandas as pd
import plotly.express as px
from hh.io import read_parquet
from hh.viz import style, BLUE, money, pct
households = read_parquet("processed" , "households_summary.parquet" )
donors = households[households["is_donor" ]]
by_band = (
donors.groupby("distance_band" , observed= True )
.agg(
households= ("id" , "count" ),
dollars= ("total_donations" , "sum" ),
mean_gift= ("total_donations" , "mean" ),
median_gift= ("total_donations" , "median" ),
)
.reset_index()
)
by_band["share" ] = by_band["dollars" ] / by_band["dollars" ].sum ()
by_band["cum_share" ] = by_band["dollars" ].cumsum() / by_band["dollars" ].sum ()
by_band.style.format (
{"dollars" : money, "mean_gift" : money, "median_gift" : money, "share" : pct, "cum_share" : pct}
)
0
[0, 1)
164
$119,310
$728
$92
8.2%
8.2%
1
[1, 5)
182
$470,186
$2,583
$140
32.3%
40.5%
2
[5, 10)
292
$199,136
$682
$50
13.7%
54.1%
3
[10, 20)
247
$319,359
$1,293
$25
21.9%
76.1%
4
[20, ∞)
407
$348,883
$857
$30
23.9%
100.0%
Code
fig = px.bar(by_band, x= "distance_band" , y= "dollars" , color_discrete_sequence= [BLUE])
fig.update_layout(
showlegend= False ,
xaxis_title= "Distance from Hubbard Hall (miles)" ,
yaxis_title= "Donated $" ,
)
fig.update_yaxes(tickprefix= "$" , tickformat= ",.0f" )
fig.update_traces(hovertemplate= "$% {y:,.0f} <extra></extra>" )
style(fig).show()
The tightest band includes some approximate (ZIP-centroid) placements — its centroid share, to read the 1-mile count in context:
Code
prec = (
donors.groupby("distance_band" , observed= True )["geo_precision" ]
.apply (lambda s: (s == "centroid" ).mean())
.to_frame("centroid_share" )
)
prec.style.format ({"centroid_share" : pct})
distance_band
[0, 1)
12.8%
[1, 5)
1.6%
[5, 10)
9.9%
[10, 20)
15.0%
[20, ∞)
14.7%
Has donor geography changed over time?
The same dollars, split into roughly five-year periods and shown as shares within each period , so changes in the mix are visible regardless of overall growth. (This uses gift-level dates, unlike the lifetime household totals above.)
Code
from hh.clean.accounts import clean_accounts
from hh.clean.donations import clean_donations
from hh.analytics.donors import succeeded_individual_gifts
PERIODS = ["2013–2015" , "2016–2020" , "2021–2026" ]
BANDS = ["[0, 1)" , "[1, 5)" , "[5, 10)" , "[10, 20)" , "[20, ∞)" ]
BAND_COLORS = dict (zip (BANDS, ["#b7d3f6" , "#6da7ec" , "#3987e5" , "#184f95" , "#0d366b" ]))
accounts_geo = read_parquet("processed" , "accounts_geocoded.parquet" )
gifts = succeeded_individual_gifts(clean_donations(accounts= clean_accounts()))
gifts["distance_band" ] = gifts["account_id" ].map (
accounts_geo.set_index("account_id" )["distance_band" ]
)
gifts["period" ] = pd.cut(
gifts["donation_date" ].dt.year, bins= [2010 , 2015 , 2020 , 2026 ], labels= PERIODS
)
gd = (
gifts.dropna(subset= ["distance_band" , "period" ])
.groupby(["period" , "distance_band" ], observed= True )["donation_amount" ]
.sum ()
.reset_index()
)
gd["share" ] = gd.groupby("period" , observed= True )["donation_amount" ].transform(
lambda s: s / s.sum ()
)
w10 = {
p: f" { gd[(gd['period' ] == p) & (gd['distance_band' ].isin(BANDS[:3 ]))]['share' ]. sum ():.0%} "
for p in PERIODS
}
Code
fig = px.bar(
gd, x= "period" , y= "share" , color= "distance_band" ,
color_discrete_map= BAND_COLORS, category_orders= {"distance_band" : BANDS},
)
fig.update_layout(
xaxis_title= "Period" , yaxis_title= "Share of donated $" , legend_title= "Distance band"
)
fig.update_yaxes(tickformat= ".0%" )
style(fig).show()
The within-10-mile share of donated dollars was 53% in 2013–2015, 55% in 2016–2020, and 54% in 2021–2026.