• The Trolley
  • Data Wrangling
  • Finding the Best Area For Analysis
    • Limiting to a Buffered Region
    • Excluding Blue Line 2021 Expansion
    • Relating Trolley Stops and Tracts
  • Visualizing the Variables
    • Variable 1: Population
    • Variable 2: Monthly Rent
    • Variable 3: White-Only Population
    • Variable 4: Poverty
    • TOD Indicator Tables
    • TOD Indicator Plots
  • Plotting by Disance from the Stop
    • Line Graph of Multi-Ring Buffer
  • Conclusions

The Trolley

An MTS trolley in downtown San Diego. Sourced from Times of San Diego

The San Diego Trolley light rail system, known as “The Trolley”, started as a connector between downtown San Diego and the South Bay region in the early 1980’s. It expanded regularly in the following decades to service the East County and Central San Diego areas of San Diego County, and most recently opened its northern Blue Line extension to UC San Diego in November 2021.

While subways and metro-rail systems have established demand, my group was curious how popular light rail systems are, in contrast. San Diego’s 2050 Regional Transportation Plan has more extensions planned for the Trolley, but local officials withdrew an additional tax ballot measure for transit at the beginning of the pandemic, stating:

“The unfortunate reality of the global pandemic that we face right now is that our focus as elected officials, as leaders, as an agency really is for the foreseeable future, how do we respond to the public health challenges in front of us….While the day will come that we ask the public to make an investment in transit… that day is not going to come in 2020” - MTS Chairman Nathan Fletcher

While the pandemic is still a concern, proper health precautions have made transit a safe alternative to driving once again. Examining the demand for development before 2020 would reveal how much local residents value development close to transit, and whether more development funding should be acquired to meet this demand.

Data Wrangling

Loading Packages, Functions, and Palette

We use the tidyverse, tidycensus, sf, and kable packages for data wrangling, analysis, and visualization. Most of the ggplot maps we made use custom palettes, so I’ve loaded those here as well. Obtaining the tract data and geometry also requires the use of a US Census API key. Last reference item to load is the Coordinate Reference System (CRS) or projection we’ll be using to align the different data sources. It’s at the top in a variable for easy reference for all later parts of the code.

# Load Libraries and Methods
library(tidyverse)
library(tidycensus)
library(sf)
library(kableExtra)
library(tmap)

#No Scientific Notation
options(scipen=999)
options(tigris_class = "sf")

#Loading methods from our sourcebook repository
source("https://raw.githubusercontent.com/urbanSpatial/Public-Policy-Analytics-Landing/master/functions.r")

#Loading Custom Palettes
palette5 <- c("#f0f9e8","#bae4bc","#7bccc4","#43a2ca","#0868ac")
paletteR <- c("#9AD2FF","#80A1F0","#706EFF","#633DB3","#4A1080")
paletteP <- c("#FFD02E","#FFB52B","#FF8D29","#FF642B","#FA3D30")
paletteD <- c("#FA3D30","#FF642B","#FF8D29","#FFB52B","#FFD02E")
paletteG <- c("#39EB78","#72F067","#C6FF50","#F6FF5B","#FFFFCB")
paletteY <- c("#7231BD","#D63A8D","#FF8052","#FFBE45","#FFF433")
paletteZ <- c("#FFD02E","#FFB52B","#FF8D29","#FF642B","#FA3D30")

#Loading Census API Key
census_api_key("3c9540be1434ac4b38e6e55d60e8ee95909f2254", overwrite = TRUE)

SD_crs = 'ESRI:102411' #This measurement-based CRS projection is for the West Coast

2009 and 2019 ACS Data

Now that the reference items have been loaded, we move on to obtaining the American Census Survey (ACS) 5-year estimates from the Census API. Our analysis here depends on analyzing different variables that determine how in-demand an area is, and the small tract-level areas helps us distinguish the areas that are close to stations or further away.

Some variables we’re focusing on: * Population Density * Average rent * Demographics(White-Only Residents) * Poverty

