El Cerrito Police Activity, 2019-2026

Seven fiscal years of police records, July 2019 through June 2026

Author

Ira Sharenow

Published

September 23, 2026

Executive summary

This report analyzes 146,818 El Cerrito Police Department call records from July 1, 2019 through June 30, 2026, covering seven full fiscal years, FY 2019-20 through FY 2025-26. It looks at what officers spend their time on, how crime reports and parking enforcement have changed, where activity is concentrated, how quickly police respond, and how activity varies by season, day, and hour, with particular attention to the most recent year. All figures describe calls and reports logged by the department. They are not the same as confirmed crimes, and a change in a count can reflect a change in reporting or recording as well as a change in events.

Reports in every crime category reached their lowest level of the seven fiscal years studied. Reports of crimes against persons fell from 597 in FY 2019-20 to 418 in FY 2025-26, and property crime reports fell by more than half, from 2,016 to 976. Robbery and carjacking reports dropped from 48 to 15 in a single year. Shooting and gun-related calls declined from 115 to 52 over the period.

Parking tickets more than tripled between FY 2019-20 and FY 2024-25, from 702 to 2,317, then fell 17% to 1,931 in FY 2025-26. About 40% of the decline came on San Pablo Avenue. Enforcement has also shifted toward weekday business hours: in FY 2019-20 about a quarter of tickets were written on weekends, and in FY 2025-26 fewer than one in twelve were. Tickets within 1,000 feet of El Cerrito Plaza BART reached their highest count of the seven years even as the citywide total fell.

Police activity on the Ohlone Greenway was 48% higher in FY 2025-26 than its average over the six earlier fiscal years, driven by pedestrian stops and foot patrol. Officer-initiated activity on the greenway had fallen sharply in January 2020, before the COVID-19 shutdown, and stayed low for five years before rising in FY 2025-26.

The mix of police work changed. Traffic stops grew from 12% of all logged activity in FY 2019-20 through FY 2024-25 to 26% in FY 2025-26, while security checks fell from 17% to 5%. Changes near individual landmarks, including a large increase near Del Norte BART, came mainly from officer-initiated activity such as traffic stops and parking enforcement rather than from calls reporting crimes.

Median response time for call-and-respond incidents held at 7 to 9 minutes from FY 2019-20 through FY 2024-25 and was 6 minutes in FY 2025-26. Part of that improvement reflects a change in the mix of calls, so one year is not enough to call it a trend.

Measure FY 2019-20 FY 2024-25 FY 2025-26
Crimes against persons (reports) 597 502 418
Property crime (reports) 2,016 1,164 976
Robbery and carjacking 74 48 15
Parking tickets 702 2,317 1,931
Parking tickets on weekends 27.4% 18.6% 7.8%
Median response, call-and-respond incidents (minutes) 8 7 6

About the data

The El Cerrito Police Department logs every call for service and every officer-initiated activity in its records system. Each record includes an event number, a call type (such as “211 - ROBBERY” or “1195 - TRAFFIC STOP”), the time the call was received, the time the first unit arrived, the time the call was cleared, and a location.

Two sources are combined here. Records through June 2025 came from 13 half-year PDF reports, which were extracted to spreadsheets, checked, and corrected. Records for FY 2025-26 came directly from the department as an Excel export, which removed the extraction step entirely.

The analysis uses the city’s fiscal year, July 1 through June 30, so that each year compared is a complete twelve months. The period covers seven full fiscal years, FY 2019-20 through FY 2025-26, for a combined 146,818 records. Throughout the report, “the six earlier years” means FY 2019-20 through FY 2024-25, and comparisons with the most recent year use their average.

Fiscal year Records
FY 2019-20 23,015
FY 2020-21 17,964
FY 2021-22 16,489
FY 2022-23 18,011
FY 2023-24 23,167
FY 2024-25 25,054
FY 2025-26 23,118

A call record shows that something was reported or that an officer took an action. It does not show whether a crime occurred, whether anyone was charged, or when an incident actually happened. A theft discovered in the morning is logged in the morning, even if it happened overnight. For these reasons the report refers to “reports of” and “calls about” rather than to crime rates.

How police time is spent

To make the data meaningful for residents, each of the 144 call types in the records was assigned to one of seven categories. The assignments are the analyst’s, based on the call-type names, and are not department classifications. The full mapping is published with the code.

Category Examples
Crimes against persons Assault, robbery, sexual assault, shootings, threats, domestic violence
Property crime Burglary, car break-ins, petty and grand theft, vehicle theft, vandalism, fraud
Dangerous driving Hit and run, drunk driving, reckless driving, speeding, sideshows
Disorder Disturbances, unwanted persons, trespassing, drugs and alcohol, noise
Parking Parking violations
Officer-initiated Traffic, pedestrian, and bike stops; security checks; patrol; warrants
Everything else Alarms, collisions, welfare and medical calls, suspicious activity, administrative calls
Category FY 19-20 FY 20-21 FY 21-22 FY 22-23 FY 23-24 FY 24-25 FY 25-26
Crimes against persons 597 431 504 499 560 502 418
Property crime 2,016 1,469 1,540 1,271 1,307 1,164 976
Dangerous driving 309 297 299 266 305 288 245
Disorder 2,352 1,998 1,901 2,074 1,878 1,842 1,667
Parking 702 610 863 1,054 1,519 2,317 1,931
Officer-initiated 9,190 6,302 4,241 5,221 7,530 8,762 8,289
Everything else 7,849 6,857 7,141 7,626 10,068 10,179 9,592

Officer-initiated activity fell by more than half between FY 2019-20 and FY 2021-22 and has since recovered to near its earlier level. The mix within it has changed. In the six earlier years, security checks were 17% of all records and traffic stops 12%. In FY 2025-26, traffic stops were 26% of all records and security checks 5%. Call transfers went from 3% to 9%. The records show what was logged; they do not explain why the mix changed.

Show code
police_geo_all %>%
  count(call_for_service, sort = TRUE) %>%
  mutate(`% of all records` = round(100 * n / sum(n), 1)) %>%
  slice_head(n = 10) %>%
  transmute(`Call type` = str_to_title(call_for_service), Records = n, `% of all records`)
Table 1: Ten most common call types, FY 2019-20 through FY 2025-26.
Call type Records % of all records
1059 - Security Check 21908 14.9
1195 - Traffic Stop 21317 14.5
Parker - Parking Violation 8996 6.1
911dis - 911 Disconnect 8758 6.0
Unwant - Unwanted Person 6455 4.4
Xfer - Call Transfer 6001 4.1
1033a - Alarm Audible 5881 4.0
Follow - Follow Up 4769 3.2
1154 - Suspicious Vehicle 4528 3.1
415 - Disturbance 4309 2.9

Crime reports

Show code
police_all %>%
  filter(category %in% c("Crimes against persons", "Property crime",
                         "Dangerous driving", "Disorder")) %>%
  count(category, fy_label) %>%
  ggplot(aes(fy_label, n, group = 1)) +
  geom_line(color = "steelblue", linewidth = 1) +
  geom_point(color = "steelblue", size = 2) +
  facet_wrap(~ category, scales = "free_y") +
  expand_limits(y = 0) +
  scale_y_continuous(labels = comma) +
  labs(title = "Crime reports by category", x = NULL, y = "Reports") +
  theme_report +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

