Understanding Membership Conversion

This analysis updates a previous student project done on 2022-23 ticketing data, focusing on membership trends and conversion. One of the major focuses is to understand what about the experience, namely touchpoints or member benefits, may lead a prospective Member to join.

The updated data contains information about the event, ticket type, membership, and date of visit. Please note that quarterly comparisons including 2026 Q3 reflect a partial quarter of data (although all prior quarters are complete).

Who are the visitors?

Of the 295,910 unique visitors in the dataset, 90.3% arrived without a membership on their first visit. Of the non-members, 890 went on to become members, giving the Barnes Foundation a 0.33% conversion rate among new visitors.

image1 <- knitr::include_graphics("memberblocks.png")

manipulateWidget::staticImage(image1, style = "max-width:100%")
PieData <- Visitation |>
  mutate(PieGroup = case_when(
    FirstVisit & !is.na(MembershipId) ~ "ExistingMember", 
    is.na(MembershipId) & is.na(Converts) ~ "NewVisitor", 
    Converts == "Converted" ~ "NewMember"
  )) |>
  select(CustomerId, PieGroup) |> 
  unique() |>
  filter(!is.na(PieGroup)) |>
  summarise(`Existing members` = sum(str_detect(PieGroup, "Exist")),
            `New visitors` = sum(str_detect(PieGroup, "Visit")), 
            `New members` = sum(str_detect(PieGroup, "NewM"))) |>
  pivot_longer(values_to = "Count", names_to = "Category", col = 1:3) |>
  mutate(prop = Count/sum(Count), 
         ymax = cumsum(prop), 
         ymin = (c(0, head(ymax, n = -1))))

# ggplot(PieData, aes(ymax = ymax, ymin = ymin, xmax = 4, xmin = 3, fill = Category)) +
#  geom_rect(color = "white", linewidth = 0.5) +
# coord_polar(theta = "y") +
#  xlim(2, 4) +
#  theme_void() +
#  scale_fill_manual(values = c("#F8B6A9", "#D64120", "grey")) +
#  labs(fill = "")

Pie1 <- plot_ly(PieData, labels = ~Category, values = ~prop, 
        marker = list(colors = c("#F8B6A9", "#D3D3D3", "#D64120")), 
        hoverinfo = 'label+text',
        text = ~Count, 
        textinfo = 'percent', 
        showlegend = FALSE) |>
  add_pie(hole = 0.5) |>
  layout(
    annotations = list(
  list(
    x = 0.5,
    y = 0.48,
    text = "unique visitors",
    showarrow = FALSE
  ),
  list(
    x = 0.5,
    y = 0.52,
    text = "295, 910",
    showarrow = FALSE, 
    font = list(size = 20)
  )
) 
  )


# add barplot --  need to filter so only first conversion for each new member is counted

FirstConvertedDF <- Visitation |>
  filter(Converts == "Converted") |>
  group_by(CustomerId) |>
  summarise(Date = min(Date), FirstConverted = TRUE, Converts = "Converted")

Visitation <- Visitation |>
  left_join(FirstConvertedDF, by = c("CustomerId", "Date", "Converts"))

Bar1 <- ggplotly(ggplot(Visitation |> 
         select(CustomerId, Date, FirstConverted) |>
         unique() |>
         filter(FirstConverted) |>
           mutate(Year = year(Date)), aes(x = Year)) +
  geom_bar(fill = "#24578C") +
    labs(x = "Year", y = "New Members Gained") +
    theme_minimal()
  )

combineWidgets(Pie1, Bar1, nrow = 1)

Visitors who eventually converted averaged 6.6 visits, compared to the 1.4 visit average for non-converting new visitors. Existing members averaged 3.5 visits.

What Brings People to the Barnes?

Excluding general admission, Free First Sundays are the most attended event by far. First Friday places third, reflecting how wide-reaching free public programs can be, compared to paid events. Highlights Tours rank second, and are one of six education-focused events in this top ten.