Each of these variables are commonly correlated with areas of high demand. For instance, population density often increases near high-demand public amenities and job markets, and increases in demand may signal increases in rent. The county shares a border with Mexico, so measuring demographics could illustrate demographic and community shifts. Also, those with low incomes are more likely to be severely rent-burdened, and but their household may want to stay in a more-expensive area because of community or access to transit. Many of these trends change based on the specific location, but it’s a decent baseline assumption.

Two estimates that are one decade apart will indicate if there’s a changing trend, as well.

After obtaining base count variables like Total Population, etc., we add new calculations using mutate(). It’s helpful to equalize data between tracts by dividing each of their variables by their total size. We can get rates per person, per housing unit, or per area, which will avoid direct comparison of unequal areas.

All tract data is then combined into a single data frame using rbind().

tracts09 <-  
  get_acs(geography = "tract",
          variables = c("B25026_001","B25058_001",
                        "B06012_002", "B25002_003",
                        "B25002_001", "B02001_002"), #loading ACS variables
          year=2009, state=06,
          county=073, geometry=TRUE, output = 'wide') %>% 
  st_transform(SD_crs) %>%
  rename(TotalPop = B25026_001E,
         Whites = B02001_002E,
         MedRent = B25058_001E,
         TotalPoverty = B06012_002E,
         Vacancy = B25002_003E,
         Total_Occ = B25002_001E)%>%
  dplyr::select(-NAME, -starts_with("B")) %>%
  mutate(pctWhite = ifelse(TotalPop > 0, Whites / TotalPop,0),
        pctPoverty = ifelse(TotalPop > 0, TotalPoverty / TotalPop, 0),
         year = "2009",
         density_sqmi = as.vector(round((TotalPop/(st_area(geometry)/1610^2)), digits = 0)),
         pctVacancy = ifelse(Total_Occ > 0, Vacancy / Total_Occ, 0)) %>%
  dplyr::select(-TotalPoverty, -Vacancy, -Total_Occ)

tracts19 <- 
  get_acs(geography = "tract", 
          variables = c("B25026_001","B25058_001",
                        "B06012_002","B25002_003",
                        "B25002_001", "B02001_002"), 
          year=2019, state=06,
          county=073, geometry=TRUE, output = 'wide') %>% 
  st_transform(SD_crs) %>%
  rename(TotalPop = B25026_001E, 
         Whites = B02001_002E,
         MedRent = B25058_001E,
         TotalPoverty = B06012_002E,
          Vacancy = B25002_003E,
         Total_Occ = B25002_001E) %>%
  dplyr::select(-NAME, -starts_with("B")) %>%
  mutate(pctWhite = ifelse(TotalPop > 0, Whites / TotalPop,0),
          pctPoverty = ifelse(TotalPop > 0, TotalPoverty / TotalPop, 0),
         year = "2019",
         density_sqmi = as.vector(round((TotalPop/(st_area(geometry)/1610^2)), digits = 0)),
        pctVacancy = ifelse(Total_Occ > 0, Vacancy / Total_Occ, 0)) %>%
  dplyr::select(-TotalPoverty, -Vacancy, -Total_Occ)

allTracts <- rbind(tracts09,tracts19)

Obtaining Trolley Transit Data

Two sources make up the data we need to illustrate the Trolley stations, both from SANDAG&SanGIS Regional GIS Data Warehouse Open Data Portal. The first variable, SD_stops, contains the location data for many different forms of transit around San Diego. In order to reduce it to the ~60 stops that make up today’s Trolley lines, we had to join SD_RoutesTable, a separate table that contains the route ID for each stop. After filtering the stop route data to just the Trolley stops and renaming them appropriately, we join the appropriate stop data together by their shared stop_UID variable, and we finally have a data set of only Trolley stops.

A couple issues exist with the table still before we can combine transit data with census data. Here’s the order we tackle them in:

  1. Our joined data in SD_trolleyDubs contains duplicate observations for stations because the stop routes had separate entries for stations servicing multiple lines. We index unique entries via ![SD_trolleyDubs$stop_name] and now have all unique current stops.

  2. Our point geometry from SD_Stops does not have a map projection encoded in the data frame, but does list the latitude and longitude. We can assume from the point data’s observations, being in common lat/long format, that it can be assigned the most-common CRS EPSG:4326 via the st_as_sf() function. Then we can use st_transform(SD_crs), citing the stored West-Coast meters-based projection from earlier, to make our transit data and tract data align geographically.

  3. Some of the stations did not exist before 2021, explained later. We separate the tract-relevant 2009 trolley stations into SD_trolley09 from the new collection in SD_trolley22.