All four crime categories were at their lowest level of the seven years in FY 2025-26.

Crimes against persons

Type FY 19-20 FY 20-21 FY 21-22 FY 22-23 FY 23-24 FY 24-25 FY 25-26
Assault 235 162 195 201 201 176 171
Robbery and carjacking 74 65 47 74 81 48 15
Shootings and guns 115 99 86 88 85 61 52
Threats and stalking 43 33 39 43 61 90 58
Domestic violence 39 24 42 39 42 43 32
Crimes against children and elders 32 26 32 20 38 40 37
Sexual assault 14 5 10 8 13 21 18

Robbery and carjacking reports fell from 48 in FY 2024-25 to 15 in FY 2025-26, the largest one-year change in any category. Because a drop that size could signal a recording change, it was checked directly. The 15 robberies are spread across nine of the twelve months rather than missing from a block of time, and no robbery call type was renamed or moved. Robbery alarms, which are business alarm activations rather than confirmed robberies, are counted separately and numbered 20. The decline appears in the data as recorded; with counts this small, part of a one-year change can be chance.

Shooting and gun-related calls, which include shots-fired reports, brandishing, and person-with-a-gun calls, declined in five of the six year-to-year changes. Assault reports have been roughly level for two years. Threats and stalking reports rose to 90 in FY 2024-25 before falling to 58. Sexual assault reports were somewhat higher in the last two years than earlier, though the numbers are small enough that year-to-year variation is expected.

Property crime

Type FY 19-20 FY 20-21 FY 21-22 FY 22-23 FY 23-24 FY 24-25 FY 25-26
Petty theft 676 554 529 439 452 436 304
Car break-in 416 152 135 126 144 70 107
Vandalism and arson 244 197 190 146 162 145 132
Burglary (home, business) 189 102 118 108 99 111 66
Vehicle theft 189 187 256 221 242 141 127
Fraud and identity theft 154 118 128 87 120 146 148
Grand theft 108 103 150 119 69 107 74

Petty theft and grand theft are shown separately because they describe very different events. Petty theft reports fell by more than half over the period. Burglary reports fell by about two-thirds. Car break-in reports dropped sharply after FY 2019-20, reached a low of 70 in FY 2024-25, and rose to 107 in FY 2025-26. Fraud and identity theft is the one property category that has not declined.

Dangerous driving and disorder

Dangerous driving reports, which include hit and run, drunk driving, reckless driving, and sideshows, ranged from 266 to 309 a year before falling to 245 in FY 2025-26. Disorder reports, led by disturbances and unwanted-person calls, declined from 2,352 to 1,667 over the period.

Parking enforcement

Parking tickets are the single subject residents are most likely to have direct experience with, so they are treated separately from crime throughout this report.

Show code
parking_fy %>%
  ggplot(aes(fy_label, parking_violations)) +
  geom_col(fill = "steelblue") +
  geom_text(aes(label = comma(parking_violations)), vjust = -0.4, size = 3.5) +
  scale_y_continuous(labels = comma, expand = expansion(mult = c(0, 0.08))) +
  labs(title = "Parking tickets by fiscal year", x = NULL, y = "Tickets") +
  theme_report
Figure 2: Parking tickets by fiscal year.

Tickets more than tripled from FY 2019-20 to FY 2024-25, with the largest increases in FY 2023-24 and FY 2024-25. FY 2025-26 was the first decline after four consecutive increases, and the total was still about 2.75 times the FY 2019-20 level.

The decline was uneven across the year. The largest monthly drops compared with FY 2024-25 came in May (379 to 164), September (233 to 138), April (217 to 129), and January (169 to 89). July and June were higher than the year before. The pattern suggests enforcement that varies in intensity from month to month rather than a steady reduction.

Show code
parking_points %>%
  filter(fy %in% c(2025, 2026)) %>%
  mutate(month = factor(as.character(month(dt, label = TRUE)), levels = FY_MONTHS)) %>%
  count(fy_label, month) %>%
  pivot_wider(names_from = fy_label, values_from = n, values_fill = 0) %>%
  mutate(Change = `FY 2025-26` - `FY 2024-25`) %>%
  rename(Month = month)
Table 2: Parking tickets by month, FY 2024-25 and FY 2025-26.
Month FY 2024-25 FY 2025-26 Change
Jul 171 271 100
Aug 183 177 -6
Sep 233 138 -95
Oct 208 164 -44
Nov 133 103 -30
Dec 114 83 -31
Jan 169 89 -80
Feb 118 119 1
Mar 118 141 23
Apr 217 129 -88
May 379 164 -215
Jun 274 353 79

Where tickets are written

Street FY 19-20 FY 20-21 FY 21-22 FY 22-23 FY 23-24 FY 24-25 FY 25-26
San Pablo Ave 84 72 47 118 209 337 180
Liberty St 43 42 90 45 89 129 136
Kearney St 19 10 37 28 58 72 78
Lexington Ave 16 25 71 31 51 59 74
Elm St 17 14 39 29 32 32 45
Ashbury Ave 7 3 8 20 34 46 42
Stockton Ave 5 1 2 8 13 9 30

San Pablo Avenue accounted for 157 of the 386-ticket decline in FY 2025-26. Tickets on several residential streets, including Liberty, Kearney, Lexington, Elm, and Stockton, rose. Enforcement is not concentrated on a handful of streets: in every year, the five busiest streets accounted for between 24% and 33% of tickets.

Compared with FY 2024-25, tickets within 500 feet of Del Norte BART rose from 32 to 80 and those near the library rose from 27 to 52, while tickets near the Community Center fell from 38 to 16 and near Harding Elementary from 38 to 13.

Fiscal year Tickets within 1,000 ft of El Cerrito Plaza BART Share of mappable tickets
FY 2019-20 60 8.5%
FY 2020-21 49 8.1%
FY 2021-22 101 11.7%
FY 2022-23 60 5.7%
FY 2023-24 111 7.3%
FY 2024-25 183 7.9%
FY 2025-26 196 10.3%

Through FY 2024-25, tickets near El Cerrito Plaza grew no faster than tickets citywide. In FY 2025-26, however, the number of tickets near the Plaza reached its highest level of the seven years while the citywide total fell, and the Plaza’s share rose to its second-highest level.

Show code
ggplot() +
  annotation_map_tile(type = "osm", zoomin = 0) +
  stat_density_2d(data = parking_usable %>% filter(fy %in% c(2025, 2026)),
                  aes(x = long, y = lat, fill = after_stat(level)),
                  geom = "polygon", contour = TRUE, alpha = 0.5, bins = 15) +
  scale_fill_gradient(low = "yellow", high = "red", breaks = range,
                      labels = c("Fewer tickets", "More tickets")) +
  geom_point(data = landmarks_geocoded, aes(x = long, y = lat),
             color = "black", size = 2, shape = 17) +
  geom_label_repel(data = landmarks_geocoded, aes(x = long, y = lat, label = landmark),
                   size = 2.3, fontface = "bold", label.padding = 0.12, label.size = 0,
                   fill = alpha("white", 0.75), min.segment.length = 0, max.overlaps = Inf) +
  facet_wrap(~ fy_label) +
  coord_sf(xlim = c(EC_MAP_LONG_MIN, EC_MAP_LONG_MAX),
           ylim = c(EC_MAP_LAT_MIN, EC_MAP_LAT_MAX), crs = 4326) +
  labs(title = "Where parking tickets are written", fill = NULL) +
  theme_report +
  theme(axis.title = element_blank(), axis.text = element_blank())
