Philly 311 Call Data Analysis

Set Up

Datasets used to analyze Philly 311 calls include:

  1. Philly 311 Dataset, ranged from 12/08/2014 to 09/28/2016;

  2. Philadelphia 2015 crime dataset, data source: OpenDataPhilly;

  3. Philadelphia neighborhood boundary, data source: OpenDataPhilly;

  4. 2015 ACS 5-year Estimates, data source: U.S. Census Bureau

# Install packages
library(tidyverse)
library(dplyr)
library(ggplot2)
library(scales)
library(stringr)
library(tidycensus)
library(sf)
library(tigris) 
library(viridis)
library(GISTools)
library(leaflet)
options(tigris_class = "sf")
options(tigris_use_cache = TRUE)

# ggplot theme
theme.graph <- function(base_size = 11, base_family = "Arial", lines_lwd = 0.50, plot_grid = TRUE, axis_font = base_family, title_size = base_size*1.5, legend_size = base_size*0.8,
                        bg_col = "white",title_font = base_family , base_col  = "black", axis_lines = TRUE,
                        minor_grid = ifelse(plot_grid, TRUE, FALSE), vert_grid = ifelse(plot_grid, TRUE, FALSE), ticks_type = "outer", horz_grid = ifelse(plot_grid, TRUE, FALSE), alpha_leg = 0.1, bord_size = 0,
                        legend_bg = "white", strip_bg = "white", grid_thick = 1,
                        grid_type = "solid", ticks_xy  = "xy", grid_cols = c("grey50", "grey70")){
  theme_bw()+
    ggplot2::theme(
      plot.margin = grid::unit(c(1, 1, .5, .7), "cm"),
      text = ggplot2::element_text(family = base_family, size = base_size),
      axis.line =  element_line(size = ifelse(axis_lines, grid::unit(lines_lwd, "mm"),0), color = "black"),
      axis.ticks.length = grid::unit(ifelse(ticks_type == "outer", 0.15, -0.15), "cm"),
      axis.ticks.x =  element_line(size = ifelse(stringr::str_detect(ticks_xy, "x"), grid::unit(lines_lwd, "cm"),0), color = "black"),
      axis.ticks.y =  element_line(size = ifelse(stringr::str_detect(ticks_xy, "y"), grid::unit(lines_lwd, "cm") ,0), color = "black"),
      axis.text.x = ggplot2::element_text(angle=45,hjust= .5, vjust= .5, size = base_size, colour = base_col , family = axis_font,margin=margin(ifelse(ticks_type == "inner", 11, 5),5,10,5,"pt")),
      axis.text.y = ggplot2::element_text(size = base_size, colour = base_col , family = axis_font, margin=margin(5,ifelse(ticks_type == "inner", 11, 5),10,5,"pt")),
      axis.title.y = ggplot2::element_text(angle=0,vjust = .5, margin = margin(r = 15),size =  base_size, colour = base_col , family = axis_font),
      axis.title.x = ggplot2::element_text(size = base_size,colour = base_col ,vjust = -.5, family = axis_font),
      panel.background = ggplot2::element_rect(fill = bg_col),
      plot.background = ggplot2::element_rect(fill = bg_col),
      panel.border = ggplot2::element_rect(colour = "black", fill=NA, size = bord_size),
      panel.grid.major.x = ggplot2::element_line(linetype = grid_type,colour = ifelse(vert_grid, grid_cols[1],bg_col), size = ifelse(vert_grid,0.25 * grid_thick, 0)),
      panel.grid.minor.x = ggplot2::element_line(linetype = grid_type,colour = ifelse(vert_grid, ifelse(minor_grid, grid_cols[2 - (length(grid_cols) == 1)   ],bg_col),bg_col), size = ifelse(vert_grid,0.15* grid_thick, 0)),
      panel.grid.major.y = ggplot2::element_line(linetype = grid_type,colour = ifelse(horz_grid, grid_cols[1],bg_col), size = ifelse(horz_grid,0.25* grid_thick, 0)),
      panel.grid.minor.y = ggplot2::element_line(linetype = grid_type,colour = ifelse(horz_grid, ifelse(minor_grid, grid_cols[2 - (length(grid_cols) == 1)  ],bg_col),bg_col), size = ifelse(horz_grid,0.15* grid_thick, 0)),
      plot.title = ggplot2::element_text(face="bold",vjust = 2, colour = base_col , size = title_size, family = title_font),
      legend.background = ggplot2::element_rect(fill = scales::alpha(legend_bg, alpha_leg)), legend.key = ggplot2::element_blank(),
      legend.text = ggplot2::element_text(size = legend_size, family = base_family),
      legend.title = element_blank(),
      strip.background =  ggplot2::element_rect(colour = strip_bg, fill = strip_bg),
      strip.text.x = ggplot2::element_text(size = base_size + 1),
      strip.text.y = ggplot2::element_text(size = base_size + 1)
    )
}


