Details on the analyses with code for replication

Information about the data

Parking lot counts - the number of empty parking spaces in a parking lot - are publicly available data for many of the public parking lots and structures in Santa Monica. Data is available in real time with counts reported every 5 minutes. This data started being collected in November 2014. Data is available [online][https://data.smgov.net/Transportation/Parking-Lot-Counts/ng8m-khuz].

Data for downtown Santa Monica parking lots was downloaded in early October 2019. Analysis herein starts at the beginning of the data availability (i.e., November 2014) and goes through September 2019.

Parking lots included in this analysis were structures 1-9 and the Library parking lot.

Lots 27/28 and Structure 10 are in downtown, but these lots were not in the online data. Assumptions are that lots 27 and 28 are not outfitted with the count technology and Structure 10 is either not outfitted with the count technology or perhaps it is included under Structure 9 as these parking lots are physically adjacent to each other.

Data analysis and visualization

Parking space availability was summed for all parkings lots included in the analysis. Time series interpolation was used where counts were missing.

The purpose of the analysis was to investigate how parking lot use was changing through time. Two aspects were considered. First, how use is changing generally. Second, how peak demand for parking is changing.

The estimated number of parking spaces in all lots combined is 5946. The total number of parking spaces in the lots was not provided in the data. The total number of parking spaces was estimated as the maximum value for available parking spaces across the entire time series considered.

General change to parking use

Trends in parking use were calculated. Trends are what emerge after accounting for daily, weekly, and seasonal oscillations. The graph below shows the overall trend in UCLA blue. This is a prediction of the average or typical parking use. The 7-day moving average is presented in UCLA yellow to show a distilled representation of the raw data for direct visual interpretation.

Change in peak demand

Trends in peak demand parking use were calculated. Again, trends are what emerge after accounting for daily, weekly, and seasonal oscillations. In this case, the trend is for the weekly minimum availabilty of parking spaces. The graph below shows this trend in UCLA blue. The actual data (i.e., peak demand moment each week) is presented in UCLA yellow.

Finally, peak demand was shown as the minimum availability for the 5 days in each year with the lowest minimum availability. Code available to producing the table provided at the end of the code block below.

Code used to generate the above results. Code was executed in R version 3.6.1.

library(dplyr)
library(tidyr)
library(ggplot2)
library(lubridate)
library(magrittr)
library(imputeTS)
library(zoo)
library(decompose)
library(forecast)
library(knitr)
library(kableExtra)

##Create date range
full.time <- seq(from = ymd_hms('2014-11-06 15:15:00'),to = ymd_hms('2019-09-30 23:55:00'), by = "5 mins")
full.time <- data.frame(full.time)
full.time <- rename(full.time, ymd_hms = full.time)

##Read in csv files, rename parking structure data files, trim variables, join to full.time, and impute missing Available values
temp = list.files(pattern="*.csv")
list2env(
  lapply(setNames(temp, make.names(gsub("*.csv$", "", temp))), 
         read.csv), envir = .GlobalEnv)

##Lubridate date-time stamp, join to full time and select key columns

S1$ymd_hms <- mdy_hms(S1$Date.Time)
S1 <- left_join(full.time, S1, by="ymd_hms")
S1 <-  select(S1, c(ymd_hms,Lot,Available))
S1 <- distinct(S1,ymd_hms,.keep_all = T)
S1$Available <- na_interpolation(S1$Available)
S1$Available <- floor(S1$Available)
S1 <- S1 %>% select(ymd_hms,Available)
S1 <- S1 %>% rename(EmptyS1=Available)

S2$ymd_hms <- mdy_hms(S2$Date.Time)
S2 <- left_join(full.time, S2, by="ymd_hms")
S2 <-  select(S2, c(ymd_hms,Lot,Available))
S2 <- distinct(S2,ymd_hms,.keep_all = T)
S2$Available <- na_interpolation(S2$Available)
S2$Available <- floor(S2$Available)
S2 <- S2 %>% select(ymd_hms,Available)
S2 <- S2 %>% rename(EmptyS2=Available)

S3$ymd_hms <- mdy_hms(S3$Date.Time)
S3 <- left_join(full.time, S3, by="ymd_hms")
S3 <-  select(S3, c(ymd_hms,Lot,Available))
S3 <- distinct(S3,ymd_hms,.keep_all = T)
S3$Available <- na_interpolation(S3$Available)
S3$Available <- floor(S3$Available)
S3 <- S3 %>% select(ymd_hms,Available)
S3 <- S3 %>% rename(EmptyS3=Available)

S4$ymd_hms <- mdy_hms(S4$Date.Time)
S4 <- left_join(full.time, S4, by="ymd_hms")
S4 <-  select(S4, c(ymd_hms,Lot,Available))
S4 <- distinct(S4,ymd_hms,.keep_all = T)
S4$Available <- na_interpolation(S4$Available)
S4$Available <- floor(S4$Available)
S4 <- S4 %>% select(ymd_hms,Available)
S4 <- S4 %>% rename(EmptyS4=Available)

S5$ymd_hms <- mdy_hms(S5$Date.Time)
S5 <- left_join(full.time, S5, by="ymd_hms")
S5 <-  select(S5, c(ymd_hms,Lot,Available))
S5 <- distinct(S5,ymd_hms,.keep_all = T)
S5$Available <- na_interpolation(S5$Available)
S5$Available <- floor(S5$Available)
S5 <- S5 %>% select(ymd_hms,Available)
S5 <- S5 %>% rename(EmptyS5=Available)

S6$ymd_hms <- mdy_hms(S6$Date.Time)
S6 <- left_join(full.time, S6, by="ymd_hms")
S6 <-  select(S6, c(ymd_hms,Lot,Available))
S6 <- distinct(S6,ymd_hms,.keep_all = T)
S6$Available <- na_interpolation(S6$Available)
S6$Available <- floor(S6$Available)
S6 <- S6 %>% select(ymd_hms,Available)
S6 <- S6 %>% rename(EmptyS6=Available)

S7$ymd_hms <- mdy_hms(S7$Date.Time)
S7 <- left_join(full.time, S7, by="ymd_hms")
S7 <-  select(S7, c(ymd_hms,Lot,Available))
S7 <- distinct(S7,ymd_hms,.keep_all = T)
S7$Available <- na_interpolation(S7$Available)
S7$Available <- floor(S7$Available)
S7 <- S7 %>% select(ymd_hms,Available)
S7 <- S7 %>% rename(EmptyS7=Available)

S8$ymd_hms <- mdy_hms(S8$Date.Time)
S8 <- left_join(full.time, S8, by="ymd_hms")
S8 <-  select(S8, c(ymd_hms,Lot,Available))
S8 <- distinct(S8,ymd_hms,.keep_all = T)
S8$Available <- na_interpolation(S8$Available)
S8$Available <- floor(S8$Available)
S8 <- S8 %>% select(ymd_hms,Available)
S8 <- S8 %>% rename(EmptyS8=Available)

S9$ymd_hms <- mdy_hms(S9$Date.Time)
S9 <- left_join(full.time, S9, by="ymd_hms")
S9 <-  select(S9, c(ymd_hms,Lot,Available))
S9 <- distinct(S9,ymd_hms,.keep_all = T)
S9$Available <- na_interpolation(S9$Available)
S9$Available <- floor(S9$Available)
S9 <- S9 %>% select(ymd_hms,Available)
S9 <- S9 %>% rename(EmptyS9=Available)

SLibrary$ymd_hms <- mdy_hms(SLibrary$Date.Time)
SLibrary <- left_join(full.time, SLibrary, by="ymd_hms")
SLibrary <-  select(SLibrary, c(ymd_hms,Lot,Available))
SLibrary <- distinct(SLibrary,ymd_hms,.keep_all = T)
SLibrary$Available <- na_interpolation(SLibrary$Available)
SLibrary$Available <- floor(SLibrary$Available)
SLibrary <- SLibrary %>% select(ymd_hms,Available)
SLibrary <- SLibrary %>% rename(EmptySLibrary=Available)


#Make a combined columns data frame
All <- left_join(S1,S2, by="ymd_hms") %>%
  left_join(.,S3, by="ymd_hms") %>%
  left_join(.,S4, by="ymd_hms") %>%
  left_join(.,S5, by="ymd_hms") %>%
  left_join(.,S6, by="ymd_hms") %>%
  left_join(.,S7, by="ymd_hms") %>%
  left_join(.,S8, by="ymd_hms") %>%
  left_join(.,S9, by="ymd_hms") %>%
  left_join(.,SLibrary, by="ymd_hms")

All <- All %>% mutate(Available = EmptyS1+EmptyS2+EmptyS3+
                EmptyS4+EmptyS5+EmptyS6+EmptyS7+EmptyS8+
                EmptyS9+EmptySLibrary)


##Detrend/decompose with stl
#Make parking counts into a time series with ts using 52 week frequency
ts_all_52week <- ts(All$Available, frequency = 104832)
decomposed <- stl(ts_all_52week, s.window="periodic")
#Extract trend data
trend_52week <- trendcycle(decomposed)

Trend_Annual <- trend_52week
Trend_Annual <- as.data.frame(Trend_Annual)
Trend_Annual <- cbind(Trend_Annual, full.time)
Trend_Annual <- rename(Trend_Annual, Trend = x)
#Trend_Annual$Change <- Trend_Annual %>% mutate(Trend-Trend_Annual[1,1])

#Create 7 day moving average to plot behind the trend line created above
All <- All %>%
  mutate(MA7day = rollmean(All$Available,k=7*24*12,fill=NA))
                                                 
ggplot(Trend_Annual, aes(ymd_hms, Trend))+
  geom_line(data=All, aes(ymd_hms, MA7day),color="#F2A900",alpha=1/2)+
  geom_line(size=1.5, color="#2D68C4")+
  theme_minimal()+
  theme(#axis.line = element_line(colour = "black"),
    axis.text = element_text(size = 12),
    text=element_text(size=12),
    #panel.grid.major = element_blank(), panel.grid.minor = element_blank(),
    panel.background = element_blank(), 
    title=element_text(color="black"), 
    axis.title=element_text(color="black"), 
    legend.text = element_text(size = 16))+
  labs(title="Typical Number of Parking Spaces", y="Empty Spaces", x="")+
  scale_y_continuous(limits=c(min(All$MA7day),max(All$MA7day)),
                     sec.axis = sec_axis(~ .*(-1)+max(All$Available),
                                         name="Occupied Spaces"))

ggsave("SaMoParkingTrend.png",width=6, height=4.5, units="in",dpi=300, device="png")

##Investigating peak demand times

#Extract weekly min values
All$Week <- round_date(All$ymd_hms, "week")

AllMinMaxWeekly <- All %>%
  group_by(Week) %>%
  summarise(WeeklyMin <- min(Available))

AllMinMaxWeekly$Week <- ymd(AllMinMaxWeekly$Week)
AllMinMaxWeekly <- AllMinMaxWeekly %>% rename(WeeklyMin = `WeeklyMin <- min(Available)`)

#Decompose Weekly Minimum Time Series

ts_allmin_52week <- ts(AllMinMaxWeekly$WeeklyMin, frequency = 52)
decomposed.weeklymin <- stl(ts_allmin_52week, s.window="periodic")
#Extract trend data
trend_52week_weeklymin <- trendcycle(decomposed.weeklymin)

Trend_Annual_Min <- trend_52week_weeklymin
Trend_Annual_Min <- as.data.frame(Trend_Annual_Min)
Trend_Annual_Min <- data.frame(Trend_Annual_Min)
weekly.full.time <- seq(from = ymd_hms('2014-11-06 15:15:00'),
                        to = ymd_hms('2019-09-30 23:55:00'),
                        by = "week")
weekly.full.time <- data.frame(weekly.full.time)
weekly.full.time$weekly.full.time <- ymd_hms(weekly.full.time$weekly.full.time)
Trend_Annual_Min <- cbind(Trend_Annual_Min, weekly.full.time)
Trend_Annual_Min <- rename(Trend_Annual_Min, Trend = x)
Trend_Annual_Min <- rename(Trend_Annual_Min, ymd_hms = weekly.full.time)
AllMinMaxWeekly <- cbind(AllMinMaxWeekly, weekly.full.time)
AllMinMaxWeekly <- rename(AllMinMaxWeekly, ymd_hms = weekly.full.time)


ggplot(Trend_Annual_Min, aes(ymd_hms, Trend))+
  geom_line(data=AllMinMaxWeekly, aes(ymd_hms, WeeklyMin),color="#F2A900",alpha=1/2)+
  geom_line(size=1.5, color="#2D68C4")+
  theme_minimal()+
  theme(#axis.line = element_line(colour = "black"),
    axis.text = element_text(size = 12),
    text=element_text(size=12),
    #panel.grid.major = element_blank(), panel.grid.minor = element_blank(),
    panel.background = element_blank(), 
    title=element_text(color="black"), 
    axis.title=element_text(color="black"), 
    legend.text = element_text(size = 16))+
  labs(title="Weekly Minimum in Available Parking", y="Empty Spaces", x="")+
  scale_y_continuous(limits=c(500,2500), breaks=c(500,1000,1500,2000,2500),
                     sec.axis = sec_axis(trans=~./max(All$Available)*100,
                                         name="Percent of Total Parking",
                                         breaks = c(10,20,30,40)))
  
ggsave("WeeklyMinParking.png",width=6, height=4.5, units="in",dpi=300, device="png")


##Daily peak demand across years

All$Date <- round_date(All$ymd_hms, "day")

AllMinDaily <- All %>%
  group_by(Date) %>%
  summarise(DailyMin <- min(Available))

AllMinDaily$Date <- ymd(AllMinDaily$Date)
AllMinDaily <- AllMinDaily %>% rename(DailyMin = `DailyMin <- min(Available)`)

AllMinDaily <- AllMinDaily %>% mutate(Year=year(Date))
AllMinDaily$Year <- as.factor(AllMinDaily$Year)

LowestDailies <- AllMinDaily %>%
  filter(Year != "2014") %>%
  group_by(Year) %>%
  mutate(Rank = rank(DailyMin)) %>%
  filter(Rank <= 5) %>%
  group_by(Year) %>%
  arrange(DailyMin, .by_group=TRUE) %>%
  mutate(Day = mday(Date)) %>%
  mutate(Month = month(Date)) %>%
  select(Year, Month, Day, DailyMin)

kable(LowestDailies,
      col.names=c("Year","Month","Day","Minimum Availability"),
      caption="Table. Greatest parking demand days each year") %>%
  kable_styling(latex_options="scale_down")