With that, it’s time for the first visualization!! we plot all our unique trolley stops on a unified tract to make sure each are represented and linging up with the correct projections.

Looks like it matches the transit map! San Diego Trolley light rail system

Finding the Best Area For Analysis

Limiting to a Buffered Region

Evidently, San Diego is a large county with many regions, and the trolley system is just in a single urban corner. By limiting our analysis to tracts whose center falls within a large 9 mile buffer, we can reduce the influence of unknown variables from the analysis that occur in other distinct regions of the county. This new boundary still provides plenty of tracts that one would consider for transit-oriented development or not.

San Diego County, with truncated analysis area outlined in Black. Included Regions in teal, excluded regions in red

Excluding Blue Line 2021 Expansion

The San Diego Trolley’s Blue Line also built the Northern extension recently, starting construction in 2012 and opening in November 2021. Including these Northern tracts as the same designation as the rest of the tracts is dubious, as they did not exist prior to 2021. However, One could argue that anticipation of the extension would still bring potential development, as seen in other light rail projects like Maryland and DC’s ongoing Purple Line. For these reasons, I’ll remove them from the initial analysis, as shown in the plot below:

# 9 mile buffer around the trolley stops to limit evaluation

trolleyMaxBuffer <- 
    st_union(st_buffer(SD_trolley09, 14484)) %>%
    st_sf()%>%
    mutate(Legend = "Max Buffer")

#Intersect the tracts with the max buffer based on the tract's centroids. Must be separate or else left_join will duplicate.
tracts09_LimitSD <-
  st_centroid(tracts09)[trolleyMaxBuffer,] %>%
  st_drop_geometry() %>%
  left_join(., dplyr::select(tracts09, GEOID), by = "GEOID") %>%
  st_sf()

tracts19_LimitSD <-
  st_centroid(tracts19)[trolleyMaxBuffer,] %>%
  st_drop_geometry() %>%
  left_join(., dplyr::select(tracts19, GEOID), by = "GEOID") %>%
  st_sf()

allTracts_LimitSD <- rbind(tracts09_LimitSD, tracts19_LimitSD)
  
#Plot the intersection to see the shape
ggplot() +
  geom_sf(data=allTracts, fill = "#FFF190", color = "#DB782C") +
  geom_sf(data=allTracts_LimitSD, fill=NA, color="Black") +
  geom_sf(data=trolleyMaxBuffer, fill = alpha("#FFBE45", 0.5), color = "#2F59FF") +
  geom_sf(data=SD_trolley09, 
          color = "#2F59FF", 
          show.legend = "point", size= 2) +
  labs(title = "Reduced Analysis Area", subtitle = "San Diego County with 9 Mile Buffer Around 2019 Transit Stations") +
  mapTheme()

Relating Trolley Stops and Tracts