Bar3 <- Visitation |> 
  filter(!EventName == "Admission") |>
  filter(!EventName == "Calder Gardens Admission") |>
  mutate(EventsGrouped = str_sub(EventName, end = 20L),
         EventsGrouped = case_when(
           str_detect(EventsGrouped, "Highlights") ~ "Highlights Tours", 
           !is.na(EventsGrouped) ~ EventsGrouped
         ),
         EventCat = case_when(
           str_detect(EventsGrouped, "Member") ~ "Member events", 
           EventsGrouped %in% c("Barnes on the Block ", "First Friday! at the", "Young Professionals", "Toddlerpalooza", "Free First Sundays") ~ "Public programs/events",
           !is.na(EventsGrouped) ~ "Tours & talks" 
         )) |>
  group_by(EventsGrouped, EventCat) |>
  summarize(tickets = sum(Quantity)) |>
  arrange(desc(tickets)) |>
  filter(!(EventsGrouped == "Issue Vouchers"))

ggplotly(ggplot(Bar3[1:10, ] |>
                  mutate(EventsGrouped = case_when(
                    EventsGrouped == "Member Exclusive Hou" ~ "Member Exclusive Hours", 
                    EventsGrouped == "Members-Only Online " ~ "Members-Only Online Talk",
                    EventsGrouped == "First Friday! at the" ~ "First Friday! at the Barnes", 
                    EventsGrouped == "Member Appreciation " ~ "Member Appreciation Talks", 
                    EventsGrouped == "Barnes Highlights To" ~ "Barnes Highlights Tour",
                    EventsGrouped == "Public Spotlight Tou" ~ "Public Spotlight Tours",
                    EventsGrouped == "PreK-12 In-School Po" ~ "PreK-12 In-School Lessons", 
                    !is.na(EventsGrouped) ~ EventsGrouped
                  )), aes(x = tickets, y = fct_reorder(EventsGrouped, tickets), fill = EventCat, text = paste(EventsGrouped, "\n", tickets, "tickets"))) +
  geom_col() +
  theme_minimal() +
  scale_fill_manual(values = c("#D64120", "#8E9935", "#24578C")) +
  labs(x = "Tickets Sold", y = "", fill = "") +
  theme(panel.grid.minor = element_blank(), legend.position = "top", panel.grid.major.y = element_blank()) 
  , tooltip = c("text"))

Who Attends Each Event?

Free public events, such as Barnes on the Block and Free First Sundays, which we know are high-attendance events, are mostly attended by Non-Members. Student Lessons and Gallery Lessons are 100% Non-Member tickets, while events like First Friday and Private Tours are roughly evenly attended by Members and Non-Members.

SideBar1 <- Visitation |>
  mutate(IsMember = ifelse(!is.na(MembershipId), "Member ticket", "Non-Member ticket"), 
         EventName = case_when(
           str_detect(EventName, "Members-Only Online Tour") ~ "Members-Only Online Tour", 
           str_detect(EventName, "Highlights ") ~ "Highlights Tour", 
           str_detect(EventName, "Barnes on the Block") ~ "Barnes on the Block", 
           str_detect(EventName, "Private Tours") ~ "Barnes Private Tours", 
           !is.na(EventName) ~ EventName
         )) |>
  filter(EventName %in% c("Admission", "Free First Sundays", "Highlights Tour", "First Friday! at the Barnes", "Member Exclusive Hours", "Barnes on the Block", "Members-Only Online Tour", "Gallery Lessons", "Barnes Private Tours", "PreK-12 In-School Post-Visit Lessons")) |>
  group_by(EventName, IsMember) |>
  summarise(tickets = sum(Quantity)) |>
  group_by(EventName) |>
  mutate(totalTickets = sum(tickets), 
         ticketprop = round((tickets/totalTickets), 3)*100, totalprop = 100)

ggplotly(ggplot(SideBar1, aes(y = fct_reorder(EventName, totalTickets), x = ticketprop, fill = IsMember, text = paste0(EventName, ": ", ticketprop, "% ", IsMember, "s, ", totalTickets, " total tickets"))) +
  geom_col() +
  theme_minimal() +
  theme(legend.position = "top", panel.grid.major = element_blank()) +
  labs(x = "% of visitors", y = "", fill = "") +
  scale_fill_manual(values = c("#D64120", "#F8B6A9")) 
  , tooltip = "text")

Member Guests

Members recieve guest passes as part of their membership benefits. Of the 28,837 unique Members in the dataset, 65% used at least one guest pass, accounting for 32,911 total guest admissions across all Members.

image3 <- knitr::include_graphics("memberguest.png")