Figure 3: Where parking tickets were written, FY 2024-25 and FY 2025-26.

When tickets are written

Show code
ggplot(parking_schedule, aes(fy_label, share, fill = slot)) +
  geom_col(width = 0.75) +
  geom_text(aes(label = if_else(share >= 0.04, percent(share, accuracy = 1), "")),
            position = position_stack(vjust = 0.5), size = 3.3, color = "white",
            fontface = "bold") +
  scale_fill_manual(values = c("Weekend" = "#E69F00",
                               "Weekday, other hours" = "#56B4E9",
                               "Weekday, 7 a.m. to 4 p.m." = "#0072B2")) +
  scale_y_continuous(labels = percent, expand = c(0, 0)) +
  labs(title = "When parking tickets are written",
       x = NULL, y = "Share of the year's tickets", fill = NULL) +
  theme_report +
  theme(axis.text.x = element_text(angle = 45, hjust = 1), legend.position = "top")
Figure 4: Parking tickets by time of week, each fiscal year. Each bar adds to 100%.

In FY 2019-20, 42% of parking tickets were written on weekdays between 7 a.m. and 4 p.m., 31% on weekdays at other hours, and 27% on weekends. In FY 2025-26, the weekday 7 a.m. to 4 p.m. share was 81%, with 11% at other weekday hours and 8% on weekends.

Show code
parking_points %>%
  filter(fy %in% c(2020, 2026)) %>%
  mutate(weekday = wday(dt, label = TRUE, week_start = 1)) %>%
  count(fy_label, weekday) %>%
  group_by(fy_label) %>%
  mutate(share = n / sum(n)) %>%
  ungroup() %>%
  ggplot(aes(weekday, share, fill = fy_label)) +
  geom_col(position = position_dodge(width = 0.8), width = 0.75) +
  geom_text(aes(label = percent(share, accuracy = 1)),
            position = position_dodge(width = 0.8), vjust = -0.4, size = 3) +
  scale_fill_manual(values = c("FY 2019-20" = "#E69F00", "FY 2025-26" = "#0072B2")) +
  scale_y_continuous(labels = percent, expand = expansion(mult = c(0, 0.1))) +
  labs(title = "Parking tickets by day of the week",
       x = NULL, y = "Share of the year's tickets", fill = NULL) +
  theme_report +
  theme(legend.position = "top")
Figure 5: Share of parking tickets written on each day of the week, FY 2019-20 and FY 2025-26.

The weekly schedule has also varied from year to year; in FY 2022-23, for example, 31% of tickets were written on Fridays. The chart below shows the full pattern for the two most recent years, by day and hour.

Show code
parking_points %>%
  filter(fy %in% c(2025, 2026)) %>%
  mutate(weekday = wday(dt, label = TRUE, week_start = 1),
         hour    = hour(dt)) %>%
  count(fy_label, weekday, hour) %>%
  complete(fy_label, weekday, hour = 0:23, fill = list(n = 0)) %>%
  group_by(fy_label) %>%
  mutate(share = n / sum(n)) %>%
  ungroup() %>%
  ggplot(aes(hour, fct_rev(weekday), fill = share)) +
  geom_tile(color = "white", linewidth = 0.3) +
  facet_wrap(~ fy_label, ncol = 1) +
  scale_fill_viridis_c(option = "plasma", labels = percent) +
  scale_x_continuous(breaks = seq(0, 21, 3),
                     labels = c("Midnight", "3 a.m.", "6 a.m.", "9 a.m.",
                                "Noon", "3 p.m.", "6 p.m.", "9 p.m.")) +
  labs(title = "When parking tickets are written", x = NULL, y = NULL,
       fill = "Share of year") +
  theme_report
Figure 6: Share of each year’s parking tickets by weekday and hour, FY 2024-25 and FY 2025-26.

The Ohlone Greenway

The Ohlone Greenway is a linear trail that runs the length of El Cerrito alongside the BART tracks. Because it is a line rather than a point, greenway activity was measured from the location text rather than from map coordinates. This matters for accuracy: 99 FY 2025-26 records are logged simply as “OHLONE GREENWAY,” with no cross street, and could not be placed on a map.

The department’s name for the location changed during the period. Through June 2025 it was most often recorded as “BART PATH”; in FY 2025-26 it is almost always “OHLONE.” Counting both names is essential. A count using only “OHLONE” would suggest that greenway activity more than doubled, when the actual increase was 25%.

Records were sorted into two groups: activity on the greenway itself, where the location names the greenway, and activity at addresses next to it, where the greenway is noted as the nearest cross street. On the greenway, FY 2025-26 had 344 records compared with an average of 232 a year over the six earlier years. Next to the greenway, FY 2025-26 had 45 records compared with an average of 47.

Show code
police_all %>%
  filter(!is.na(address)) %>%
  mutate(greenway = greenway_bucket(address)) %>%
  filter(greenway == "on greenway") %>%
  mutate(type = if_else(call_for_service %in% officer_initiated_types,
                        "Officer-initiated", "Other calls")) %>%
  count(fy_label, type) %>%
  ggplot(aes(fy_label, n, fill = type)) +
  geom_col() +
  scale_fill_manual(values = c("Officer-initiated" = "steelblue", "Other calls" = "grey65")) +
  labs(title = "Activity on the Ohlone Greenway", x = NULL, y = "Records", fill = NULL) +
  theme_report +
  theme(axis.text.x = element_text(angle = 45, hjust = 1), legend.position = "top")
Figure 7: Police activity on the Ohlone Greenway by fiscal year.
Fiscal year Officer-initiated Other calls Total
FY 2019-20 393 102 495
FY 2020-21 105 93 198
FY 2021-22 98 86 184
FY 2022-23 56 98 154
FY 2023-24 80 94 174
FY 2024-25 48 137 185
FY 2025-26 206 138 344

For this section, officer-initiated activity means security checks, pedestrian, traffic, and bike stops, foot patrol, and parking enforcement. That activity was highest in FY 2019-20, fell sharply, stayed low for five years, and rose about fourfold in FY 2025-26 to roughly half its FY 2019-20 level. The type of activity also changed, as the table below shows: security checks on the greenway fell, while pedestrian stops and foot patrol rose. Other calls on the greenway have been about 137 a year for the past two years, up from roughly 90 in earlier years.

Show code
greenway_fy %>%
  filter(greenway == "on greenway") %>%
  mutate(period = if_else(fy == LATEST_FY, "latest", "base")) %>%
  count(call_for_service, period) %>%
  pivot_wider(names_from = period, values_from = n, values_fill = 0) %>%
  mutate(base = round(base / length(BASE_FY), 1)) %>%
  arrange(desc(latest)) %>%
  slice_head(n = 12) %>%
  transmute(`Call type` = str_to_title(call_for_service),
            `Six earlier years, per year` = base, `FY 2025-26` = latest)
