Skip to main content

6  What performances do donors attend?

The overlap chapter showed that most donor households also attend; this one asks what they attend. Each registration is tagged with whether the registering household has ever given a SUCCEEDED individual gift (“donor household”). Performance attendance is headcount (succeeded ticket attendees — see Methodology), attributed to the registering household; canceled, moved, and rehearsal events are excluded.

Code
import pandas as pd
import plotly.express as px
from hh.io import read_parquet
from hh.viz import style, BLUE, pct
from hh.analytics.productions import match_production, is_excluded, count_succeeded_attendees

regs = read_parquet("processed", "registrations_enriched.parquet")
hh = read_parquet("processed", "households_summary.parquet")

m = regs.merge(hh[["id", "is_donor"]], on="id", how="left")
m["is_donor"] = m["is_donor"].fillna(False)
perf = m[(m["event_majorcat"] == "performance") & ~m["event_name"].map(is_excluded)].copy()
perf["attendees"] = perf["tickets"].map(count_succeeded_attendees)

by_cat = perf.groupby("event_minorcat").apply(
    lambda d: pd.Series(
        {
            "attendees": d["attendees"].sum(),
            "donor attendees": d.loc[d["is_donor"], "attendees"].sum(),
        }
    ),
    include_groups=False,
).astype(int)
by_cat["donor share"] = by_cat["donor attendees"] / by_cat["attendees"]
by_cat = by_cat.reindex(["theater", "music", "opera", "dance", "other"]).dropna()
overall_share = perf.loc[perf["is_donor"], "attendees"].sum() / perf["attendees"].sum()

Donor households account for 69% of all performance attendance. By genre:

Code
fig = px.bar(
    by_cat.reset_index(),
    x="event_minorcat",
    y="donor share",
    color_discrete_sequence=[BLUE],
    custom_data=["donor attendees", "attendees"],
)
fig.update_layout(showlegend=False, xaxis_title="", yaxis_title="Attendance from donor households")
fig.update_yaxes(tickformat=".0%", range=[0, 1])
fig.update_traces(
    hovertemplate="%{y:.0%} (%{customdata[0]:,} of %{customdata[1]:,} attendees)<extra></extra>"
)
style(fig).show()
Figure 6.1: Share of performance attendance from donor households, by genre
Code
by_cat.style.format({"attendees": "{:,}", "donor attendees": "{:,}", "donor share": pct})
  attendees donor attendees donor share
event_minorcat      
theater 13,351 8,510 63.7%
music 6,990 5,400 77.3%
opera 2,772 1,871 67.5%
dance 799 501 62.7%
other 833 677 81.3%

Donor households are the majority of every genre’s audience — highest for music (77%), lowest for dance (63%), with theater at 64%. Selling tickets and cultivating donors are not separate audiences at Hubbard Hall.

6.1 Which offerings drew the most donor attendance?

Attendance from donor households by offering — production titles where the run mapping applies (recurring titles such as Winter Carnival aggregate across their annual runs), otherwise the event’s own name:

Code
perf["offering"] = perf["event_name"].map(lambda n: match_production(n) or n)
g = perf.groupby("offering").agg(
    genre=("event_minorcat", "first"),
    first_year=("starts_on", lambda s: int(s.dt.year.min())),
    attendees=("attendees", "sum"),
)
g["donor attendees"] = perf[perf["is_donor"]].groupby("offering")["attendees"].sum()
g["donor attendees"] = g["donor attendees"].fillna(0).astype(int)
g["donor share"] = g["donor attendees"] / g["attendees"]
top = g.sort_values("donor attendees", ascending=False).head(15)
top.index.name = None
top.style.format(
    {"attendees": "{:,}", "donor attendees": "{:,}", "donor share": pct}
)
  genre first_year attendees donor attendees donor share
Winter Carnival of New Work theater 2015 680 521 76.6%
The Mystery of Edwin Drood theater 2017 866 452 52.2%
The 25th Annual Putnam County Spelling Bee theater 2014 628 451 71.8%
Fun Home theater 2023 634 411 64.8%
Whispering Bones other 2013 491 355 72.3%
The Elixir of Love (opera) opera 2017 465 306 65.8%
The Book Club Play theater 2017 507 300 59.2%
The Crucible theater 2016 519 298 57.4%
Rigoletto (opera) opera 2015 443 297 67.0%
Madama Butterfly (opera) opera 2016 414 274 66.2%
The Velocity of Autumn theater 2018 448 243 54.2%
Bread & Puppet Theater theater 2025 409 238 58.2%
Tartuffe theater 2015 338 238 70.4%
The Glass Menagerie theater 2017 404 234 57.9%
Ondine theater 2013 342 230 67.3%

The list is led by the big ensemble musicals and the summer operas — the same shows that fill the house generally — plus Winter Carnival of New Work, whose audience is the most donor-heavy of any large offering.

Do donor and non-donor households split their activity differently? Registration mix (all event types, registrations rather than headcount):

Code
mix = (
    m.groupby([m["is_donor"].map({True: "Donor households", False: "Non-donor households"}),
               "event_majorcat"], observed=True)
    .size()
    .unstack("event_majorcat")
    .fillna(0)
)
mix = mix.div(mix.sum(axis=1), axis=0)
mix.index.name = None
mix.style.format("{:.0%}")
event_majorcat class community other performance
Donor households 45% 4% 3% 47%
Non-donor households 55% 2% 1% 41%

Non-donor households skew toward classes; donor households split more evenly between classes and performances.