manipulateWidget::staticImage(image3, style = "max-width:100%")

Note: One transaction can cover multiple guests, ticket counts represent the quantity of tickets, not amount of transactions

hist2data <- Visitation |> 
  filter(str_detect(TicketType, "Member Guest")) |>
  mutate(Guests = case_when(
    Quantity == 1 ~ "1 guest", 
    Quantity == 2 ~ "2 guests",
    Quantity == 3 ~ "3 guests",
    Quantity > 3 ~ "4+ guests",
  )) |>
  group_by(Guests) |>
  summarize(count = n()) |>
  filter(!is.na(Guests))

hist2 <- ggplotly(ggplot(hist2data, aes(x = Guests, y = count, text = paste(count, "Members brought", Guests))) +
  geom_col(fill = "#77AFE9") +
  theme_minimal() +
  labs(x = "Guest tickets used", y = "Number of Members") +
  theme(panel.grid.major.x = element_blank()), tooltip = "text")

TS2data <- data.frame(Visitation |>
  filter(str_detect(TicketType, "Member Guest")) |>
  group_by(Quarter = as.character(yearquarter(Date))) |>
  summarise(count = sum(Quantity)) )


TS2 <- plot_ly(TS2data, type = 'scatter', mode = 'markers', hovertemplate = "%{x}<br>%{y} Member guest tickets<extra></extra>", 
        line = list(color = '#77AFE9', width = 2), 
        marker = list(color = "#77AFE9")) |>
  add_trace(x = ~Quarter, y = ~count, line = list(shape = "spline", smoothing = 0.55, width = 3)) |>
  layout(showlegend = FALSE, 
         
         xaxis = list(
           tickmode = "array", 
           tickvals = c("2022 Q1", "2023 Q1", "2024 Q1", "2025 Q1", "2026 Q1"), 
           ticktext = c("2022 Q1", "2023 Q1", "2024 Q1", "2025 Q1", "2026 Q1")
         ))


manipulateWidget::combineWidgets(hist2, TS2, nrow = 1)

The drop in 2026 Q3 is due to it being a partial quarter of data, not a decline in visitation.

What Events do Members Bring Guests To?

Members are most frequently bringing guests for regular gallery-viewing through General Admission or Members-Only Hours, and are almost never bringing guests for talks, lectures, or other educational programming, wih only 1.5% of guest tickets being used on Spotlight Tours.

extra1 <- Visitation |>
  filter(str_detect(TicketType, "Member Guest")) |>
  mutate(EventName = case_when(
           str_detect(EventName, "Members-Only Online Tour") ~ "Members-Only Online Tour", 
           str_detect(EventName, "Highlights ") ~ "Highlights Tour", 
           str_detect(EventName, "Barnes on the Block") ~ "Barnes on the Block", 
           str_detect(EventName, "Private Tours") ~ "Barnes Private Tours", 
           str_detect(EventName, "Lecture") ~ "Lecture", 
           str_detect(EventName, "Curators in") ~ "Curators in Conversation", 
           str_detect(EventName, "First Friday") ~ "First Friday", 
           str_detect(EventName, "ber App") ~ "Member Appreciation Talk", 
           str_detect(EventName, "Public Exh") ~ "Public Exhibition Tours", 
           str_detect(EventName, "Private Exh") ~ "Private Exhibition Tours", 
           str_detect(EventName, "Spotlight") ~ "Public Spotlight Tours",
           !is.na(EventName) ~ EventName
         )) |>
  group_by(EventName) |>
  summarize(GuestTix = sum(Quantity)) |>
  arrange(desc(GuestTix)) |>
  filter(GuestTix > 1)

extra1 |>
  plot_ly(
    labels = ~EventName, values = ~GuestTix, 
    marker = list(colors = c("#77AFE9", "#24578C",  "#F8B6A9", "#8E9935","#DDC144"))
  ) |>
  add_pie(hole = 0.65) |>
  layout(
    legend = list(
      y = -0.1, 
      font = list(size = 9),
      itemsizing = "constant",
      itemwidth = 20
    ))

The Path To Membership

Of the Unique visitors in this dataset, 890 Non-Members eventually became members. The following analyses trace what those new Members experienced, and which touchpoints may have been critical to their decision to become a member.