Table 3: Most common call types on the greenway: average of the six earlier years vs. FY 2025-26.
Call type Six earlier years, per year FY 2025-26
1059 - Security Check 90.7 66
1194 - Pedestrian Stop 19.0 65
Footp - Foot Patrol - Miscellaneous 3.2 30
1195 - Traffic Stop 10.8 20
Pmisc - Police Miscellaneous 7.3 16
Xfer - Call Transfer 3.7 16
Parker - Parking Violation 3.8 14
1195b - Bike Stop 2.5 11
911dis - 911 Disconnect 4.5 10
415 - Disturbance 6.7 9
Welfck - Welfare Check 5.5 9
Unwant - Unwanted Person 3.7 7

The early-2020 decline

Officer-initiated activity on the greenway declined in 2020, which coincided with the start of the COVID-19 pandemic. Monthly records show that the decline began earlier. Security checks on the greenway averaged about 40 a month from July through December 2019, fell to 11 and 13 in January and February 2020, and stayed between 2 and 14 a month through 2021.

Show code
police_clean_dated %>%
  filter(call_for_service == "1059 - SECURITY CHECK",
         dt >= FY_START, dt < as.POSIXct("2022-01-01", tz = "UTC")) %>%
  mutate(month = floor_date(dt, "month"),
         on_greenway = !is.na(address) & greenway_bucket(address) == "on greenway") %>%
  group_by(month) %>%
  summarize(citywide = n(), greenway = sum(on_greenway), .groups = "drop") %>%
  mutate(share = greenway / citywide) %>%
  ggplot(aes(month, share)) +
  geom_line(color = "steelblue", linewidth = 1) +
  geom_point(color = "steelblue", size = 1.5) +
  geom_vline(xintercept = as.POSIXct("2020-03-01", tz = "UTC"), linetype = "dashed") +
  scale_y_continuous(labels = percent, limits = c(0, NA)) +
  labs(title = "Greenway share of all security checks",
       x = NULL, y = "Share of citywide security checks") +
  theme_report
Figure 8: Greenway share of all security checks, by month, July 2019 through December 2021. Dashed line: March 2020.

The decline was specific to the greenway. Citywide security checks did not fall in early 2020, and in March 2020 they rose to 1,269, about three times their usual monthly level. The greenway’s share of all security checks dropped from about 13% in July through December 2019 to between 1% and 5% from January 2020 onward. The data show when the change happened but not why.

Where activity happens

Show code
ggplot() +
  annotation_map_tile(type = "osm", zoomin = 0) +
  stat_density_2d(data = crime_map_all,
                  aes(x = long, y = lat, fill = after_stat(level)),
                  geom = "polygon", contour = TRUE, alpha = 0.5, bins = 25) +
  scale_fill_gradient(low = "yellow", high = "red", breaks = range,
                      labels = c("Fewer incidents", "More incidents")) +
  geom_point(data = landmarks_geocoded, aes(x = long, y = lat),
             color = "black", size = 2.5, shape = 17) +
  geom_label_repel(data = landmarks_geocoded, aes(x = long, y = lat, label = landmark),
                   size = 2.8, fontface = "bold", label.padding = 0.15, label.size = 0,
                   fill = alpha("white", 0.75), min.segment.length = 0, max.overlaps = Inf) +
  coord_sf(xlim = c(EC_MAP_LONG_MIN, EC_MAP_LONG_MAX),
           ylim = c(EC_MAP_LAT_MIN, EC_MAP_LAT_MAX), crs = 4326) +
  labs(title = "Where crime-related incidents happen, 2019-2026", fill = NULL) +
  theme_report +
  theme(axis.title = element_blank())
Figure 9: Density of crime-related incidents, FY 2019-20 through FY 2025-26, relative to nine community landmarks.

Crime-related incidents concentrate along the San Pablo Avenue corridor, El Cerrito’s main commercial spine, and drop off sharply moving east into the hillside residential neighborhoods. This map and the landmark tables below use the crime-related call types described in the technical appendix, which include parking violations; the maps in the parking section show tickets on their own.

Show code
compute_landmark_counts(landmark_input_all, "Records within 500 ft") %>%
  arrange(desc(`Records within 500 ft`)) %>%
  rename(Landmark = landmark)
Table 4: All police activity within 500 feet of each landmark, FY 2019-20 through FY 2025-26.
Landmark Records within 500 ft
EC Plaza BART 2496
EC Community Center 1991
ECHS 1301
Del Norte BART 1235
Library 1157
Harding 870
Madera 543
Castro Park 397
Korematsu 326

The table counts all police activity, not only crime, within 500 feet of nine community landmarks. Del Norte BART sits on the city’s border with Richmond and has its own BART Police substation. Incidents handled by BART Police or by Richmond police are not part of this dataset, so the count reflects El Cerrito Police Department activity only.

Show code
vehicle_all   <- crime_map_all %>% filter(cfs_key %in% vehicle_crime_types)
dangerous_all <- crime_map_all %>% filter(cfs_key %in% dangerous_driving_types)
other_all     <- crime_map_all %>% filter(!cfs_key %in% c(vehicle_crime_types, dangerous_driving_types))

compute_landmark_counts(vehicle_all, "Vehicle crime") %>%
  left_join(compute_landmark_counts(dangerous_all, "Dangerous driving"), by = "landmark") %>%
  left_join(compute_landmark_counts(other_all, "All other"), by = "landmark") %>%
  mutate(Total = `Vehicle crime` + `Dangerous driving` + `All other`) %>%
  arrange(desc(Total)) %>%
  rename(Landmark = landmark)
Table 5: Crime-related incidents within 500 feet of each landmark by type, FY 2019-20 through FY 2025-26.
Landmark Vehicle crime Dangerous driving All other Total
EC Plaza BART 28 17 399 444
EC Community Center 28 17 364 409
ECHS 9 11 334 354
Library 25 2 327 354
Del Norte BART 12 13 318 343
Harding 9 8 200 217
Castro Park 4 4 84 92
Korematsu 3 2 64 69
Madera 4 2 56 62

Vehicle crime (car break-ins, vehicle theft, carjacking) and dangerous driving (hit and run, drunk driving) are shown separately because they are different kinds of risk: property crime against a parked car, and danger from a car in motion. “All other” covers every other crime-related type, such as assault, robbery, burglary, theft, and vandalism.