theme.widegraph<-theme.graph()+
  theme(legend.position = c(.9,.55)) +
  theme(axis.text.x = element_text(angle = 0, hjust = 1, vjust =.4))

theme.map <-theme(text = element_text(family = "Arial"))+
                  theme(plot.title =element_text(size=11*1.5),
                  plot.subtitle = element_text(size=11),
                  plot.caption = element_text(size = 8),
                  axis.line=element_blank(),
                  axis.text.x=element_blank(),
                  axis.text.y=element_blank(),
                  axis.ticks=element_blank(),
                  axis.title.x=element_blank(),
                  axis.title.y=element_blank(),
                  panel.background=element_blank(),
                  panel.border=element_blank(),
                  panel.grid.major=element_line(colour = 'transparent'),
                  panel.grid.minor=element_blank(),
                  legend.direction = "vertical", 
                  legend.position = "right",
                  plot.margin = margin(1, 1, 1, 1, 'cm'),
                  legend.key.height = unit(1, "cm"), legend.key.width = unit(0.2, "cm"))

Characteristics of Philly 311 Calls

Request Time

The Analysis of Philly 311 calls shows that most of service requests are from 9 am to 3 pm during the weekdays, especially on Monday and Tuesday between 9 am and 10 am. Potential Reasons include 311 Call center is closed during weekends and request sent during weekends through mobile app will be transferred to the department together on Monday.

phlcalls$request_date<-substr(phlcalls$requesteddatetime,1,10)
phlcalls$request_date<-as.Date(phlcalls$request_date,"%m/%d/%Y")
phlcalls$request_weekday<-weekdays(phlcalls$request_date)
phlcalls$request_time<-substr(phlcalls$requesteddatetime,12,22)
phlcalls$request_time<-strptime(phlcalls$request_time, format="%I:%M:%S %p")
phlcalls$request_time<-substr(phlcalls$request_time,12,19)
phlcalls$request_year<-substr(phlcalls$request_date,1,4)
daily_counts <- phlcalls%>%
  group_by(phlcalls$request_time)%>%
  summarise(dailyCounts=n())
daily_counts<-data.frame(daily_counts)
daily_counts<-daily_counts%>%
  rename(request_time=phlcalls.request_time,Counts=dailyCounts)%>%
  mutate(time=substr(request_time,1,5))
daily_counts2<-daily_counts%>%
  group_by(time)%>%
  summarise(Tot.counts=sum(Counts))


daily_counts2$time<-strptime(daily_counts2$time,format="%H:%M")
daily_counts2$time<-format(daily_counts2$time, format="%H:%M")

daily_counts2$timelabel<-paste(substr(daily_counts2$time,1,2),":00")
summary<-quantile(daily_counts2$Tot.counts)  #3rd Quantile: 324.5
daily_counts2$proxy.count<-daily_counts2$Tot.counts>summary[4]



ggplot(data=daily_counts2,aes(x=time,y=Tot.counts))+geom_col(aes(fill = proxy.count))+
  scale_fill_manual(labels=c("Rank Below 75% of Number of 311 Calls","Rank Above 75% of Number of 311 calls "),values = c("#424242","dark red" ))+
  labs(x="Time of Day",y="Number of 311 Calls")+
  scale_x_discrete(breaks=c("00:00","01:01","02:00",'03:00',"04:00",'05:00','06:00',
                            '07:00','08:00','09:00','10:00','11:00','12:00','13:00',
                            '14:00','15:00','16:00','17:00','18:00','19:00','20:00','21:00',
                            '22:00','23:00'),
                   labels=unique(daily_counts2$timelabel))+
  ggtitle('Philadelphia Number of 311 Calls by Time of  Day ')+
  theme.graph()

Services and Responsible Agencies

Considering the reasons why people call 311, Maintenance Residential or Commercial is the most common request except for information request; Of the top 20 service requests, most are related to housing and transportation, which leads to the imbalanced responsible agencies showing in the right graph. Of all the agencies, Street Department and License & Inspections respond to the majority of 311 calls.

Geographical Patterns

Considering the Geographical patterns of 311, the top six census tracts with over 100 service requests per 1000 people is located dispersedly across the Philadelphia, however, there are three high request services cluster areas, which are neighborhoods along Delaware Ave, like Port Richmond; the border area of Center City and South Philadelphia, and West Philadelphia.

census_api_key("2c3e9f9d2d65abab5f7b81fe418054415d363a43",overwrite=TRUE)
acs_variable_list.2015 <- load_variables(2015, #year
                                         "acs5", #five year ACS estimates
                                         cache = TRUE)
acs_vars<-c("B01001_001E"  # ACS total Pop estimates
            ) 