First-Visit Event & Conversion Rate

For each new (non-member) visitor, their first visit can be identified, to see what share of visitors of that event would eventually convert. Events are included only if they converted at least five new members.

General admission has the highest number of conversions, but due to it being the most common ticket type, Admission has a low overall conversion rate. The event with the highest conversion rate is the Curatorial Preview for Water, Wind and Breath, which converted 6 of 87 attendees.

Bar4 <- Visitation |>
  filter(FirstVisit & Converts == "Will convert") |>
  mutate(EventName = case_when(
           str_detect(EventName, "Members-Only Online Tour") ~ "Members-Only Online Tour", 
           str_detect(EventName, "Highlights ") ~ "Highlights Tour", 
           str_detect(EventName, "Barnes on the Block") ~ "Barnes on the Block", 
           str_detect(EventName, "Private Tours") ~ "Barnes Private Tours", 
           !is.na(EventName) ~ EventName
         )) |>
  group_by(EventName) |>
  summarise(WillConvert = n()) |>
  arrange(desc(WillConvert))

totalAttendance <- Visitation |>
  mutate(EventName = case_when(
           str_detect(EventName, "Members-Only Online Tour") ~ "Members-Only Online Tour", 
           str_detect(EventName, "Highlights ") ~ "Highlights Tour", 
           str_detect(EventName, "Barnes on the Block") ~ "Barnes on the Block", 
           str_detect(EventName, "Private Tours") ~ "Barnes Private Tours", 
           !is.na(EventName) ~ EventName
         )) |>
  group_by(EventName) |>
  summarise(totalCount = n())

Bar4 <- Bar4 |>
  left_join(totalAttendance) |>
  mutate(conversionRate = round(WillConvert/totalCount, 3)*100, 
         conversionGroup = case_when(
           conversionRate >=5 ~ "5% or higher",
           conversionRate >=1 & conversionRate < 5 ~ "1-5%",
           conversionRate < 1 ~ "less than 1%"
         )) |>
  filter(WillConvert >= 5) |>
  arrange(desc(WillConvert))
  

ggplotly(ggplot(Bar4 |>
         filter(!(EventName %in% c("Member Appreciation Talk: Thom Collins on Calder Gardens (online)", "Calder Gardens Admission"))), aes(x = WillConvert, y = fct_reorder(EventName, WillConvert), fill = factor(conversionGroup, levels = c("less than 1%", "1-5%", "5% or higher")), text = paste0(WillConvert, " conversions, ", conversionRate, "% conversion rate"))) +
  geom_col() +
  theme_minimal() +
  theme(legend.position = "top", panel.grid.major = element_blank()) +
  labs(y = "", x = "Number of Members Converted", fill = "Conversion Rate:") +
  scale_fill_manual(values = c("#D9E0A5", "#8E9935", "#474D1A")), tooltip = "text")

Last Event Attended Before Converting

Among the 890 new Members with at least one pre-membership visit in this dataset, which event did they attend immediately before becoming a member? Looking at this can tell us the final touchpoint that may have pushed them to join.

General Admission is the most common last event before conversion, followed by Free First Sunday and **First Friday!. These are all high volume ticket types and expose the visitor to the Barnes as a whole instead of events that focus more in-depth on smaller parts of the Foundation.

LastPreConvert <- Visitation |>
  filter(Converts == "Will convert") |>
  group_by(CustomerId) |>
  summarise(Date = max(Date), LastPreConvert = TRUE, Converts = "Will convert")

Visitation <- Visitation |>
  left_join(LastPreConvert, by = c("CustomerId", "Date", "Converts"))

PieData3 <- Visitation |>
  mutate(EventName = case_when(
           str_detect(EventName, "Members-Only Online Tour") ~ "Members-Only Online Tour", 
           str_detect(EventName, "Highlights ") ~ "Highlights Tour", 
           str_detect(EventName, "Barnes on the Block") ~ "Barnes on the Block", 
           str_detect(EventName, "Private Tours") ~ "Barnes Private Tours", 
           !is.na(EventName) ~ EventName
         )) |>
  filter(LastPreConvert) |>
  group_by(EventName) |>
  summarise(count = n()) |>
  left_join(totalAttendance)|>
  mutate(conversionRate = round(count/totalCount, 3)*100) |>
  arrange(desc(count)) |>
  filter(count >= 5) |>
  filter(!(EventName %in% c("Member Appreciation Talk: Thom Collins on Calder Gardens (online)", "Calder Gardens Admission")))