Show code
ggplot() +
  annotation_map_tile(type = "osm", zoomin = 0) +
  stat_density_2d(data = vehicle_all,
                  aes(x = long, y = lat, fill = after_stat(level)),
                  geom = "polygon", contour = TRUE, alpha = 0.5, bins = 15) +
  scale_fill_gradient(low = "yellow", high = "red", breaks = range,
                      labels = c("Fewer incidents", "More incidents")) +
  geom_point(data = landmarks_geocoded, aes(x = long, y = lat),
             color = "black", size = 2.5, shape = 17) +
  geom_label_repel(data = landmarks_geocoded, aes(x = long, y = lat, label = landmark),
                   size = 2.8, fontface = "bold", label.padding = 0.15, label.size = 0,
                   fill = alpha("white", 0.75), min.segment.length = 0, max.overlaps = Inf) +
  coord_sf(xlim = c(EC_MAP_LONG_MIN, EC_MAP_LONG_MAX),
           ylim = c(EC_MAP_LAT_MIN, EC_MAP_LAT_MAX), crs = 4326) +
  labs(title = "Where vehicle crime happens, 2019-2026", fill = NULL) +
  theme_report +
  theme(axis.title = element_blank())
Figure 10: Density of vehicle-crime incidents, FY 2019-20 through FY 2025-26.

Changes in the most recent year

Activity within 500 feet of nine landmarks in FY 2025-26 was compared with the average of the six earlier years. To keep the periods comparable, call types whose locations were withheld in the PDF records, such as mental health and juvenile calls, are excluded from both.

Show code
landmark_base %>%
  left_join(landmark_latest, by = "landmark") %>%
  arrange(desc(latest)) %>%
  transmute(Landmark = landmark, `Six earlier years, per year` = base_per_year,
            `FY 2025-26` = latest)
Table 6: All police activity within 500 feet of each landmark: average of the six earlier years vs. FY 2025-26.
Landmark Six earlier years, per year FY 2025-26
Del Norte BART 146 359
EC Plaza BART 359 343
Library 152 248
EC Community Center 291 247
ECHS 188 176
Harding 118 161
Castro Park 57 54
Madera 82 48
Korematsu 47 44

The table below shows which call types changed most near Del Norte BART, the library, and Madera. In each case, the largest changes were in officer-initiated and administrative activity, such as traffic stops, parking tickets, security checks, and call transfers, rather than in crime reports.

Show code
bind_rows(
  landmark_calls("Del Norte BART") %>% mutate(Landmark = "Del Norte BART"),
  landmark_calls("Library")        %>% mutate(Landmark = "Library"),
  landmark_calls("Madera")         %>% mutate(Landmark = "Madera")
) %>%
  transmute(Landmark, `Call type` = str_to_title(call_for_service),
            `Six earlier years, per year` = base, `FY 2025-26` = latest,
            Change = round(change, 1))
Table 7: Largest changes by call type within 500 feet: average of the six earlier years vs. FY 2025-26.
Landmark Call type Six earlier years, per year FY 2025-26 Change
Del Norte BART 1195 - Traffic Stop 22.7 98 75.3
Del Norte BART Parker - Parking Violation 10.8 80 69.2
Del Norte BART Xfer - Call Transfer 21.0 77 56.0
Del Norte BART 911dis - 911 Disconnect 13.7 26 12.3
Del Norte BART 1194 - Pedestrian Stop 3.0 10 7.0
Del Norte BART Oaided - Outside Assist 11.2 7 -4.2
Library Parker - Parking Violation 15.8 52 36.2
Library 1059 - Security Check 21.3 43 21.7
Library 1195 - Traffic Stop 8.0 28 20.0
Library Xfer - Call Transfer 5.7 19 13.3
Library 1066 - Suspicious Person 6.3 13 6.7
Library 1154 - Suspicious Vehicle 3.7 8 4.3
Madera 1059 - Security Check 44.5 18 -26.5
Madera 1154 - Suspicious Vehicle 2.5 0 -2.5
Madera Xpat - Extra Patrol 11.8 10 -1.8
Madera 1124 - Abandoned Vehicle 0.3 2 1.7
Madera 1033a - Alarm Audible 3.7 5 1.3
Madera Footp - Foot Patrol - Miscellaneous 1.2 0 -1.2
Show code
fy26_crime_points <- police_fy26_geocoded %>%
  filter(!BAD_LATLON_FLAG) %>%
  mutate(cfs_key = str_to_lower(call_for_service)) %>%
  left_join(call_categories, by = "cfs_key") %>%
  filter(category %in% c("Crimes against persons", "Property crime"))

ggplot() +
  annotation_map_tile(type = "osm", zoomin = 0) +
  stat_density_2d(data = fy26_crime_points,
                  aes(x = long, y = lat, fill = after_stat(level)),
                  geom = "polygon", contour = TRUE, alpha = 0.5, bins = 20) +
  scale_fill_gradient(low = "yellow", high = "red", breaks = range,
                      labels = c("Fewer reports", "More reports")) +
  geom_point(data = landmarks_geocoded, aes(x = long, y = lat),
             color = "black", size = 2.5, shape = 17) +
  geom_label_repel(data = landmarks_geocoded, aes(x = long, y = lat, label = landmark),
                   size = 2.8, fontface = "bold", label.padding = 0.15, label.size = 0,
                   fill = alpha("white", 0.75), min.segment.length = 0, max.overlaps = Inf) +
  coord_sf(xlim = c(EC_MAP_LONG_MIN, EC_MAP_LONG_MAX),
           ylim = c(EC_MAP_LAT_MIN, EC_MAP_LAT_MAX), crs = 4326) +
  labs(title = "Where crimes were reported, FY 2025-26", fill = NULL) +
  theme_report +
  theme(axis.title = element_blank())
Figure 11: Crimes against persons and property crime reports, FY 2025-26 (parking excluded).

Response times

Response time is measured from when a call is received to when the first officer arrives. The analysis covers crime-related calls and excludes activity that officers initiate themselves, such as parking enforcement, because those records have no travel time and would make responses look faster than they are.

The exclusion matters. Including officer-initiated records, the median drops from 6 minutes in FY 2019-20 to 3 in FY 2025-26, an apparent improvement driven largely by the growth of parking enforcement, which has no dispatch or travel time. The table below uses call-and-respond incidents only.

Fiscal year Median response (minutes)
FY 2019-20 8
FY 2020-21 8
FY 2021-22 8
FY 2022-23 9
FY 2023-24 8
FY 2024-25 7
FY 2025-26 6

The FY 2025-26 median of 6 minutes was the lowest of the seven years. A comparison of individual call types found that the improvement is partly real and partly a change in the mix of calls. Of 14 common call types, 6 were faster in FY 2025-26 than the year before, 5 were unchanged, and 3 were slower. At the same time, there were fewer of some call types that typically have longer response times, such as theft reports, which lowers the overall median on its own. One year of improvement is not enough to establish a trend.

Show code
response_fy %>%
  filter(!str_to_lower(call_for_service) %in% self_initiated_types,
         fy %in% c(2025, 2026)) %>%
  group_by(call_for_service) %>%
  filter(sum(fy == 2025) >= 30, sum(fy == 2026) >= 30) %>%
  group_by(call_for_service, fy_label) %>%
  summarize(median_min = median(response_min), n = n(), .groups = "drop") %>%
  pivot_wider(names_from = fy_label, values_from = c(median_min, n)) %>%
  arrange(desc(`n_FY 2025-26`)) %>%
  transmute(`Call type` = str_to_title(call_for_service),
            `Median, FY 2024-25` = `median_min_FY 2024-25`,
            `Median, FY 2025-26` = `median_min_FY 2025-26`,
            `Cases, FY 2024-25` = `n_FY 2024-25`,
            `Cases, FY 2025-26` = `n_FY 2025-26`)
