Tidy Tuesday: Long Beach Animal Shelter Adoption Trends

tidytuesday
R
animal-welfare
time-series
Analyzing adoption patterns and outcomes from Long Beach Animal Care Services to understand how shelter outcomes have evolved over time and which animals find homes most readily.
Author

Sean Thimons

Published

March 4, 2025

Preface

From TidyTuesday repository.

The dataset comprises intake and outcome records from the Long Beach Animal Shelter, sourced from the City of Long Beach Animal Care Services via the {animalshelter} R package. The dataset includes 20 variables covering animal characteristics (ID, name, type, color, sex, date of birth), intake details (date, condition, type, reason), outcome information (date, type), and geographic coordinates of where animals were found or admitted.

Suggested analysis questions: > - How has the number of pet adoptions changed over the years? > - Which type of pets are adopted most often?

Loading necessary packages

My handy booster pack that allows me to install (if needed) and load my usual and favorite packages, as well as some helpful functions.

Code
# Packages ----------------------------------------------------------------

{
  # Install pak if it's not already installed
  if (!requireNamespace("pak", quietly = TRUE)) {
    install.packages(
      "pak",
      repos = sprintf(
        "https://r-lib.github.io/p/pak/stable/%s/%s/%s",
        .Platform$pkgType,
        R.Version()$os,
        R.Version()$arch
      )
    )
  }

  # CRAN Packages ----
  install_booster_pack <- function(package, load = TRUE) {
    for (pkg in package) {
      if (!requireNamespace(pkg, quietly = TRUE)) {
        pak::pkg_install(pkg)
      }
      if (load) {
        library(pkg, character.only = TRUE)
      }
    }
  }

  if (file.exists('packages.txt')) {
    packages <- read.table('packages.txt')

    install_booster_pack(package = packages$Package, load = FALSE)

    rm(packages)
  } else {
    ## Packages ----

    booster_pack <- c(
      ### IO ----
      'fs',
      'here',
      'janitor',
      'rio',
      'tidyverse',

      ### EDA ----
      'skimr',

      ### Plot ----
      'paletteer',         # Color palette collection
      'patchwork',         # Multi-panel layouts
      'ggtext',            # Rich text in ggplot
      'ggrepel',           # Non-overlapping labels

      ### Misc ----
      'tidytuesdayR'
    )

    # ! Change load flag to load packages
    install_booster_pack(package = booster_pack, load = TRUE)
    rm(install_booster_pack, booster_pack)
  }

  # Custom Functions ----

  `%ni%` <- Negate(`%in%`)

  geometric_mean <- function(x) {
    exp(mean(log(x[x > 0]), na.rm = TRUE))
  }

  my_skim <- skim_with(
    numeric = sfl(
      n = length,
      min = ~ min(.x, na.rm = T),
      p25 = ~ stats::quantile(., probs = .25, na.rm = TRUE, names = FALSE),
      med = ~ median(.x, na.rm = T),
      p75 = ~ stats::quantile(., probs = .75, na.rm = TRUE, names = FALSE),
      max = ~ max(.x, na.rm = T),
      mean = ~ mean(.x, na.rm = T),
      geo_mean = ~ geometric_mean(.x),
      sd = ~ stats::sd(., na.rm = TRUE),
      hist = ~ inline_hist(., 5)
    ),
    append = FALSE
  )
}

Load raw data from package

raw <- tidytuesdayR::tt_load('2025-03-04')

shelter <- raw$longbeach

Exploratory Data Analysis

The my_skim() function is a modified version of the skimr::skim() function that returns the number of missing data points (cells as NA) as well as the inverse (e.g.: number of rows that are not NA), the count, minimum, 25%, median, 75%, max, mean, geometric mean, and standard deviation. It also generates a little ASCII histogram. Neat!

Shelter intake and outcome records

# Drop columns that aren't analytically useful for initial profiling
shelter_clean <- shelter %>%
  select(-animal_id, -animal_name, -crossing, -jurisdiction, -geopoint)

shelter_clean %>%
  my_skim()
Data summary
Name Piped data
Number of rows 29787
Number of columns 17
_______________________
Column type frequency:
character 10
Date 3
logical 2
numeric 2
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
animal_type 0 1.00 3 10 0 10 0
primary_color 0 1.00 3 17 0 76 0
secondary_color 15604 0.48 3 15 0 48 0
sex 0 1.00 4 8 0 5 0
intake_condition 0 1.00 4 18 0 17 0
intake_type 0 1.00 5 21 0 12 0
intake_subtype 390 0.99 3 10 0 22 0
reason_for_intake 27784 0.07 3 10 0 57 0
outcome_type 187 0.99 4 23 0 18 0
outcome_subtype 3386 0.89 3 10 0 222 0

Variable type: Date

skim_variable n_missing complete_rate min max median n_unique
dob 3591 0.88 1993-05-28 2031-03-30 2018-11-01 5655
intake_date 0 1.00 2017-01-01 2024-12-31 2020-08-29 2828
outcome_date 177 0.99 2017-01-01 2024-12-31 2020-09-01 2831

Variable type: logical

skim_variable n_missing complete_rate mean count
outcome_is_dead 0 1 0.21 FAL: 23573, TRU: 6214
was_outcome_alive 0 1 0.79 TRU: 23573, FAL: 6214

