In this report, I explore intake and outcome records from the Sonoma County animal shelter in California. The data come from the Sonoma County Open Data portal and include about 34,500 shelter stays from 2013 to 2026. Each row is one animal’s stay, with information such as the kind of animal, the date it arrived, how it left the shelter, and how many days it stayed.
Shelter data is often only about dogs and cats, but this shelter also cares for birds, rabbits, livestock, wildlife, and other small pets. To compare all of them, I use an animal category column that groups the many animal types into broader categories.
Each kind of animal takes its own path through the shelter: when it arrives, how it leaves, and how long it stays all depend on what kind of animal it is.
The audience for this story is shelter staff, volunteers, and people thinking about adopting. Knowing when animals arrive and which ones stay the longest can help a shelter plan for busy months and show adopters which animals need homes the most.
I tell the story in three parts, following an animal’s journey through the shelter:
I use tidyverse to clean the data and make plots,
here to find files in my project folder,
ggbeeswarm for extra plot types, and
RColorBrewer for color palettes.
# load packages ----
library(tidyverse)
library(here)
library(ggbeeswarm)
library(RColorBrewer)
I read every column in as text first, because a few almost-empty columns contain a mix of values that confuse R. Then I remove any completely blank rows and take a look at the columns. The dataset has 34,574 rows and 29 columns, so I hide the full list of columns here to keep the report short.
# read in shelter data ----
plotshelter <- read_csv(
here("data", "animal_shelter_intake_outcome.csv"),
col_types = cols(.default = col_character())
)
# remove completely blank rows ----
plotshelter <- plotshelter %>%
filter(!if_all(everything(), is.na))
# view fields ----
glimpse(plotshelter)
The dataset has 29 columns, but I only need four of them. In this step I:
Days in Shelter becomes
days)# rename the columns we need and change field types ----
plotshelter <- plotshelter %>%
rename(days = `Days in Shelter`,
outcome = `Outcome Type`,
intake_date = `Intake Date`) %>%
mutate(days = as.numeric(days),
outcome = str_to_sentence(outcome), # "RETURN TO OWNER" -> "Return to owner"
intake_date = mdy(intake_date)) %>% # text "1/8/2024" -> a real date
filter(!is.na(outcome)) # drop animals with no outcome yet
Next, I count how many animals are in each category.
# summarising how many animals per category ----
plotshelter %>%
group_by(animal_category) %>%
summarise(animals = n())
## # A tibble: 9 × 2
## animal_category animals
## <chr> <int>
## 1 Birds 952
## 2 Cat 11954
## 3 Dog 19473
## 4 Fish 34
## 5 Livestock and equines 440
## 6 Other small pets 246
## 7 Other wildlife 491
## 8 Rabbit 722
## 9 Reptiles 37
Fish and reptiles each have fewer than 40 animals, so their percentages would jump around a lot. I remove these two small groups and order the other seven categories from most to least common, so every plot lists the animals in the same order.
# drop the two tiny groups and order categories from most to least common ----
plotshelter <- plotshelter %>%
filter(!animal_category %in% c("Fish", "Reptiles")) %>%
mutate(animal_category = fct_infreq(animal_category))
The first plot shows when each kind of animal arrives. For each category, I calculate the percent of its yearly intakes that happen in each month. If animals arrived evenly all year, each month would have about 8% (100% divided by 12 months). I group the percents into five color bands, from much quieter than an average month (dark blue) to much busier (dark red), and print the exact percent in each square.
# PLOT 1: when does each kind of animal arrive? (heatmap) ----
plotshelter %>%
mutate(month = month(intake_date, label = TRUE)) %>% # Jan, Feb, ...
group_by(animal_category, month) %>%
summarise(n = n()) %>%
mutate(percent = n / sum(n) * 100) %>% # percent within each category
# group percents into 5 bands so one extreme month can't wash out the colors
mutate(busyness = cut(percent,
breaks = c(0, 5, 7, 10, 13, 100),
labels = c("Much quieter (under 5%)",
"Quieter (5-7%)",
"Typical (7-10%)",
"Busier (10-13%)",
"Much busier (over 13%)"))) %>%
ggplot(aes(x = month, y = fct_rev(animal_category), fill = busyness)) +
geom_tile(color = "white", linewidth = 1) + # one square per animal per month
geom_text(aes(label = round(percent)), size = 3.5) + # print the % in each square
scale_fill_brewer(palette = "RdBu", direction = -1, # blue = quiet, red = busy
drop = FALSE) +
theme_minimal(base_size = 14) +
theme(plot.title = element_text(hjust = 0.5), # center the title
plot.subtitle = element_text(hjust = 0.5)) + # center the subtitle
theme(panel.grid = element_blank()) +
labs(title = "The shelter journey starts with the seasons",
subtitle = "Most animals arrive in spring and summer and fewer in late fall;\ndogs arrive steadily all year",
caption = "data from Sonoma County Open Data (https://data.sonomacounty.ca.gov/)",
x = "Month of intake",
y = NULL,
fill = "Compared with an average month")
Figure 1. Percent of each kind of animal’s yearly intakes by month at the Sonoma County shelter, 2013–2026. Each row adds up to 100%, and an even spread would be about 8% per month. Cats arrive mostly from May to September, while dogs arrive at a steady rate all year. The spikes for other small pets in April and October are likely single large intakes rather than a seasonal pattern.
Most kinds of animals arrive more often in spring and summer and less often in late fall and winter, which shows up as red in the middle of the plot and blue at the edges. Cats have the clearest pattern: about 12% of their yearly intakes arrive in each month from June to September, but only 4–5% in December, January, and February. This matches kitten season, when many kittens are born. Dogs are the exception: their row is almost all grey, meaning they arrive at about the same rate every month.
The second plot shows how each kind of animal leaves the shelter. I keep the four most common outcomes and calculate what percent of each category ends in each one. Each panel shows one outcome, so you can read down a panel to compare animals.
# PLOT 2: how does each kind of animal leave the shelter? (bar chart) ----
# percent of each category that ends in each of the four main outcomes
outcome_summary <- plotshelter %>%
filter(outcome %in% c("Adoption", "Return to owner", "Transfer", "Euthanize")) %>%
group_by(animal_category, outcome) %>%
summarise(n = n()) %>%
mutate(percent = n / sum(n) * 100) %>% # percent within each category
ungroup() %>%
mutate(outcome = factor(outcome,
levels = c("Adoption", "Return to owner",
"Transfer", "Euthanize")))
# one panel per outcome
outcome_summary %>%
ggplot(aes(x = fct_rev(animal_category), y = percent, fill = outcome)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = paste0(round(percent), "%")), hjust = -0.2, size = 3.5) +
coord_flip() +
facet_wrap(~ outcome, nrow = 1) +
scale_y_continuous(limits = c(0, 110)) + # room for the labels
scale_fill_brewer(palette = "Dark2") +
theme_classic(base_size = 14) +
theme(plot.title = element_text(hjust = 0.5), # center the title
plot.subtitle = element_text(hjust = 0.5)) + # center the subtitle
theme(axis.text.x = element_blank(), # % labels replace the x axis
axis.ticks.x = element_blank(),
axis.line.x = element_blank()) +
labs(title = "Each kind of animal leaves the shelter in a different way",
subtitle = "Transfer to another organization is a major way out for most animals;\nonly dogs and livestock are often reclaimed by owners",
caption = "data from Sonoma County Open Data (https://data.sonomacounty.ca.gov/)",
x = NULL,
y = NULL)
Figure 2. Percent of each kind of animal leaving the Sonoma County shelter by the four most common outcomes, 2013–2026. Transfer to another organization is a major way out for most animals, while only dogs (50%) and livestock and equines (48%) are often returned to their owners. Most wildlife arrives for quarantine, which explains its high euthanasia rate (70%).
# how to get ggplots out of R ----
ggsave(here("output", "plot2_outcomes.png"), width = 12, height = 5, dpi = 300)
Half of all dogs (50%) and almost half of livestock and equines (48%) go back to their owners. For every other group, owners rarely come back: only 12% of cats, 6% of birds, and 3% of rabbits are reclaimed. These animals rely on adoption or transfer instead. Transfer is the most common outcome for rabbits (56%), other small pets (50%), and birds (43%), and cats are the most likely to be adopted (41%).
Wildlife follows a separate path. Almost all wildlife arrives for quarantine (for example, bats tested for rabies), so its high euthanasia rate reflects public health rules, not whether the animals could be adopted.
The last plot shows how long each kind of animal stays in the shelter. I use a boxplot: the box covers the middle 50% of stays, and the thick line inside is the median (the middle value). A few animals stayed for months or even years, so I hide those extreme points and zoom in on the first 80 days.
# PLOT 3: how long does each kind of animal stay? (boxplot) ----
plotshelter %>%
ggplot(aes(x = fct_rev(animal_category), # same order as Plots 1 and 2
y = days,
fill = animal_category)) +
geom_boxplot(outlier.shape = NA, show.legend = FALSE) + # hide extreme points
coord_flip(ylim = c(0, 80)) + # zoom in on the first 80 days
scale_fill_brewer(palette = "Set2") +
theme_classic(base_size = 14) +
theme(plot.title = element_text(hjust = 0.5), # center the title
plot.subtitle = element_text(hjust = 0.5)) + # center the subtitle
labs(title = "Animals that rarely go home with an owner stay the longest",
subtitle = "Median stay is about 8 days for dogs and livestock, 10 days for cats,\nand 15 to 21 days for birds, small pets, and rabbits",
caption = "data from Sonoma County Open Data (https://data.sonomacounty.ca.gov/)",
x = NULL,
y = "Days in shelter")
Figure 3. Days spent in the Sonoma County shelter by kind of animal, 2013–2026. Boxes show the middle 50% of stays and the thick line is the median; extreme stays are hidden and the axis stops at 80 days. Dogs and livestock, the groups most often reclaimed by owners, have the shortest typical stays (about 8 days). Cats, which are rarely reclaimed but often adopted, fall in the middle (about 10 days), while birds, other small pets, and rabbits stay 15 to 21 days.
To check the medians in the plot, I calculate them directly:
# median days in the shelter for each kind of animal ----
plotshelter %>%
group_by(animal_category) %>%
summarise(median_days = median(days, na.rm = TRUE))
## # A tibble: 7 × 2
## animal_category median_days
## <fct> <dbl>
## 1 Dog 8
## 2 Cat 10
## 3 Birds 15
## 4 Rabbit 21
## 5 Other wildlife 0
## 6 Livestock and equines 7.5
## 7 Other small pets 17
This plot connects back to Part 2. Dogs and livestock, the two groups most often reclaimed by their owners, have the shortest typical stays at about 8 days. Cats fall in the middle at about 10 days: owners rarely come back for them, but they are the group most likely to be adopted, which keeps their stays fairly short. Birds, other small pets, and rabbits, which are almost never reclaimed, stay about two to three times longer, and rabbits have the longest typical stay at about 21 days. Wildlife is the exception again, since most of it leaves within a day after quarantine.
Following animals through the Sonoma County shelter shows that each kind of animal takes its own path:
For shelter staff and volunteers, this suggests preparing extra space and foster homes for cats before summer, and focusing adoption promotion on rabbits, small pets, and birds, since no one is likely to come back for them.
Sonoma County. Animal Shelter Intake and Outcome. Sonoma County Open Data portal.