Table 8: Median response time (minutes) by call type, FY 2024-25 and FY 2025-26, for call-and-respond types with at least 30 cases in each year.
Call type Median, FY 2024-25 Median, FY 2025-26 Cases, FY 2024-25 Cases, FY 2025-26
Unwant - Unwanted Person 6.0 6.0 769 677
415 - Disturbance 8.0 7.0 504 542
488 - Petty Theft 8.0 7.5 406 284
243a - Assault / Battery 6.0 6.0 115 121
20002 - Hit And Run No Injury 9.0 9.0 128 114
594 - Vandalism 8.0 9.0 116 108
1053 - Person Down 4.0 4.0 134 103
459a - Auto Burglary 9.5 7.0 64 103
10851 - Motor Vehicle Theft 13.0 11.0 109 100
602l - Trespassing 6.0 7.0 88 77
487 - Grand Theft 12.0 8.0 103 72
1179 - Accident W/ Medical Routed 4.0 3.0 70 65
422 - Criminal Threats 13.0 10.0 66 47
459r - Residential Burglary 10.5 14.0 62 41

For the most urgent calls, including shots fired, robbery, and assault with a deadly weapon, median response times in FY 2025-26 were between 2 and 5 minutes, consistent with earlier years. The number of these calls is small, so they are best read as a group rather than type by type.

Seasonal patterns

This section asks whether police activity follows regular patterns by month of the year, day of the week, and hour of the day, and whether those patterns have changed over time. Three series are examined: crimes against persons, property crime, and parking. The methods follow Hyndman and Athanasopoulos, Forecasting: Principles and Practice (3rd edition), using the fpp3 packages in R. The monthly series run from July 2019 through June 2026, 84 months in all. A pattern that is not there is reported as such.

Month of the year

The first step is simply to look at the data over time, because a long-term trend or a sudden shift can be mistaken for a seasonal pattern.

Show code
monthly_ts %>%
  autoplot(n) +
  facet_grid(series ~ ., scales = "free_y") +
  labs(title = "Monthly counts", x = NULL, y = "Per month") +
  theme_report +
  theme(legend.position = "none")
Figure 12: Monthly reports and tickets, July 2019 through June 2026.

Property crime shows a long decline. Parking shows strong growth through 2025 with large month-to-month swings. Crimes against persons is comparatively flat, with considerable month-to-month noise at about 40 reports a month.

Show code
monthly_ts %>%
  gg_season(n, labels = "right") +
  labs(title = "Each year's monthly pattern", x = NULL, y = "Per month") +
  theme_report
Figure 13: Seasonal plot: each calendar year drawn as its own line across the months.

The seasonal plot overlays the years. If a seasonal pattern existed, the lines would rise and fall together in the same months. For property crime the lines are stacked by year, reflecting the decline, with no consistent shape in common. For parking the years differ so much in both level and shape that no common pattern stands out.

Show code
monthly_ts %>%
  gg_subseries(n) +
  labs(title = "Month-by-month comparison across years", x = NULL, y = "Per month") +
  theme_report
Figure 14: Subseries plot: each calendar month across the years. Blue lines show each month’s average.

The subseries plot groups the same month across all years, which makes it easier to see whether a month is consistently high or low. A month with a high average but widely scattered values, as with October for parking, reflects a few unusual years rather than a reliable pattern.

Show code
monthly_stl %>%
  autoplot() +
  labs(title = "Trend, seasonal, and remainder components") +
  theme_report
Figure 15: STL decomposition: each series separated into trend, seasonal, and remainder components.

STL decomposition separates each series into a smooth trend, a repeating yearly pattern, and what is left over. The scale bars on the left of each panel show relative size: when the seasonal component’s range is small compared with the trend and the remainder, seasonality plays a minor role.

Show code
monthly_ts %>%
  ACF(n, lag_max = 24) %>%
  autoplot() +
  labs(title = "Autocorrelation of monthly counts") +
  theme_report
Figure 16: Autocorrelation of monthly counts. A seasonal pattern appears as a spike at lag 12.

Autocorrelation measures how strongly each month resembles the months before it. A yearly pattern shows up as a spike at lag 12. For parking and property crime, where a strong trend is present, autocorrelation is high at every lag, which reflects the trend rather than seasonality; the STL results above are the better guide for those two series.

Show code
strength <- monthly_ts %>%
  features(n, feat_stl) %>%
  select(series, trend_strength, seasonal_strength_year)

peaks <- monthly_stl %>%
  as_tibble() %>%
  mutate(month_name = month(month, label = TRUE, abbr = FALSE)) %>%
  group_by(series, month_name) %>%
  summarize(effect = mean(season_year), .groups = "drop") %>%
  group_by(series) %>%
  summarize(peak = as.character(month_name[which.max(effect)]),
            low  = as.character(month_name[which.min(effect)]),
            .groups = "drop")

strength %>%
  left_join(peaks, by = "series") %>%
  mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
  rename(Series = series, `Trend strength` = trend_strength,
         `Seasonal strength` = seasonal_strength_year,
         `Peak month` = peak, `Low month` = low)
Table 9: Seasonal and trend strength (0 = none, 1 = the series is entirely that component), with peak and low months from the STL seasonal component.
Series Trend strength Seasonal strength Peak month Low month
Crimes against persons 0.43 0.47 November July
Property crime 0.82 0.25 October April
Parking 0.58 0.32 October December

Seasonal effects are modest compared with the trends. For property crime, the largest seasonal effect is about 11% of an average month, with a peak in October and a low in April; the long decline matters far more than the season. For crimes against persons, the largest seasonal effect is about 19% of an average month, with a peak in November and a low in July, but with about 42 reports a month some of that variation is chance. For parking, the peak month (October) is an average over years that differed a great deal, so it should not be read as a reliable annual pattern; the decomposition assumes the same seasonal shape every year, which fits parking poorly.

Day of the week

Show code
daily_ts %>%
  gg_subseries(n, period = "week") +
  labs(title = "Daily counts by day of the week", x = NULL, y = "Per day") +
  theme_report
Figure 17: Daily counts grouped by day of the week, July 2019 through June 2026. Blue lines show each day’s average.
Show code
ggplot(weekday_share, aes(weekday, pct / 100, color = fy_label, group = fy_label)) +
  geom_line() +
  geom_point(size = 1) +
  geom_hline(yintercept = 1 / 7, linetype = "dashed", color = "grey50") +
  facet_wrap(~ series, ncol = 1, scales = "free_y") +
  scale_y_continuous(labels = percent) +
  labs(title = "Day of the week, by fiscal year", x = NULL,
       y = "Share of the year", color = NULL) +
  theme_report
Figure 18: Share of each fiscal year’s records by day of the week. The dashed line is an even split (14.3% per day).
Show code
weekday_share %>%
  select(Series = series, Year = fy_label, weekday, pct) %>%
  mutate(pct = round(pct, 1)) %>%
  pivot_wider(names_from = weekday, values_from = pct)