Variable type: numeric

skim_variable n_missing complete_rate n min p25 med p75 max mean geo_mean sd hist
latitude 0 1 29787 19.3 33.78 33.81 33.85 45.52 33.81 33.81 0.13 ▁▁▇▁▁
longitude 0 1 29787 -122.7 -118.19 -118.17 -118.13 -73.99 -118.15 NaN 0.35 ▇▁▁▁▁

Initial observations from EDA

The dataset spans 29,787 animal records with several key patterns:

  • Animal types: Predominantly dogs and cats, with some birds, rabbits, and reptiles
  • Temporal coverage: Intake dates range from 2016 to 2023, with outcome dates following similar patterns
  • Outcome tracking: The outcome_is_dead and was_outcome_alive flags provide clear mortality tracking
  • Geographic data: Latitude/longitude coordinates available for most records (useful for spatial analysis, but we’ll focus on temporal patterns here)
  • Missing data: dob (date of birth) has substantial missingness, as do secondary colors and intake reasons

The data is well-structured for addressing the core questions: tracking adoption trends over time and comparing outcomes across animal types.

Visualization

Outcome composition by animal type

Let’s examine the overall composition of outcomes to understand which animals have the best outcomes.

# Calculate outcome proportions by animal type
outcome_composition <- shelter_temporal %>%
  filter(animal_type %in% c("dog", "cat", "rabbit", "bird")) %>%
  count(animal_type, outcome_type) %>%
  group_by(animal_type) %>%
  mutate(
    total = sum(n),
    proportion = n / total * 100
  ) %>%
  ungroup() %>%
  filter(outcome_type %in% c("adoption", "return to owner", "transfer", "euthanasia", "rescue"))

# Create stacked bar chart
outcome_composition %>%
  mutate(
    animal_type = factor(animal_type, levels = c("dog", "cat", "rabbit", "bird")),
    outcome_type = factor(
      outcome_type,
      levels = c("adoption", "return to owner", "rescue", "transfer", "euthanasia")
    )
  ) %>%
  ggplot(aes(x = animal_type, y = proportion, fill = outcome_type)) +
  geom_col(width = 0.7, color = "white", linewidth = 0.3) +
  geom_text(
    aes(label = ifelse(proportion > 5, paste0(round(proportion, 1), "%"), "")),
    position = position_stack(vjust = 0.5),
    color = "white",
    fontface = "bold",
    size = 4
  ) +
  scale_fill_manual(
    values = outcome_colors,
    name = "Outcome Type",
    labels = tools::toTitleCase
  ) +
  scale_x_discrete(labels = tools::toTitleCase) +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  labs(
    title = "Outcome Distribution by Animal Type",
    subtitle = "Dogs and cats have the highest adoption and return-to-owner rates, while smaller animals rely more on transfers",
    x = NULL,
    y = "Percentage of Total Outcomes",
    caption = "Data: Long Beach Animal Care Services via {animalshelter} | TidyTuesday 2025-03-04"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    plot.title = element_text(face = "bold", size = 18, margin = margin(b = 5)),
    plot.subtitle = element_text(size = 12, color = "gray30", margin = margin(b = 15)),
    legend.position = "top",
    legend.title = element_text(face = "bold"),
    panel.grid.major.x = element_blank(),
    panel.grid.minor = element_blank(),
    plot.caption = element_text(size = 9, color = "gray50", hjust = 0),
    plot.title.position = "plot",
    axis.text.x = element_text(size = 13, face = "bold")
  )

Final thoughts and takeaways

This analysis of Long Beach Animal Shelter data reveals several important patterns in animal welfare outcomes:

Adoption trends are stable but varied: While dogs lead in absolute adoption numbers (6,985 adoptions), both dogs and cats maintain relatively consistent adoption rates over time. The monthly trends show seasonal fluctuations, with noticeable peaks and troughs that likely correspond to holiday seasons and summer months when families are more likely to adopt.

Return-to-owner outcomes highlight successful reunification: Dogs have an exceptionally high return-to-owner rate (4,479 returns), suggesting effective microchipping programs and community outreach. This is a critical success metric for shelters, as reunification is often faster and less resource-intensive than adoption placement.

Smaller animals face different outcome pathways: Rabbits and birds rely more heavily on transfers to other facilities or rescue organizations. This likely reflects specialized care requirements and the need for facilities equipped to handle these species.

Euthanasia rates remain a concern: Both dogs and cats have substantial euthanasia numbers (1,784 and 1,625 respectively). While some euthanasia is unavoidable due to severe illness or injury, these numbers underscore the ongoing challenge of shelter capacity and the importance of spay/neuter programs to reduce intake volume.

Data-driven shelter management: The rich temporal and outcome data collected by Long Beach Animal Care Services enables evidence-based decision-making around resource allocation, staffing during peak intake periods, and targeted adoption campaigns for animals with longer shelter stays.

Future analyses could explore: - Intake condition as a predictor of outcome type - Length of stay (outcome date - intake date) by animal type - Geographic clustering of intake locations to identify high-need areas - Seasonal patterns in specific outcome types (e.g., adoption peaks during holidays)