Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
506 lines (404 sloc) 24.9 KB
---
title: Fun with parking tickets
author: Jens von Bergmann
date: '2019-12-09'
slug: fun-with-parking-tickets
categories:
- Vancouver
- Transportation
tags: []
description: "Looking for excuses to showcase my {VancouvR} R package to access Vancouver Open Data."
featured: ''
images: ["https://doodles.mountainmath.ca/posts/2019-12-09-fun-with-parking-tickets_files/figure-html/fire_hydrant_ticket_map-1.png"]
featuredalt: ""
featuredpath: ""
linktitle: ''
type: "post"
math: true
blackfriday:
fractions: false
hrefTargetBlank: true
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(
echo = FALSE,
message = FALSE,
warning = FALSE,
fig.width=8,
cache = TRUE
)
library(tidyverse)
library(VancouvR)
library(sf)
short_months_names <- c("Jan","Feb","Mar","Apr","May","Jun","Jul", "Aug","Sep","Oct","Nov", "Dec")
plot_theme <- list(
theme_light(),
labs(caption="MountainMath, Vancouver Open Data via VancouvR")
)
normalize_addresses <- function(data){
data %>%
mutate(original_street=street) %>%
mutate(street=toupper(street)) %>%
mutate(street=gsub("\\.","",street)) %>%
mutate(street=gsub("^EAST ","E ",street)) %>%
mutate(street=gsub("^WEST ","W ",street)) %>%
mutate(street=case_when(grepl("^E |^S |^W |^N ",street) ~ street,
grepl(" W$",street) ~ paste0("W ",gsub(" W$","",street)),
grepl(" E$",street) ~ paste0("E ",gsub(" E$","",street)),
grepl(" S$",street) ~ paste0("S ",gsub(" S$","",street)),
grepl(" N$",street) ~ paste0("N ",gsub(" N$","",street)),
TRUE ~ street)) %>%
mutate(street=street %>%
gsub("\\.","",.) %>%
gsub(" AVE$"," AV",.) %>%
gsub(" AVE "," AV ",.) %>%
gsub(" DR$"," DRIVE",.) %>%
#gsub(" DRIVE$"," DR",.) %>%
gsub(" PLACE$"," PL",.) %>%
gsub(" BLVD$"," BOULEVARD",.) %>%
gsub(" CR$"," CRESCENT",.) %>%
gsub(" S$"," SOUTH",.) %>%
gsub(" N$"," NORTH",.)) %>%
mutate(street=gsub("^N DUNLEVY AV$","DUNLEVY AV",street)) %>%
mutate(Address=paste0(block," ",street))
}
cov <- cancensus::get_census("CA16",regions=list(CSD="5915022"),geo_format = "sf")
vector_tiles <- cancensusHelpers::simpleCache(
cancensusHelpers::get_vector_tiles(bbox=sf::st_bbox(cov %>% sf::st_transform(4326))),
"cov_vector_tiles")
# vector tiles return all layers (roads, water, buildings, etc) in a list
roads <- rmapzen::as_sf(vector_tiles$roads) %>% filter(kind != "ferry")
water <- rmapzen::as_sf(vector_tiles$water)
bbox=sf::st_bbox(cov)
pretty_table <- function(data){
data %>%
mutate_if(is.character,~gsub("\n"," -- ",.)) %>%
knitr::kable() %>%
kableExtra::kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = F,format="html")
}
```
Almost three years ago I ran the numbers to identify "Vancouver's most lucrative fire hydrant".
{{< tweet 826883926614421507 >}}
<img src="https://raw.githubusercontent.com/mountainMath/VancouvR/master/images/VancouvR-sticker.png" style="float:right;width:40%;">
Being a card-carrying Shoupista it's high time for me to do an update. And looking back I can't help but realize how my approach to data analysis, even about such trivial things as parking tickets, has changed since then. Back then I scripted makeshift analysis in a general purpose language. Nowadays I work in R or Python and am much more structured in my approach, with emphasis on reproducibility and transparency. And this included the entire pipeline, from data acquisition, to data cleaning, data analysis and visualization.
In this case, we are working with City of Vancouver Open Data, and data acquisition happens through my relatively new [`VancouvR` package](https://mountainmath.github.io/VancouvR/index.html) that ties into the new City of Vancouver Open Data API and is now on CRAN. Usually I hide the code from the post but make it available on GitHub in case people are interested, but this time around I am leaving some of the code blocks visible to showcase the `VancouvR` package and give people an idea what it looks like, and as advertisement for more people to come work with Vancouver data.
First up, let's check for parking ticket related datasets.
```{r echo=TRUE}
library(tidyverse)
library(VancouvR)
ticket_datasets <- search_cov_datasets("parking tickets")
ticket_datasets %>%
select(dataset_id,title) %>%
pretty_table()
```
Before accessing the data it's often useful to take a peek at the metadata to get and overview of what to expect from the dataset.
```{r echo=TRUE}
get_cov_metadata(ticket_datasets$dataset_id %>% first) %>%
pretty_table()
```
We are only interested in parking tickets related to fire hydrants. One great feature of the new City of Vancouver Open Data API is that we can do some basic summary statistics on their server, greatly simplifying and speeding up the analysis. The API accepts some SQL-like dialect that allows to specify basic `where` and `group_by` clauses, as well `select` statements that in our R package default to simply counting the number of rows.
```{r echo=TRUE}
agg <- aggregate_cov_data("parking-tickets-2017-2019",
where = "infractiontext LIKE 'FIRE'",
group_by = "bylaw,section,infractiontext,status")
agg %>% pretty_table()
```
We learn that `r filter(agg,status=="IS")$count` non-voided or disputed tickets have been issued for "STOP WITHIN 5 METRES OF A FIRE HYDRANT". Armed with that knowledge, we now query more detailed data on all these tickets. One hiccup is that the datasets for different time frames are inconsistently formatted, turning off automatic type-casting based on the inconsistent metadata makes it easier to work with the data.
```{r echo=TRUE}
fire_hydrant_tickets <- ticket_datasets$dataset_id %>%
lapply(function(ds)get_cov_data(ds, where = "section = '17.2(C)' and status = 'IS'",cast_type=FALSE)) %>%
bind_rows
fire_hydrant_tickets %>%
ggplot(aes(x=year)) +
geom_bar(fill="steelblue") +
scale_y_continuous(labels=scales::comma) +
plot_theme +
labs(title="City of Vancouver parking tickets",x="",y="Number of issued tickets")
```
So the City has issued around 3,000 tickets a year for parking within 5 metres of a fire hydrant. The last data entry we have for 2019 is from `r fire_hydrant_tickets %>% filter(year=="2019") %>% pull(entrydate) %>% max`, so there is still time for that number to grow. The [Parking Bylaw](https://bylaws.vancouver.ca/2849c.PDF) calls for a $100 penalty for parking within 5 meteres of a fire hydrant, as measured along the curb from the closest point to the hydrant. So that comes out to about $300k a year in fines for blocking fire hydrants in the City of Vancouver.
Next up, lets check the top 5 most heavily ticketed fire hydrants in our 9 year period.
```{r echo=TRUE}
fire_hydrant_tickets %>%
count(block,street) %>%
top_n(5) %>%
arrange(-n) %>%
pretty_table()
```
Looking at the list we immediately notice something odd. Numbers 2 and 4 appear to be the same block and street, just written differently. The addresses aren't properly normalized, we will have to do some data cleaning work first. 麻煩! We hide the code for that behind a function call.
```{r echo=TRUE}
top_hydrants <- fire_hydrant_tickets %>%
normalize_addresses() %>%
count(Address) %>%
top_n(5)
top_hydrants %>% arrange(-n) %>% pretty_table()
```
<img src="/images/W_40th_fire_hydrant.png" style="width:50%;float:right">
The clear winner is the one on the 2100 block of W 40TH AVE. Checking [Google Street View](https://www.google.ca/maps/place/2150+W+40th+Ave,+Vancouver,+BC+V6M+1W5/@49.2355542,-123.155923,3a,75y,295.03h,84t/data=!3m6!1e1!3m4!1scoq6Uv8Vvi4qkn3j61Eb3Q!2e0!7i16384!8i8192!4m13!1m7!3m6!1s0x5486737a4fb01651:0xc469fd2de990469d!2s2150+W+40th+Ave,+Vancouver,+BC+V6M+1W5!3b1!8m2!3d49.2352754!4d-123.1566618!3m4!1s0x5486737a4fb01651:0xc469fd2de990469d!8m2!3d49.2352754!4d-123.1566618), there is only one on the block. It was hard to find because -- two cars blocked the view on it.
Next up are the ones on the [1100 block of Haro St](https://www.google.ca/maps/@49.2846472,-123.1270377,3a,75y,180.87h,89.92t/data=!3m7!1e1!3m5!1s0LfohWAkdxHFE8zeGx8xgg!2e0!5s20150501T000000!7i13312!8i6656), closely followed by the [400 block on Keefer St](https://www.google.ca/maps/@49.2846587,-123.1270505,3a,75y,180.87h,89.92t/data=!3m6!1e1!3m4!1sUxShPIv9pAxcFUerFjX6aw!2e0!7i16384!8i8192).
To see how things have evolved over time we can check how they fared over the years.
```{r echo=TRUE}
fire_hydrant_tickets %>%
normalize_addresses() %>%
count(Address,year) %>%
group_by(year) %>%
top_n(1) %>%
arrange(year,-n) %>%
pretty_table()
```
The winner each year is from our overall top 5 list. Current front runner for 2019 is the one on the 5600 block of Ormidale St, which deserves a closer look.
```{r echo=TRUE}
fire_hydrant_tickets %>%
normalize_addresses() %>%
filter(Address=="5600 ORMIDALE ST") %>%
mutate(month=strftime(entrydate,"%m")) %>%
mutate(Date=as.Date(paste0(year,"-",month,"-15"))) %>%
ggplot(aes(x=Date)) +
geom_bar(fill="brown") +
plot_theme +
labs(title="Fire hydrant on the 5600 block of Ormidale St",x="Month",y="Number of tickets")
```
It looks like the hydrant was a fairly low-key affair until 2014, when it dropped off the map and then took off around 2018. A [quick check with Google Street View](https://www.google.ca/maps/place/5650+Ormidale+St,+Vancouver,+BC+V5R+4P9/@49.2332689,-123.0248187,3a,75y,66.64h,85.76t/data=!3m6!1e1!3m4!1susroxeU5VcCUA8FuH-KDvg!2e0!7i16384!8i8192!4m5!3m4!1s0x5486768d71570e31:0xf10ee71183a0f600!8m2!3d49.233085!4d-123.0245468) indicates that this sits in front of a new development. Checking through the timeline, the site [shows a house in May 2009](https://www.google.ca/maps/place/5650+Ormidale+St,+Vancouver,+BC+V5R+4P9/@49.2332266,-123.0248471,3a,75y,66.64h,85.76t/data=!3m7!1e1!3m5!1sHjs5BZw_U3zbRxVIyNkOQg!2e0!5s20090501T000000!7i13312!8i6656!4m5!3m4!1s0x5486768d71570e31:0xf10ee71183a0f600!8m2!3d49.233085!4d-123.0245468), which has been [torn down by June 2012](https://www.google.ca/maps/place/5650+Ormidale+St,+Vancouver,+BC+V5R+4P9/@49.2332058,-123.0248472,3a,75y,66.64h,85.76t/data=!3m7!1e1!3m5!1s_Can0hgwYMm21cA3YQ_nUg!2e0!5s20120601T000000!7i13312!8i6656!4m5!3m4!1s0x5486768d71570e31:0xf10ee71183a0f600!8m2!3d49.233085!4d-123.0245468), although it is still possible to illegally park in front of it. By [May 2014](https://www.google.ca/maps/place/5650+Ormidale+St,+Vancouver,+BC+V5R+4P9/@49.2332318,-123.0248536,3a,75y,69.26h,82.76t/data=!3m7!1e1!3m5!1sRR2gC_GphGkZ1mRsGop3gw!2e0!5s20140501T000000!7i13312!8i6656!4m5!3m4!1s0x5486768d71570e31:0xf10ee71183a0f600!8m2!3d49.233085!4d-123.0245468) and [July 2014](https://www.google.ca/maps/place/5650+Ormidale+St,+Vancouver,+BC+V5R+4P9/@49.2332318,-123.0248536,3a,75y,69.26h,82.76t/data=!3m7!1e1!3m5!1sRR2gC_GphGkZ1mRsGop3gw!2e0!5s20140501T000000!7i13312!8i6656!4m5!3m4!1s0x5486768d71570e31:0xf10ee71183a0f600!8m2!3d49.233085!4d-123.0245468) the neighbouring house is gone too and there is a hole in the ground with heavy machinery digging a foundation, but cars can still illegally park there. In [June 2015](https://www.google.ca/maps/place/5650+Ormidale+St,+Vancouver,+BC+V5R+4P9/@49.233219,-123.0248525,3a,75y,81.67h,81.35t/data=!3m7!1e1!3m5!1sUVboNiTGz8TFUBdb-dR-QQ!2e0!5s20150601T000000!7i13312!8i6656!4m5!3m4!1s0x5486768d71570e31:0xf10ee71183a0f600!8m2!3d49.233085!4d-123.0245468) and [May 2016](https://www.google.ca/maps/place/5650+Ormidale+St,+Vancouver,+BC+V5R+4P9/@49.233219,-123.0248525,3a,75y,81.67h,81.35t/data=!3m7!1e1!3m5!1sUVboNiTGz8TFUBdb-dR-QQ!2e0!5s20160501T000000!7i13312!8i6656!4m5!3m4!1s0x5486768d71570e31:0xf10ee71183a0f600!8m2!3d49.233085!4d-123.0245468) it's a full-on construction site with no options to park illegally any more. [August 2017](https://www.google.ca/maps/place/5650+Ormidale+St,+Vancouver,+BC+V5R+4P9/@49.233219,-123.0248525,3a,75y,81.67h,81.35t/data=!3m7!1e1!3m5!1sUVboNiTGz8TFUBdb-dR-QQ!2e0!5s20170801T000000!7i13312!8i6656!4m5!3m4!1s0x5486768d71570e31:0xf10ee71183a0f600!8m2!3d49.233085!4d-123.0245468) marks the end of construction and the first people seem to have moved in. And the parking tickets start ramping up, with many more people trying to park on the street now.
Lastly, let's get a high-level view on all the parking tickets issued throughout the city. But here things get a little ugly as we have to first geocode the blocks, and we will hide the code from now on.
```{r}
addresses <- get_cov_data("property-addresses",format="geojson") %>%
mutate(street=std_street,
#block=str_pad(floor(as.integer(civic_number)/100),3,pad="0")) %>%
block=(floor(as.integer(civic_number)/100)*100) %>% as.character) %>%
group_by(street,block) %>%
summarise() %>%
st_centroid()
meters <- get_cov_data("parking-meters",format="geojson")
parking_tickets <- get_cov_data("parking-tickets-2017-2019") %>%
normalize_addresses()
unique_ticket_locations <- parking_tickets %>%
select(block,street) %>%
unique
geocode_unique_ticket_locations <- function(unique_ticket_locations,refresh=FALSE) {
parking_blocks <- unique_ticket_locations %>%
select(block,street) %>% unique
lookup <- parking_blocks %>%
left_join(addresses %>% mutate(block=as.integer(block)),by=c("block","street")) %>%
st_sf()
missed_lookups <- lookup %>%
filter(st_is_empty(.)) %>%
st_set_geometry(NULL) %>%
select(block,street)
acceptable_matches <- c("BLOCK","SITE","CIVIC_NUMBER","ADDRESS")
geocode_missed_lookups <- function(missed_lookups,refresh=FALSE){
geocoded_path <- file.path(getOption("custom_data_path"),"geocoded_parking_tickets.geojson")
if (refresh || !file.exists(geocoded_path)) {
geocoded_lookups <- missed_lookups %>%
mutate(addressString=paste0(as.integer(block)+50," ",street,", Vancouver")) %>%
mountainmathHelpers::geocode(localities = "Vancouver") %>%
mutate(X=ifelse(matchPrecision %in% acceptable_matches,NA,X)) %>%
mutate(Y=ifelse(matchPrecision %in% acceptable_matches,NA,Y))
failed_geocode <- geocoded_lookups %>%
filter(matchPrecision %in% acceptable_matches) %>%
select(street,block,addressString) %>%
mutate(original_block=block) %>%
mutate(block=ifelse(as.integer(block)>10000,(as.integer(block)/10) %>% as.character,block)) %>%
mutate(addressString=paste0(as.integer(block)+50," ",street,", Vancouver")) %>%
mountainmathHelpers::geocode(localities = "Vancouver") %>%
mutate(X=ifelse(matchPrecision %in% acceptable_matches,X,NA)) %>%
mutate(Y=ifelse(matchPrecision %in% acceptable_matches,Y,NA))
all_geocode <- geocoded_lookups %>%
filter(!(matchPrecision %in% acceptable_matches)) %>%
bind_rows(failed_geocode %>% mutate(block=original_block)) %>%
select(street,block,X,Y,fullAddress,matchPrecision,score) %>%
mutate(X=ifelse(matchPrecision %in% acceptable_matches,X,NA)) %>%
mutate(Y=ifelse(matchPrecision %in% acceptable_matches,Y,NA)) %>%
st_as_sf(coords=c("X","Y"),crs = 4326, agr = "constant",na.fail = FALSE)
write_sf(all_geocode,geocoded_path,delete_dsn=TRUE)
} else {
all_geocode <- read_sf(geocoded_path)
}
all_geocode
}
final_lookup <- lookup %>%
filter(!st_is_empty(.)) %>%
mutate(fullAddress=paste0(block," ",street,", Vancouver, BC"),matchPrecision="ADDRESS",score=100) %>%
rbind(geocode_missed_lookups(missed_lookups,refresh=refresh))
final_lookup
}
geo_lookup <- geocode_unique_ticket_locations(unique_ticket_locations) %>%
mutate(location_id = paste0("location_",row_number())) %>%
mutate(successful_geocode=!st_is_empty(.))
```
```{r fire_hydrant_ticket_map, fig.height=7}
fire_hydrant_summary <- fire_hydrant_tickets %>%
normalize_addresses() %>%
mutate(block=as.integer(block)) %>%
group_by(street,block) %>%
count
map_data <- geo_lookup %>% inner_join(fire_hydrant_summary,by=c("street","block")) %>%
filter(st_intersects(.,cov,sparse = FALSE) %>% unlist)
ggplot(map_data %>% filter(n>=5)) +
geom_sf(data = water, fill = "lightblue", colour = NA) +
geom_sf(size=1.5,aes(color=n),alpha=0.7) +
geom_sf(data=roads %>% filter(kind %in% c("highway", "major_road")),size=0.1) +
scale_color_viridis_c(option = "inferno",trans="log",breaks=c(1,5,10,25,50,100,200,400,800)) +
plot_theme +
coord_sf(datum=NA, xlim=c(bbox$xmin,bbox$xmax), ylim=c(bbox$ymin,bbox$ymax)) +
guides(color = guide_colorbar(barheight = unit(4, "in"), barwidth = unit(0.2, "in"))) +
labs(title="Fire hydrants attracting at least 5 parking tickets 2012-2019",color="# tickets")
```
Especially the fire hydrants that are attracting lots of tickets should probably receive a review by the engineering department. While people should pay more attention to where they park, there are some straight-forward ways to make things easier. Simply paining the curb red will probably fix this for most hydrants and make sure they are free of obstructions and easy to access in case of a fire.
## More parking tickets
Fire hydrants are just one way to get a parking ticket. We can of course continue this and see what areas got the most overall tickets. And for what reason. We will concentrate on the tickets issued 2017 to 2019.
```{r}
vancouver_hex <- cov %>%
st_transform(26910) %>%
st_make_grid(cellsize = 500,square=FALSE,what="polygons") %>%
st_sf %>%
mutate(hex_id=paste0("HEX_",row_number()))
vancouver_hex_clipped <- vancouver_hex %>%
st_intersection(cov %>% st_transform(st_crs(vancouver_hex))) %>%
st_cast("POLYGON") %>%
mutate(clipped_hex_id=paste0("HEX_CLIPPED_",row_number()))
hex_ticket_lookup <- geo_lookup %>%
st_transform(26910) %>%
st_join(vancouver_hex)
clipped_hex_ticket_lookup <- geo_lookup %>%
st_transform(26910) %>%
st_join(vancouver_hex_clipped)
# geocoded_tickets <- parking_tickets %>%
# mutate(link=paste0(block," ; ",street)) %>%
# left_join(hex_ticket_lookup %>% mutate(link=paste0(block," ; ",street)) %>% select(-block,-street),by=c("link")) %>%
# st_sf
```
```{r}
summary_tickets <- parking_tickets %>%
filter(status=="IS") %>%
group_by(block,street,year) %>%
count() %>%
inner_join(clipped_hex_ticket_lookup %>% st_set_geometry(NULL),by=c("block","street")) %>%
group_by(location_id,clipped_hex_id,year) %>%
summarize(n=sum(n))
summary_tickets_hex <- summary_tickets %>%
group_by(clipped_hex_id,year) %>%
summarize(n=sum(n))
map_data <- vancouver_hex_clipped %>%
inner_join(summary_tickets_hex,by="clipped_hex_id")
bbox2<-st_bbox(cov %>% st_transform(st_crs(map_data)))
ggplot(map_data) +
geom_sf(data = water, fill = "lightblue", colour = NA) +
geom_sf(aes(fill=n)) +
geom_sf(data=roads %>% filter(kind %in% c("highway", "major_road")),size=0.1) +
facet_wrap("year") +
scale_fill_viridis_c(trans="log",breaks=c(10,100,500,5000),labels=scales::comma) +
geom_sf(data=cov,fill=NA) +
plot_theme +
theme(legend.position = "bottom") +
guides(fill = guide_colorbar(barwidth = unit(6, "in"), barheight = unit(0.15, "in"))) +
#guide_legend(keywidth=10) +
coord_sf(datum=NA, xlim=c(bbox2$xmin,bbox2$xmax), ylim=c(bbox2$ymin,bbox2$ymax)) +
labs(title="Number of parking tickets",fill="")
```
The distribution of tickets across the city is fairly consistent across years, with total ticket counts peaking in downtown, as well as the central Broadway corridor and in Kits.
To understand these patterns better, it is useful to look at the top reasons parking tickets have been issued.
```{r}
parking_tickets %>%
filter(status=="IS") %>%
count(infractiontext) %>%
top_n(10) %>%
mutate(infractiontext=str_wrap(infractiontext,width=60)) %>%
ggplot(aes(x=reorder(infractiontext,n),y=n)) +
geom_bar(stat="identity",fill="steelblue") +
plot_theme +
scale_y_continuous(labels=scales::comma) +
coord_flip() +
labs(title="Top 10 reasons for tickets",x="",y="Number of tickets")
```
The presence of parking meters clearly plays a role in parking tickets with a total of `r scales::comma(parking_tickets %>% filter(status=="IS",grepl("METER",infractiontext)) %>% nrow())` out of the `r scales::comma(parking_tickets %>% filter(status=="IS") %>% nrow)` infractions referencing some kind of violation involving a parking meter.
```{r}
summary_meters <- meters %>%
st_transform(st_crs(vancouver_hex_clipped)) %>%
select(geometry) %>%
st_join(vancouver_hex_clipped %>% select(clipped_hex_id)) %>%
st_set_geometry(NULL) %>%
group_by(clipped_hex_id) %>%
count()
meters_map_data <- vancouver_hex_clipped %>%
inner_join(summary_meters,by="clipped_hex_id")
ggplot(meters_map_data) +
geom_sf(data = water, fill = "lightblue", colour = NA) +
geom_sf(aes(fill=n)) +
geom_sf(data=roads %>% filter(kind %in% c("highway", "major_road")),size=0.1) +
scale_fill_viridis_c(option="inferno")+#trans="log") +
geom_sf(data=cov,fill=NA) +
plot_theme +
coord_sf(datum=NA, xlim=c(bbox2$xmin,bbox2$xmax), ylim=c(bbox2$ymin,bbox2$ymax)) +
guides(fill = guide_colorbar(barheight = unit(3, "in"), barwidth = unit(0.2, "in"))) +
labs(title="Number of parking meters",fill="")
```
There seems to be a clear relationship between the number of tickets and the number of parking meters in each area. We can normalize the meter-related parking tickets by the number of meters in each area to give us a count on the average number of tickets per meter in each of the areas.
```{r}
summary_tickets <- parking_tickets %>%
filter(status=="IS",grepl("METERED",infractiontext)) %>%
group_by(block,street,year) %>%
count() %>%
inner_join(clipped_hex_ticket_lookup %>% st_set_geometry(NULL),by=c("block","street")) %>%
group_by(location_id,clipped_hex_id,year) %>%
summarize(n=sum(n))
summary_tickets_hex <- summary_tickets %>%
group_by(clipped_hex_id,year) %>%
summarize(n=sum(n))
pre_map_data <- vancouver_hex_clipped %>%
inner_join(summary_tickets_hex,by="clipped_hex_id") %>%
filter(year==2019) %>%
st_set_geometry(NULL)
map_data <- meters_map_data %>%
select(clipped_hex_id,meters_count=n) %>%
left_join(pre_map_data %>% select(clipped_hex_id,ticket_count=n),by="clipped_hex_id") %>%
mutate(n=ticket_count/meters_count) %>%
mutate(n=ifelse(n>=0.45,n,NA))
ggplot(map_data) +
geom_sf(data = water, fill = "lightblue", colour = NA) +
geom_sf(aes(fill=n)) +
geom_sf(data=roads %>% filter(kind %in% c("highway", "major_road")),size=0.1) +
scale_fill_viridis_c(trans="log",breaks=c(0.5,1,5,10,25,50,100,200),labels=scales::comma) +
geom_sf(data=cov,fill=NA) +
plot_theme +
guides(fill = guide_colorbar(barheight = unit(3, "in"), barwidth = unit(0.2, "in"))) +
coord_sf(datum=NA, xlim=c(bbox2$xmin,bbox2$xmax), ylim=c(bbox2$ymin,bbox2$ymax)) +
labs(title="Average number of parking tickets per meter",fill="")
```
This shows a much more uniform pattern, with a clear outlier in Strathcona which might be worth looking into further.
```{r}
ticket_summary <- parking_tickets %>%
filter(status=="IS",!grepl("METERED",infractiontext)) %>%
group_by(street,block,year) %>%
count
ticket_summary_locations <- geo_lookup %>% inner_join(ticket_summary,by=c("street","block"))
```
Now that we have some understanding of meter-related tickets, we can take a look at the remaining non-meter related tickets.
```{r}
parking_tickets %>%
filter(status=="IS",!grepl("METERED",infractiontext)) %>%
count(infractiontext) %>%
top_n(10) %>%
mutate(infractiontext=str_wrap(infractiontext,width=60)) %>%
ggplot(aes(x=reorder(infractiontext,n),y=n)) +
geom_bar(stat="identity",fill="steelblue") +
plot_theme +
scale_y_continuous(labels=scales::comma) +
coord_flip() +
labs(title="Top 10 reasons for tickets",x="",y="Number of tickets")
```
The remaining tickets distribute quite well over a range of categories.
```{r fig.height=7}
map_data <- ticket_summary_locations %>% filter(st_intersects(.,cov,sparse = FALSE) %>% unlist)
ggplot(map_data %>% filter(year==2019,n>=100)) +
geom_sf(data = water, fill = "lightblue", colour = NA) +
geom_sf(size=2,aes(color=n),alpha=0.7) +
geom_sf(data=roads %>% filter(kind %in% c("highway", "major_road")),size=0.1) +
scale_color_viridis_c(option = "inferno",trans="log",breaks=c(100,200,500)) +
plot_theme +
guides(color = guide_colorbar(barheight = unit(4, "in"), barwidth = unit(0.2, "in"))) +
coord_sf(datum=NA, xlim=c(bbox$xmin,bbox$xmax), ylim=c(bbox$ymin,bbox$ymax)) +
labs(title="Blocks with over 100 non-meter related tickets so far in 2019",color="# tickets")
```
Geographically, highly ticketed blocks cluster in the downtown core and surrounding areas, and spill out along commercial corridors. One can't help but notice the correlation with meter locations, possibly due to ticketing officers focusing their efforts on those areas.
## Next steps
That's a wrap for tonight's quick run-through on how to use our new-ish `VancouvR` package to easily access Vancouver Open Data. As usual, the code is [available for anyone to download and adapt for their own purposes](https://github.com/mountainMath/doodles/blob/master/content/posts/2019-12-09-fun-with-parking-tickets.Rmarkdown).
You can’t perform that action at this time.