Table 10: Share of each fiscal year’s records by day of the week (%).
Series Year Mon Tue Wed Thu Fri Sat Sun
Crimes against persons FY 2019-20 14.6 14.9 15.4 17.8 14.2 12.9 10.2
Crimes against persons FY 2020-21 16.7 14.2 14.4 13.7 13.7 15.1 12.3
Crimes against persons FY 2021-22 18.5 13.3 14.5 15.5 12.7 13.1 12.5
Crimes against persons FY 2022-23 16.2 13.2 14.8 13.0 18.2 9.4 15.0
Crimes against persons FY 2023-24 15.7 12.9 12.7 19.3 14.3 12.9 12.3
Crimes against persons FY 2024-25 11.2 14.3 15.9 16.9 17.7 11.6 12.4
Crimes against persons FY 2025-26 15.3 18.4 16.0 11.5 14.8 13.2 10.8
Property crime FY 2019-20 15.9 17.8 13.2 15.2 13.9 12.4 11.6
Property crime FY 2020-21 15.1 14.7 14.4 16.3 14.6 10.5 14.4
Property crime FY 2021-22 16.2 16.5 15.9 13.9 15.3 11.9 10.3
Property crime FY 2022-23 14.6 15.9 15.7 15.2 15.6 12.7 10.4
Property crime FY 2023-24 16.2 16.8 14.4 15.2 13.8 12.3 11.2
Property crime FY 2024-25 15.7 15.6 15.5 15.5 16.5 11.3 9.8
Property crime FY 2025-26 16.8 14.2 15.6 18.8 14.0 9.3 11.3
Parking FY 2019-20 14.1 13.7 19.1 12.3 13.5 17.0 10.4
Parking FY 2020-21 12.1 19.0 18.9 13.6 15.4 11.0 10.0
Parking FY 2021-22 5.0 22.5 25.0 22.7 18.4 2.5 3.8
Parking FY 2022-23 12.8 8.1 12.7 11.5 31.0 11.0 12.9
Parking FY 2023-24 16.9 15.7 17.8 11.8 16.7 10.3 10.7
Parking FY 2024-25 11.8 20.7 19.8 14.7 14.4 7.5 11.2
Parking FY 2025-26 10.7 23.9 22.8 19.8 14.9 4.6 3.2

Property crime reports are steady from year to year: each weekday takes 14% to 17% of the year’s reports and weekends 9% to 12%. Because these are reporting times, the lower weekend share may reflect when thefts are reported as much as when they occur. Crimes against persons shows no stable weekly pattern; with about 500 reports a year, shifts of three to six percentage points from one year to the next are consistent with chance. Parking follows an enforcement schedule rather than a natural pattern, and the schedule has changed from year to year. In FY 2021-22 and FY 2025-26 almost no tickets were written on weekends, while in FY 2022-23, 31% were written on Fridays.

Hour of the day

Show code
ggplot(hour_share, aes(hour, pct / 100, color = fy_label, group = fy_label)) +
  geom_line() +
  facet_wrap(~ series, ncol = 1, scales = "free_y") +
  scale_x_continuous(breaks = seq(0, 21, 3),
                     labels = c("Midnight", "3 a.m.", "6 a.m.", "9 a.m.",
                                "Noon", "3 p.m.", "6 p.m.", "9 p.m.")) +
  scale_y_continuous(labels = percent) +
  labs(title = "Hour of the day, by fiscal year", x = NULL,
       y = "Share of the year", color = NULL) +
  theme_report
Figure 19: Share of each fiscal year’s records by hour of the day.
Show code
hour_share %>%
  group_by(series, fy_label) %>%
  summarize(`Busiest hour`  = hour[which.max(pct)],
            `Busiest share (%)` = round(max(pct), 1),
            `Quietest hour` = hour[which.min(pct)],
            `Quietest share (%)` = round(min(pct), 1),
            .groups = "drop") %>%
  rename(Series = series, Year = fy_label)
Table 11: Busiest and quietest hour by series and fiscal year (hour 0 = midnight to 1 a.m.).
Series Year Busiest hour Busiest share (%) Quietest hour Quietest share (%)
Crimes against persons FY 2019-20 17 8.4 4 0.5
Crimes against persons FY 2020-21 13 8.4 4 0.2
Crimes against persons FY 2021-22 16 8.9 6 0.4
Crimes against persons FY 2022-23 16 8.2 4 0.2
Crimes against persons FY 2023-24 15 9.5 5 0.4
Crimes against persons FY 2024-25 15 8.2 3 0.4
Crimes against persons FY 2025-26 15 9.3 4 0.2
Property crime FY 2019-20 11 7.9 3 0.5
Property crime FY 2020-21 11 7.9 3 0.5
Property crime FY 2021-22 14 8.1 1 0.5
Property crime FY 2022-23 14 7.9 2 0.2
Property crime FY 2023-24 10 7.8 3 0.7
Property crime FY 2024-25 12 7.3 3 0.3
Property crime FY 2025-26 15 8.4 1 0.5
Parking FY 2019-20 10 8.3 6 0.6
Parking FY 2020-21 11 11.1 5 0.5
Parking FY 2021-22 15 16.3 5 0.3
Parking FY 2022-23 10 11.2 6 0.3
Parking FY 2023-24 15 11.1 6 0.6
Parking FY 2024-25 11 10.7 5 0.6
Parking FY 2025-26 12 15.1 5 0.1

Crimes against persons are least often reported around 4 a.m. and most often in the mid to late afternoon, and the curves for all seven years lie nearly on top of one another. Property crime reports rise sharply at 8 a.m. and stay high until about 4 p.m.; because these are the times reports were made, this likely reflects when people discover and report thefts rather than when thefts occur. Parking tickets follow a daytime work schedule, concentrated between 7 a.m. and 4 p.m. In FY 2019-20 a noticeable share of tickets was written overnight; in recent years almost none are.

Observations on the data

The following observations may be useful to the department’s records management. They are offered as practical notes from working with the data closely, and several also affected how this analysis had to be done.

The Excel export was a substantial improvement over PDF reports. It eliminated a time-consuming extraction step and the errors that come with it, and its dates arrive as true date-and-time values. The FY 2025-26 data also corrected spelling errors in call-type names that appeared in earlier records, such as “VEHCILE PARTS THEFT.”

Some locations are recorded in forms that mapping software cannot place. These include shorthand such as “SPA/BRIGHTON” for San Pablo Avenue at Brighton Avenue, typographical variants such as “JACK N BOX & CUJTTING,” and named places without a street address, such as “HARDING PARK” and “ECPD.” About 2.5% of FY 2025-26 records could not be placed on a map. Most of these were recorded in forms like those above; the rest were genuine locations outside El Cerrito. Recording a street address or a full intersection would make these records usable for location analysis.

The Ohlone Greenway is recorded under several names, including “BART PATH” in earlier years and “OHLONE GREENWAY” and “OHLONE TRL” more recently, and 99 FY 2025-26 records name the greenway without a cross street. A standard name, together with the nearest cross street, would make greenway activity easier to measure over time.

The time of first officer arrival is missing for 19.9% of FY 2025-26 records. In the earlier records, a few show response times that are not possible, such as a first arrival recorded before the call was received, or one of more than two days. Both limit response-time analysis.