acsTracts.2015<-get_acs(geography = "tract",
                        year=2015,
                        variables=acs_vars,
                        geometry=TRUE,
                        state="PA",
                        county="Philadelphia",
                        output= "wide")%>%
  dplyr::select(GEOID,NAME,acs_vars)%>%
  rename(total.pop.2015 = B01001_001E )%>%
  st_as_sf()%>%
  st_transform(crs=4326)

philadelphia<-counties('PA')%>%
  st_as_sf()%>%
  st_transform(crs=4326)%>%
  filter(NAME == 'Philadelphia')


geophlcalls<-phlcalls%>%
  subset(is.na(censustract)==FALSE)%>%
  dplyr::select(servicerequestid,servicename,agencyresponsible,
         latitude,longitude,censustract,request_date,
         request_weekday,request_time,request_year)%>%
  st_as_sf(coords = c("longitude", "latitude"), crs = 4326, agr = "constant")

Tracts.phlcalls <-st_intersection(geophlcalls,acsTracts.2015)
calls.CTs<-Tracts.phlcalls%>%
  group_by(GEOID)%>%
  summarise(counts=n())%>%
  data.frame()%>%
  dplyr::select(GEOID,counts)

acsTracts.2015<-left_join(acsTracts.2015,calls.CTs,by='GEOID')
acsTracts.2015$counts.person<-(acsTracts.2015$counts/acsTracts.2015$total.pop.2015)*1000

acsTracts.2015_2<-filter(acsTracts.2015,total.pop.2015>counts)  #exclude outliers

acsTracts.2015_2$counts.person.pct<-ecdf(acsTracts.2015_2$counts.person)(acsTracts.2015_2$counts.person)
acsTracts.2015_2$counts.person.pct<-acsTracts.2015_2$counts.person.pct*100


acsTracts.2015_2<-acsTracts.2015_2%>%
  mutate(lon=map_dbl(geometry, ~st_point_on_surface(.x)[[1]]),
         lat = map_dbl(geometry, ~st_point_on_surface(.x)[[2]]))  #create centroid
ggplot()+
  geom_sf(data=acsTracts.2015_2,
          aes(fill=counts.person.pct),
          color="transparent")+
  scale_fill_continuous('Cumulated Percentile of service requests',
                     low='light pink',
                     high= 'dark red',
                   na.value='grey50')+
  geom_sf(data=philadelphia,
          color='black',
          fill='transparent',
          size=0.5)+
  geom_text(data=acsTracts.2015_2,aes(x=lon,y=lat,label=round(counts.person)),alpha=0.75,size=2,color='white')+
  labs(
    title = "Numbers of Requested Services via 311 Calls in Philadelphia by Census Tracts"
  )+
  theme.map
Geographical Distribution of Philly 311 Calls

Geographical Distribution of Philly 311 Calls

Crime and 311 Calls

Examine 311 data is essential, because it has significant correlation with the crime rate, which means the higher service requests, the higher crime rates in this area. Therefore, one efficient method to reduce the crime rate is to improve city services and 311 helps the city gather information on lacked city services from the public.

## 
##  Pearson's product-moment correlation
## 
## data:  acsTracts.2015.crime[acsTracts.2015.crime$counts.person.2015 <=  and acsTracts.2015.crime[acsTracts.2015.crime$counts.person.2015 <=     50 & acsTracts.2015.crime$crime.person <= 500, ]$counts.person.2015 and     50 & acsTracts.2015.crime$crime.person <= 500, ]$crime.person
## t = 12.367, df = 358, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.4703570 0.6156648
## sample estimates:
##       cor 
## 0.5471196

Focus Area: University City

University City neighborhood is a focus area across the city, where University of Pennsylvania and Drexel are located, and thousands of students and staffs are living and working here. The safety is the main concern for the students and residents.

Services and Agencies

The analysis shows that service requests are mainly related to traffic and streets, with Traffic Signal Emergency and Street Defect standing out as two major requests in the University City. Therefore, Streets Department responds to the vast majority of 311 calls in the University City.

Street Scope Complaints

This map shows the location of different service requests related to street department. From the map, we could find road improvements are needed on Market St, Chestnut St, Walnut St., and Baltimore Ave, especially on Chestnut Street where street light outage is a severe problem for students who walk from or to the school at night and improvement are also needed on intersections along N 38 th street.

Policy Recommendation

Internal 311 System:
1. Allocate more workforce from 9 am to 3 pm during the weekdays than the rest of time;
2. Train Philly 311 agents with more knowledge related to Streets Department, such as the schedule of trash and recycling collection;

Planning Process:
1. Incorporate Philly311 dataset to citywide planning to help identify the priority areas to be improved;
2. Incorporate Philly 311 dataset to transportation planning to help identify the infrastructures to be improved