PieData3 |>
  plot_ly(
    labels = ~EventName, values = ~count, 
    marker = list(colors = c("#D64120", "#F8B6A9", "#8E9935", "#77AFE9", "#24578C","#DDC144", "#A360AE", "#CADDF1", "#163352", "#D9E0A5", "#474D1A"))
  ) |>
  add_pie(hole = 0.65) |>
  layout(
    legend = list(
      orientation = "h",
      y = 1.25, 
      font = list(size = 9),
      itemsizing = "constant",
      itemwidth = 20
    ),
    annotations = list(
  list(
    x = 0.5,
    y = 0.43,
    text = "new members with\na previous visit",
    showarrow = FALSE
  ),
  list(
    x = 0.5,
    y = 0.54,
    text = "890",
    showarrow = FALSE, 
    font = list(size = 20)
  )
) 
  )

How Many Visits Before Converting?

For each new Member, how many times do they visit before converting? 59.5% of new Members in this dataset convert after just one visit, with almost 90% of conversions happening by the third visit.

Not shown in this plot due to range are the 7 new Members in this dataset who visited 20+ times before converting

hist1data <- Visitation |>
  filter(Converts == "Will convert" & !is.na(CustomerId)) |>
  group_by(CustomerId) |>
  summarise(visits = n()) |>
  arrange(desc(visits))
  

ggplotly(ggplot(hist1data, aes(x = visits, text = paste0(visits, " visit(s) before conversion"))) +
  geom_histogram(binwidth = 1, color = "white", linewidth = 1, fill = "#8E9935") +
  coord_cartesian(xlim = c(0, 10)) +
  theme_minimal() +
  theme(panel.grid.major.x = element_blank()) +
  labs(x = "Visits before converting", y = "Number of future Members")
  , tooltip = c("count", "text"))
vizdata1 <- Visitation |>
  filter(!is.na(CustomerId) & LastPreConvert) |>
  mutate(EventName = case_when(
           str_detect(EventName, "Members-Only Online Tour") ~ "Members-Only Online Tour", 
           str_detect(EventName, "Highlights ") ~ "Highlights Tour", 
           str_detect(EventName, "Barnes on the Block") ~ "Barnes on the Block", 
           str_detect(EventName, "Private Tours") ~ "Barnes Private Tours", 
           str_detect(EventName, "Lecture") ~ "Lecture", 
           str_detect(EventName, "Curators in") ~ "Curators in Conversation", 
           str_detect(EventName, "First Friday") ~ "First Friday", 
           str_detect(EventName, "ber App") ~ "Member Appreciation Talk", 
           str_detect(EventName, "Public Exh") ~ "Public Exhibition Tours", 
           str_detect(EventName, "Private Exh") ~ "Private Exhibition Tours", 
           !is.na(EventName) ~ EventName
         )) |>
  select(CustomerId, EventName) |>
  unique() |>
  left_join(hist1data) |>
  mutate(visitgroups = case_when(
    visits == 1 ~ "1 visit",
    visits == 2 ~ "2 visits",
    visits == 3 ~ "3 visits",
    visits > 3 ~ "4+ visits"
  )) |>
  group_by(EventName, visitgroups) |>
  summarize(count = n()) |>
  arrange(desc(count))

Across all future Members, regardless of how many times they visited before converting, General Admission is the most frequent final touchpoint, followed by either Free First Sundays or First Friday!

Highlights tours are also frequently attended right before converting, indicating that both the general access public programming and more in-depth material about the collection can motivate prospective Members.

image2 <- knitr::include_graphics("lasttouchpoint.png")

manipulateWidget::staticImage(image2, style = "max-width:100%")

Takeaways

Most new Members convert within the first few visits, meaning that the Barnes makes a significant impression on prospective Members with these first touchpoints. General-access programming, such as Barnes on the Block, First Sundays or First Friday are frequent first exposures that offer a wide ranging view of what the Barnes has to offer. These programs are more likely to convert visitors to Members than some of the Barnes’ more educational or in-depth programming, which may be attended more often after the conversion happens.