In the FY 2025-26 export, addresses for sensitive call types are generally shown at the block level rather than removed, which preserves their value for neighborhood-level analysis. A small number of records in these categories appear with full addresses, which may be worth reviewing.

Technical appendix: data processing and quality control

This section describes how the data were processed. Every step is implemented in a single R script with explicit verification checks; each check prints a result, and the expected and actual values are recorded in the code. The code for every chart on this page can be shown with the “Show code” buttons.

Sources and structure

The records through June 2025 came from 13 half-year PDF reports, which begin in January 2019, extracted to seven spreadsheets. Two extraction defects were found and corrected: some addresses were truncated where they wrapped across a line in the PDF, and the first one or two rows on nearly every page were initially dropped. Both were caught by extracting the data a second, independent way, comparing the two versions with each other and with the raw PDF text, and checking all 13 files. Combining the files produced 137,570 rows. Removing blank rows and duplicate records, most of them at the boundaries between files, left 137,194 records with unique event numbers. Line breaks inside address fields, left by the PDF extraction, were removed. Records from January through June 2019 were used in the data checks described here but are outside the report’s fiscal-year period.

The FY 2025-26 export contained 23,118 records in the same six columns. Integrity checks confirmed no duplicate event numbers, no records overlapping the earlier period, and no missing values except in the first-arrival time.

Harmonizing the two sources

Several differences between the sources had to be reconciled before the periods could be compared.

Dates in the earlier data were text recorded to the minute; the new export has true date-times with seconds. The new times were rounded down to the minute so that response times are measured the same way in both periods.

Call types are matched by exact text, so a renamed category would silently drop out of any analysis. A comparison of every call-type name across the two periods found three new names. One was the corrected spelling “VEHICLE PARTS THEFT,” which was mapped to its earlier misspelled form so that both periods are counted; the other two were new non-crime categories.

Location text also changed format. The new export records addresses in a long form such as “SAN PABLO AVE & POTRERO AVE, SAN PABLO AVE & POTRERO AVE (SPA & POTRERO), EL CERRITO, CA, 94530.” Each address was reduced to its street portion and the city was re-attached; unincorporated county addresses were given their ZIP code instead of a city; and department shorthand was expanded using the same rules applied to the PDF records. This reduced 7,234 distinct address strings to 5,185 to be geocoded.

Addresses for sensitive call types were blank in the earlier data but block-level in the new export. The 17 call types that were 90% or more blank in the earlier data are excluded from the landmark comparisons in both periods so that the two are measured on the same basis. They remain in all counts that do not use location.

Geocoding

Addresses were converted to coordinates with the ArcGIS geocoder through the tidygeocoder R package. An initial geocoding pass showed that the geocoder’s confidence score indicates only that it found a text match, not that the match is correct: without constraints, “SAN PABLO/PANAMA” matched the country of Panama and “CREEKSIDE PARK” a park in Australia, both with perfect scores. Every request for the FY 2025-26 records was therefore restricted to a Bay Area bounding box from the start.

Geocoded locations then passed through three quality rules. First, any location more than five miles from the center of El Cerrito was excluded; the 85 records excluded this way were at genuine locations in other cities, such as Martinez and San Francisco. Second, when the geocoder cannot match an address inside the bounding box, it can return a generic local point that passes a distance test. These fallback points were identified as single coordinates shared by 20 or more different address strings, and 157 records on two such points were excluded. Third, addresses with no house number and no cross street, such as “KEARNEY ST” or “HARDING PARK,” can only be placed at an arbitrary point on a street. A review of shared coordinates found such records placed up to two miles from their actual location, so all 434 were excluded from maps. After these rules, 22,532 FY 2025-26 records (97.5%) had usable locations, compared with 96.9% in the earlier data.

Landmark checks also found that a correct, perfect-score match can still be wrong in the reference data: Korematsu Middle School’s address matched the school’s former location, and its coordinate was corrected by hand from an independent source. Madera Elementary was checked against a hand-pulled coordinate and found to be within normal variance.

The cross-street correction

About 19% of the PDF records give a location in the form “house number and street, followed by the nearest intersection,” for example “6400 CUTTING BLVD, SAN PABLO AVE & KEARNEY ST.” For most of these, the geocoder placed the record at the correct address. For 287 addresses covering 1,493 records, it placed the record at the intersection instead. The most affected location was Del Norte BART, where 95% of station records had been placed about 3,400 feet from the station.

Landmark All activity, before correction After correction Crime-related, before After
Del Norte BART 557 953 194 259
Community Center 1,802 1,841 381 386
El Cerrito High School 1,235 1,248 324 329
Library 1,029 989 296 290

These counts cover all of the PDF records, January 2019 through June 2025. The problem was found by comparing, for every house address that appears in both periods, the earlier coordinate with the new one. Of 16,170 matched records, the median difference was zero, but 9.2% differed by more than 500 feet. The differences were concentrated: 287 addresses accounted for all of them, and the top 10 accounted for 43%. The newer coordinates were validated against verified landmark coordinates for every address near a landmark, and against a Google Maps check for the largest remaining address, where the new coordinate was 54 feet from the actual location and the old one 1,015 feet. The comparison was limited to El Cerrito addresses, because an initial version that did not apply this limit matched a Pinole address to a street of the same name in El Cerrito. About 10,000 records in this address form have no counterpart in the new data and could not be checked; in the matched sample, 91% were within 500 feet.

Categories and fiscal years

All 144 call types were assigned to the seven reader categories and to subcategories. Checks confirmed that every call type in both periods has exactly one assignment and that the mapping contains no unused entries. The maps and landmark tables in the section on where activity happens use a separate 52-type list of crime-related calls. It excludes administrative activity such as security checks and traffic stops, and financial crimes such as identity theft, which are usually logged at the victim’s home rather than where a crime occurred. It includes some non-crime activity, most notably parking violations. One type is spelled differently in the two sources (“VEHCILE PARTS THEFT” and “VEHICLE PARTS THEFT”); both spellings are counted.

Seasonal analysis

Monthly series were built as tsibble objects with gaps filled as zeros, and each was confirmed to contain all 84 months from July 2019 through June 2026. Seasonal and trend strength were measured with STL decomposition (feasts::feat_stl), and peak and low months were read directly from the seasonal component. Year-to-year comparisons of weekday and hourly patterns use each year’s percentage distribution, so that years with different volumes can be compared on shape. A check for placeholder timestamps found only 0.02% of records at exactly midnight, well within normal.

Limitations

Records reflect what was reported and logged, not verified events. Changes in reporting behavior, recording practice, or data format can affect counts. Categories are the analyst’s assignments. Locations are approximate, some records could not be mapped, and the “next to the greenway” measure captures only records where the greenway was noted as a cross street. Times are when calls were received, not when incidents occurred. Several subcategories have small annual counts, where year-to-year changes may reflect chance.

Tools

R 4.x with tidyverse, readxl, tidygeocoder, geosphere, sf, ggspatial, ggrepel, and fpp3 (tsibble, feasts, fable). The pipeline script contains every processing step and verification check described here.