Having defined our scope for tract data, we make a 0.5 mile (or 805 meter) radius around each stop with st_buffer, combine them with st_union, then see which tracts interfect with the half-mile circle and collect them in selection1 with tracts09_LimitSD[buffer,]. These will be the tracts favored for transit-oriented development because of their walkable or bikeable proximity, and we’ll consider them “TOD” tracts from now on. We also try intersecting the half-mile buffer with the center (or centroid) of each tract, but unfortunately it returns no tracts in close proximity for at least 5 different stops, and only one or two for many more. Since this would not leave us with much data to compare between “TOD” and “Non-TOD” tracts, we opt for the (admittedly very generous intersection-based selection1 instead. The plot below features these new labels.

An alternative selection method would be intersecting tracts with shapes based on walking/biking time, or isochrones, but the current distinction is good enough for a first pass at analysis.

trolleyBuffers <- 
  rbind(
    st_buffer(SD_trolley09, 805) %>%
      mutate(Legend = "Buffer") %>%
      dplyr::select(Legend),
    st_union(st_buffer(SD_trolley09, 805)) %>%
      st_sf() %>%
      mutate(Legend = "Unioned Buffer"))

buffer <- filter(trolleyBuffers, Legend=="Unioned Buffer")

#Selecting the tracts based on their intersection with the buffer

selection1 <- 
  tracts09_LimitSD[buffer,] %>%
  dplyr::select(density_sqmi) %>%
  mutate(Selection_Type = "Spatial Selection")

#Centroid selection attempt, leaves very little data for many stops

selectCentroids <-
  st_centroid(tracts19_LimitSD)[buffer,] %>%
  st_drop_geometry() %>%
  left_join(., dplyr::select(tracts19_LimitSD, GEOID), by = "GEOID") %>%
  st_sf() %>%
  dplyr::select(density_sqmi) %>%
  mutate(Selection_Type = "Select by Centroids")

intersections <- rbind(selection1, selectCentroids)

ggplot() +
  geom_sf(data=intersections, aes(fill = density_sqmi)) +
  geom_sf(data=SD_trolley09, show.legend = "point", color="#ed2c23") +
  scale_fill_viridis_c() +
  facet_wrap(~Selection_Type) + 
  labs(title = "Transit-Oriented Development Tracts", subtitle = "San Diego area, 2019 Census Tracts, pre-2021 trolley stations", caption = credit) +
  mapTheme()

tracts_LimitSD.group <- 
  rbind(
    allTracts_LimitSD[buffer,] %>%
      st_drop_geometry() %>%
      left_join(allTracts_LimitSD) %>%
      st_sf() %>%
      mutate(TOD = "TOD"),
    allTracts_LimitSD[buffer, op = st_disjoint] %>%
      st_drop_geometry() %>%
      left_join(allTracts_LimitSD) %>%
      st_sf() %>%
      mutate(TOD = "Non-TOD")) %>%
  mutate(MedRent.inf = ifelse(year == "2009", MedRent * 1.19, MedRent)) 

#Plotting 
ggplot() +
  geom_sf(data=tracts_LimitSD.group, aes(fill = TOD), color = "#7D1313") +
  scale_fill_manual(values = c("#F0B7B4", "#ee2d24")) +
  geom_sf(data=SD_trolley09, aes(color = line)) + 
  scale_color_manual(values = c("#35C1F0", "#4DF063", "#FABF57")) +

  labs(title = "Transit-Oriented Tracts", subtitle = "San Diego transit area, 2019 Census Tracts", caption = credit) +
  mapTheme() + theme(plot.title = element_text(size=22))

Visualizing the Variables

Finally, we can start observing whether the areas around the Trolley stops are in high demand based on a couple different variables.

Variable 1: Population

First is the density, measured in people per square mile of the tract. At first glance, the trolley intersects with most of the densest areas. The coastal tract seems surprisingly less dense than expected, but looking at the trolley route map above reveals that much of the low-density areas are mostly sea water. Still, here is an area of dense tracts that the Trolley seems to circle in central San Diego, but miss by a mile or so.

#Density of Residents Plot

ggplot() +
  geom_sf(data=allTracts_LimitSD, aes(fill = q5(density_sqmi)), color=NA) +
  scale_fill_manual(values = palette5,
    labels = qBr(allTracts_LimitSD, "density_sqmi"),
    name = "People/Sq. Mile\n(Quintile Breaks)") +
  labs(title = "Population Density", subtitle = "San Diego transit area, 2009 vs. 2019", caption = credit) +
  facet_wrap(~year) + 
  mapTheme() + theme(plot.title = element_text(size=22))+
  geom_sf(data=buffer, color="black", fill=NA)

If we look at total population by station, the Trolley lines evidently serve large populations of San Diego, many of which are in Downtown San Diego and in the South Bay area. The largest difference in areas could be between the Northern Green Line and the South Bay area Blue Line, where there is smaller consistent population and large concentrated population, respectively.

#allTracts <- rbind(tracts19_LimitSD,tracts09_LimitSD)
trolleyPB <- filter(trolleyBuffers, Legend == "Buffer")%>%
  tibble::rowid_to_column("ID")
allTracts_LimitSD_C <- dplyr::select(allTracts_LimitSD, GEOID, TotalPop, MedRent, year, geometry)

stationData <- 
  st_join(trolleyPB, allTracts_LimitSD_C, join = st_intersects)%>%  #get spacial join/filter of Trolley Buffers and all_tracts_limitSD
  dplyr::select(ID, Legend, GEOID, TotalPop, MedRent, geometry)%>%
  dplyr::group_by(ID)%>%
  summarize(totalPop = sum(TotalPop), avgRent=mean(MedRent, na.rm = TRUE))%>%
  st_centroid(stationData)%>%
  st_as_sf()

ggplot() +
  geom_sf(data=allTracts_LimitSD, fill = "#FFF190", color="#FFD776") +
  geom_sf(data = stationData,
          pch = 21,
          aes(size = (totalPop)),
          fill = alpha("#0868AC", 0.7),
          col = "grey20") +
  labs(title = "Population by Station", 
       subtitle = "San Diego Trolley Stations, 2019", 
       caption = credit, 
       size = "Population") +
  scale_size(range = c(1, 5))+
  mapTheme() + theme(plot.title = element_text(size=22))

Variable 2: Monthly Rent

Interestingly, the Trolley goes through some of the lowest-rent areas of San Diego. Obviously many places have increased in price between 2009 and 2019, even when inflation adjusted, but the trolley’s path on the Blue Line and parts of the Orange Line have kept lower than their surroundings.

#Rent Plot

ggplot() +
  geom_sf(data=allTracts_LimitSD, aes(fill = q5(MedRent)), color=NA) +
  scale_fill_manual(values = paletteR,
    labels = qBr(allTracts_LimitSD, "MedRent"),
    name = "Average Monthly Rent\nUSD$ 2019\n(Quintile Breaks)") +
  labs(title = "Average Rent", subtitle = "San Diego transit area 2009 vs. 2019", caption = credit) +
  facet_wrap(~year) + 
  mapTheme() + theme(plot.title = element_text(size=22))+
  geom_sf(data=buffer, color="black", fill=NA)

Average Rent by Station

Looking at a visualization of rent per station, indeed the central and Southern parts of the Blue and Orange lines demand less rent than the Green line to the North. The Downtown area is cluttered in this visualization, but looking closely at the outlines of the circles, it seems that even those areas are cheaper than the Northern areas along the Green line.

#Rent by Station Symbol Plot
ggplot() +
  geom_sf(data=allTracts_LimitSD, fill = "#FFF190", color="#FFD776") +
  geom_sf(data = stationData,
          pch = 21,
          aes(size = (avgRent)), 
          fill = alpha("#4A1080", 0.5)) +
  labs(title = "Average Rent by Station", 
       subtitle = "San Diego transit area 2009 vs. 2019",
       caption = credit, 
       size = "Average Rent\n(US$ 2019)") +
  scale_fill_manual(values = palette5,
    labels = q5(allTracts_LimitSD$density_sqmi),
    name = "Popluation\n(Quintile Breaks)")+
  scale_size(range = c(1, 4)) + 
  mapTheme() + theme(plot.title = element_text(size=22))

Variable 3: White-Only Population

The map illustrates a demographic shift in the South Bay region, where the population now has more white-only residents. Conversely, the Northern ends of the trolley line have increased populations of non-White residents. The center, circled by the trolley, does not shift and has a low white-only population.

#Percent White Population Summary Data
allTracts_LimitSD_PctW <- allTracts_LimitSD %>%
  dplyr::select(pctWhite, geometry, year)%>%
  mutate(pctWhite = pctWhite * 100)

#Map Plot
ggplot() +
  geom_sf(data=allTracts_LimitSD_PctW, aes(fill =q5(pctWhite)), color=NA) +
  scale_fill_manual(values = paletteG,
    labels = paste(qBr(allTracts_LimitSD_PctW, "pctWhite"), "%"),
    name = "Percent of\nVacant Units\n(Quintile Breaks)") +
  labs(title = "Demographics: White-Only Percentage", subtitle = "San Diego transit area 2009 vs. 2019", caption = credit) +
  facet_wrap(~year) + 
  mapTheme() + theme(plot.title = element_text(size=22))+
  geom_sf(data=buffer, color='black', fill=NA)

Variable 4: Poverty

This map shows a clear indication that the Trolley intersects many areas with relatively high poverty. This is especially true on the South Bay area of the Blue Line, and still very true along the Orange Line as it approaches East County. Again, the Trolley does miss some areas of concentrated poverty, and those areas align with the high denisty tracts in the center as noted before.

#Poverty
allTracts_LimitSD_Pov <- allTracts_LimitSD %>% #Converting Percentages to 100 for visibility on map
  dplyr::select(pctPoverty, geometry, year)%>%
  mutate(pctPoverty = pctPoverty * 100)

ggplot() +
geom_sf(data=allTracts_LimitSD_Pov, aes(fill = q5(pctPoverty)), color=NA) +
  scale_fill_manual(values = paletteZ,
    labels = paste(qBr(allTracts_LimitSD_Pov, "pctPoverty"), "%"),
    name = "Percent of\nHouseholds in Poverty\n(Quintile Breaks)") +
  labs(title = "Poverty", subtitle = "San Diego transit area 2009 vs. 2019", caption = credit) +
  facet_wrap(~year) + 
  mapTheme() + theme(plot.title = element_text(size=22))+
  geom_sf(data=buffer, color="black", fill=NA)

TOD Indicator Tables

By summarizing the four variables in a new table and plotting them, we can compare the TOD and Non-TOD tracts, sorted by year. We can see that the TOD areas on average are slightly less dense, charge less rent, have a slightly greater percentage of vacancy, and are more likely to have households experiencing poverty. TOD and Non-TOD areas seem to grow in density and rent costs equivalently, though poverty decreased in the ten year span more in TOD areas.

tracts_LimitSD.Summary <- 
  st_drop_geometry(tracts_LimitSD.group) %>%
  group_by(year, TOD) %>%
  summarize(Residents_Per_Mile = round(mean(density_sqmi, na.rm = T), digits = 0),
            Rent = mean(MedRent, na.rm = T),
            Percent_White = mean(pctWhite, na.rm = T),
            Percent_Poverty = mean(pctPoverty, na.rm = T))

tracts_LimitSD.Summary %>%
  unite(year.TOD, year, TOD, sep = ": ", remove = T) %>%
  gather(Variable, Value, -year.TOD) %>%
  mutate(Value = round(Value, 2)) %>%
  spread(year.TOD, Value) %>%
  kable() %>%
  kable_styling() %>%
  footnote(general_title = "\n",
           general = credit)
Variable 2009: Non-TOD 2009: TOD 2019: Non-TOD 2019: TOD
Percent_Poverty 0.12 0.19 0.12 0.16
Percent_White 0.89 0.76 0.83 0.73
Rent 1256.33 1026.45 1694.09 1413.80
Residents_Per_Mile 7921.00 7526.00 8835.00 8466.00

Data: US Census Beureau, ACS 5-year Estimates &
SANDAG/SanGIS Regional GIS Data Warehouse Open Data Portal

TOD Indicator Plots

We do some data conversion using gather() to easily plot these variables in side-by-side charts. The equivalent growth of rent and density is made clear on this plot, but we can easily again see that rent and density are lower in non-TOD areas.

tracts_LimitSD.Summary %>%
  rename(
      PovertyRate = Percent_Poverty,
      PercentWhite = Percent_White,
      Density = Residents_Per_Mile)%>%
  gather(Variable, Value, -year, -TOD)%>%
  ggplot(aes(year, Value, fill = TOD)) +
  geom_bar(stat = "identity", position = "dodge") +
  facet_wrap(~Variable, scales = "free", ncol=5) +
  scale_fill_manual(values = c("#bae4bc", "#0868ac")) +
  labs(title = "Indicator differences across time and space") +
  plotTheme() + theme(legend.position="bottom", plot.title = element_text(size=22))

Plotting by Disance from the Stop

Rent has been one of our strongest indicators so far that TOD areas charge less rent in San Diego, and we can go deeper in this analysis by plotting average rent across incremental distances from the Trolley stops. We start by using multipleRingBuffer() on the union of the trolley points, giving us one repeating shape, expanding outward by 0.5 miles (805 meters) each step until it reaches 9 miles (14484 meters). We then combine the tract data with each slice of the ring buffer by using a spatial join st_join, and combine those new rings with rent and year data. See the result below:

#allTracts <- rbind(tracts19_LimitSD,tracts09_LimitSD)
trolley_MRB <- multipleRingBuffer(st_union(SD_trolley09), 14484, 805)

#spatial join to make each interval of the MRB match with whichever tract centeroids fall inside its boundary
allTracts_LimitSD.rings <-
  st_join(st_centroid(dplyr::select(allTracts_LimitSD, GEOID, year)),
          trolley_MRB) %>%
  st_drop_geometry() %>% #drop geometry in preparation for a left join (avoids duplication)
  left_join(dplyr::select(allTracts_LimitSD, GEOID, MedRent, year), #join rent and year 
            by=c("GEOID"="GEOID", "year"="year")) %>%
  st_sf() %>%
  mutate(distance = distance / 1610) #convert to miles

ggplot() +
    geom_sf(data=trolley_MRB) +
    geom_sf(data=SD_trolley09, size=1) +
    geom_sf(data=st_union(tracts19_LimitSD), fill=NA, size=1.2) +
    labs(title="Half mile buffers") +
    mapTheme()+theme(plot.title = element_text(size=22))

Line Graph of Multi-Ring Buffer

After refining, grouping, and summarizing the multi-ring buffer data, we can plot it to check whether our map observations were correct:

#Creating a summary table of the ring data
allTracts.ring.summary <- allTracts_LimitSD.rings %>%
  dplyr::select(year, distance, MedRent)%>%
  st_drop_geometry()%>%
  group_by(year,distance)%>%
  summarize(Rent=median(MedRent, na.rm=T))

#Line Plot of the summary table 
ggplot(data=allTracts.ring.summary, aes(x=distance, y=Rent, group = year),)+
  geom_line(aes(color=year), size=1.5)+
  geom_point(aes(color=year), size = 3)+  
  labs(title = "Rent as a Function of Distance from a Trolley Station", 
       subtitle = "San Diego transit area, 2019 Census Tracts", 
       caption = credit,
       x="Distance from a Trolley Station (Miles)",
       y="Rent (2019 USD)")+
  theme(plot.title = element_text(size=18))

Conclusions

Indeed, rent is, on average, lower close to the Trolley and higher further away, peaking at around 6 or 7 miles out. The indicator here is that the Trolley may not draw San Diego residents to move closer to transit-oriented development areas. There are factors outside of the dense urban areas that many of them prioritize over access to a light rail, especially toward the North area of San Diego city limits and East toward the Mountain Empire region. These areas saw increased migration from non-white residents, higher rent increases, and a competitive density increase to tracts that are close to transit.

Meanwhile, the South Bay part of the Blue line, being the oldest part of the rail, continues to serve a large and dense population. It’s one that benefits from low rent, but may be experiencing higher rates of poverty than other areas. It also has had an increase in white population. The lack of inequitable increase in rent signals that displacement is less likely to be happening in those areas as a result of the demographic shift, though perhaps more white residents wanted to move to denser transit-oriented blocks. Some areas further away from transit saw increased non-white representation, which could signal a preference among non-White households to move further out from South Bay, especially considering the high cost of rent in those high-demand low-density areas.

Ultimately, it appears this light rail system is not yet a major incentive to move closer to denser parts of San Diego. Despite the low rent and increased transit amenities, perhaps there are other reasons beyond this analysis residents would rather stay where they are or move elsewhere. Examining the level of demand for recent extensions to the Trolley may also yield more insights.

the Trolley travels along the Los Angeles-San Diego-San Luis Obispo Rail Corridor, via American Society for Civil Engineers