library(tidycensus)
library(tidyverse)
library(magrittr)
library(osmdata)
library(sfnetworks)
library(units)
library(sf)
library(tidygraph)
library(tmap)
library(here)
library(progress)
ttm()
Use the Census Tract map in the following code chunk to identify the GEOIDs of the tracts you consider walkable and unwalkable.
# TASK ////////////////////////////////////////////////////////////////////////
# Set up your api key here
census_api_key(Sys.getenv('CENSUS_API'))
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Download Census Tract polygon for Fulton and DeKalb
tract <- get_acs("tract",
variables = c('pop' = 'B01001_001'),
year = 2023,
state = "GA",
county = c("Fulton", "DeKalb"),
geometry = TRUE)
tmap_mode('view')
tm_basemap("OpenStreetMap") +
tm_shape(tract) +
tm_polygons(fill_alpha = 0.2)
# =========== NO MODIFY ZONE ENDS HERE ========================================
Once you have the GEOIDs, create two Census Tract objects – one representing your most walkable area and the other your least walkable area.
# TASK ////////////////////////////////////////////////////////////////////////
# 1. Specify the GEOIDs of your walkable and unwalkable Census Tracts.
# e.g., tr_id_walkable <- c("13121001205", "13121001206")
# 2. Extract the selected Census Tracts using `tr_id_walkable` and `tr_id_unwalkable`
# For the walkable Census Tract(s)
tr_id_walkable <- c('13121001002')
tract_walkable <- tract %>% filter(GEOID %in% tr_id_walkable)
# For the unwalkable Census Tract(s)
tr_id_unwalkable <- c('13121001902')
tract_unwalkable <- tract %>% filter(GEOID %in% tr_id_unwalkable)
# //TASK //////////////////////////////////////////////////////////////////////
# TASK ////////////////////////////////////////////////////////////////////////
tmap_mode("view")
tm_shape(tract_walkable) +
tm_polygons(col = "green", alpha = 0.6) +
tm_shape(tract_unwalkable) +
tm_polygons(col = "red", alpha = 0.6) +
tm_layout(
title = "Walkable & Unwalkable Census Tracts",
legend.outside = TRUE
)
# //TASK //////////////////////////////////////////////////////////////////////
Provide a brief description of your selected Census Tracts. Why do you consider these tracts walkable or unwalkable? What factors do you think contribute to their walkability?
To obtain the OSM network for your selected Census Tracts: (1) Create
bounding boxes. (2) Use the bounding boxes to download OSM data. (3)
Convert the data into an sfnetwork object and clean it.
# TASK ////////////////////////////////////////////////////////////////////////
# Create one bounding box (`tract_walkable_bb`) for your walkable Census Tract(s) and another (`tract_unwalkable_bb`) for your unwalkable Census Tract(s).
# For the walkable Census Tract(s)
tract_walkable_bb <- tract %>%
filter(GEOID == tr_id_walkable) %>%
st_bbox()
# For the unwalkable Census Tract(s)
tract_unwalkable_bb <- tract %>%
filter(GEOID == tr_id_unwalkable) %>%
st_bbox()
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Get OSM data for the two bounding boxes
osm_walkable <- opq(bbox = tract_walkable_bb) %>%
add_osm_feature(key = 'highway',
value = c("primary", "secondary", "tertiary", "residential")) %>%
osmdata_sf() %>%
osm_poly2line()
osm_unwalkable <- opq(bbox = tract_unwalkable_bb) %>%
add_osm_feature(key = 'highway',
value = c("primary", "secondary", "tertiary", "residential")) %>%
osmdata_sf() %>%
osm_poly2line()
# =========== NO MODIFY ZONE ENDS HERE ========================================
# TASK ////////////////////////////////////////////////////////////////////////
# 1. Convert `osm_walkable` and `osm_unwalkable` into sfnetwork objects (as undirected networks),
# 2. Clean the network by (1) deleting parallel lines and loops, (2) creating missing nodes, and (3) removing pseudo nodes (make sure the `summarise_attributes` argument is set to 'first' when doing so).
net_walkable <- osm_walkable$osm_lines %>%
# Drop redundant columns
select(osm_id, highway) %>%
sfnetworks::as_sfnetwork(directed = FALSE) %>%
activate("edges") %>%
filter(!edge_is_multiple()) %>%
filter(!edge_is_loop()) %>%
convert(., sfnetworks::to_spatial_subdivision) %>%
convert(., sfnetworks::to_spatial_smooth, summarise_attributes = "first")
net_unwalkable <- osm_unwalkable$osm_lines %>%
# Drop redundant columns
select(osm_id, highway) %>%
sfnetworks::as_sfnetwork(directed = FALSE) %>%
activate("edges") %>%
filter(!edge_is_multiple()) %>%
filter(!edge_is_loop()) %>%
convert(., sfnetworks::to_spatial_subdivision) %>%
convert(., sfnetworks::to_spatial_smooth, summarise_attributes = "first")
# //TASK //////////////////////////////////////////////////////////////////////
# TASK //////////////////////////////////////////////////////////////////////
# Using `net_walkable` and`net_unwalkable`,
# 1. Activate the edge component of each network.
# 2. Create a `length` column.
# 3. Filter out short (<300 feet) segments.
# 4. Randomly Sample 100 rows per road type.
# 5. Assign the results to `edges_walkable` and `edges_unwalkable`, respectively.
# OSM for the walkable part
edges_walkable <- net_walkable %>%
st_as_sf("edges") %>%
select(osm_id, highway) %>%
mutate(length = st_length(.) %>% unclass()) %>%
filter(length >= 91) %>%
group_by(highway) %>%
slice_sample(n = 100) %>%
ungroup() %>%
mutate(edge_id = seq(1,nrow(.)))
# OSM for the unwalkable part
edges_unwalkable <- net_unwalkable %>%
st_as_sf("edges") %>%
select(osm_id, highway) %>%
mutate(length = st_length(.) %>% unclass()) %>%
filter(length >= 91) %>%
group_by(highway) %>%
slice_sample(n = 100) %>%
ungroup() %>%
mutate(edge_id = seq(1,nrow(.)))
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Merge the two
edges <- bind_rows(edges_walkable %>% mutate(is_walkable = TRUE),
edges_unwalkable %>% mutate(is_walkable = FALSE)) %>%
mutate(edge_id = seq(1,nrow(.)))
# =========== NO MODIFY ZONE ENDS HERE ========================================
getAzimuth() function.In this assignment, you will collect two GSV images per road segment, as illustrated in the figure below. To do this, you will define a function that extracts the coordinates of the midpoint and the azimuths in both directions.
If you can’t see this image, try changing the markdown editing mode from ‘Source’ to ‘Visual’ (you can find the buttons in the top-left corner of this source pane).
getAzimuth <- function(line){
# TASK ////////////////////////////////////////////////////////////////////////
# 1. Use the `st_line_sample()` function to sample three points at locations 0.48, 0.5, and 0.52 along the line. These points will be used to calculate the azimuth.
# 2. Use `st_cast()` function to convert the 'MULTIPOINT' object into a 'POINT' object.
# 3. Extract coordinates using `st_coordinates()`.
# 4. Assign the coordinates of the midpoint to `mid_p`.
# 5. Calculate the azimuths from the midpoint in both directions and save them as `mid_azi_1` and `mid_azi_2`, respectively.
# 1-3
mid_p3 <- line %>% st_line_sample(line, sample = c(0.48, 0.5, 0.52)) %>%
st_cast("POINT") %>%
st_coordinates()
# 4
mid_p <- mid_p3[2, ]
# 5
mid_azi_1 <- atan2(mid_p3[1,"X"] - mid_p3[2, "X"],
mid_p3[1,"Y"] - mid_p3[2, "Y"])*180/pi
mid_azi_2 <- atan2(mid_p3[3,"X"] - mid_p3[2, "X"],
mid_p3[3,"Y"] - mid_p3[2, "Y"])*180/pi
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
return(tribble(
~type, ~X, ~Y, ~azi,
"mid1", mid_p["X"], mid_p["Y"], mid_azi_1,
"mid2", mid_p["X"], mid_p["Y"], mid_azi_2,))
# =========== NO MODIFY ZONE ENDS HERE ========================================
}
Apply the getAzimuth() function to the
edges object. Once this step is complete, your data will be
ready for downloading GSV images.
# TASK ////////////////////////////////////////////////////////////////////////
# Apply getAzimuth() function to all edges.
# Remember that you need to pass edges object to st_geometry() before you apply getAzimuth()
edges_azi <- edges %>%
st_geometry() %>%
map_df(getAzimuth, .progress = T)
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
edges_azi <- edges_azi %>%
bind_cols(edges %>%
st_drop_geometry() %>%
slice(rep(1:nrow(edges),each=2))) %>%
st_as_sf(coords = c("X", "Y"), crs = 4326, remove=FALSE) %>%
mutate(img_id = seq(1, nrow(.)))
# =========== NO MODIFY ZONE ENDS HERE ========================================
getImage <- function(iterrow){
# This function takes one row of `edges_azi` and downloads GSV image using the information from the row.
# TASK ////////////////////////////////////////////////////////////////////////
# 1. Extract required information from the row of `edges_azi`
# 2. Format the full URL and store it in `request`. Refer to this page: https://developers.google.com/maps/documentation/streetview/request-streetview
# 3. Format the full path (including the file name) of the image being downloaded and store it in `fpath`
type <- iterrow$type
location <- paste0(iterrow$Y %>% round(5), ",", iterrow$X %>% round(5))
heading <- iterrow$azi %>% round(1)
edge_id <- iterrow$edge_id
img_id <- iterrow$img_id
key <- Sys.getenv("google_api")
endpoint <- 'https://maps.googleapis.com/maps/api/streetview'
request <- glue::glue("{endpoint}?size=640x640&location={location}&heading={heading}&fov=90&pitch=0&key={key}")
fname <- glue::glue("GSV-nid_{img_id}-eid_{edge_id}-type_{type}-Location_{location}-heading_{heading}.jpg") # Don't change this code for fname
fpath <- file.path("C:/GT MS UA/CP 8883/Projects/Major Project 2", fname)
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Download images
if (!file.exists(fpath)){
download.file(request, fpath, mode = 'wb')
}
# =========== NO MODIFY ZONE ENDS HERE ========================================
}
Before you download GSV images, make sure the row
number in edges_azi is not too large! Each row corresponds
to one GSV image, so if the row count exceeds your API quota, consider
selecting different Census Tracts.
You do not want to run the following code chunk more than once, so the code chunk option
eval=FALSEis set to prevent the API call from executing again when knitting the script.
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
for (i in seq(1,nrow(edges_azi))){
getImage(edges_azi[i,])
}
# =========== NO MODIFY ZONE ENDS HERE ========================================
ZIP THE DOWNLOADED IMAGES AND NAME IT ‘gsv_images.zip’ FOR STEP 6.
Use this Google Colab script to apply the pretrained semantic segmentation model to your GSV images.
Once all of the images are processed and saved in your Colab session
as a CSV file, download the CSV file and merge it back to
edges_azi.
# TASK ////////////////////////////////////////////////////////////////////////
# Read the downloaded CSV file containing the semantic segmentation results.
seg_output <- read.csv('C:/GT MS UA/CP 8883/Projects/Major Project 2/seg_output.csv')
# //TASK ////////////////////////////////////////////////////////////////////////
# TASK ////////////////////////////////////////////////////////////////////////
# 1. Join the `seg_output` data to `edges_azi`.
# 2. Calculate the proportion of predicted pixels for the following categories: `building`, `sky`, `road`, and `sidewalk`. If there are other categories you are interested in, feel free to include their proportions as well.
# 3. Calculate the proportion of greenness using the `vegetation` and `terrain` categories.
# 4. Calculate the building-to-street ratio. For the street, use `road` and `sidewalk` pixels; including `car` pixels is optional.
edges_seg_output <- edges_azi %>%
left_join(seg_output, by="img_id") %>%
mutate(
prop_building = building/(640*640),
prop_sky = sky/(640*640),
prop_road = road/(640*640),
prop_sidewalk = sidewalk / (640*640),
prop_greenness = (vegetation + terrain)/(640*640),
street_pixels = road + sidewalk + car,
building_to_street_ratio = ifelse(street_pixels > 0,
building / street_pixels,
NA))
# //TASK ////////////////////////////////////////////////////////////////////////
At the beginning of this assignment, you specified walkable and unwalkable Census Tracts. The key focus of this section is the comparison between these two types of tracts.
Create interactive maps showing the proportion of sidewalk, greenness, and the building-to-street ratio for both walkable and unwalkable areas. In total, you will produce 6 maps. Provide a brief description of your findings.
# TASK ////////////////////////////////////////////////////////////////////////
# Plot interactive map(s)
# As long as you can deliver the message clearly, you can use any format/package you want.
walkable_edges <- edges_seg_output %>% filter(is_walkable == TRUE)
unwalkable_edges <- edges_seg_output %>% filter(is_walkable == FALSE)
make_map <- function(tract_data, t_col, edges_data, value_col, palette_name, title_) {
tm_shape(tract_data) +
tm_borders(col = t_col, lwd = 2) +
tm_shape(edges_data)+
tm_dots(col = value_col,
palette = palette_name,
size = 0.6)+
tm_layout(
title = title_,
legend.outside = TRUE)
}
tmap_mode("view")
w1<-make_map(tract_walkable, "darkgreen", walkable_edges, 'prop_sidewalk', "PuRd", "Walkable Census Tract: Proportion of Sidewalks")
w2<-make_map(tract_walkable, "darkgreen", walkable_edges, 'prop_greenness', "PuRd", "Walkable Census Tract: Proportion of Greenness")
w3<-make_map(tract_walkable, "darkgreen", walkable_edges, 'building_to_street_ratio', "PuRd", "Walkable Census Tract: Building-to-Street Ratio")
uw1<-make_map(tract_unwalkable, "red", unwalkable_edges, 'prop_sidewalk', "PuRd", "Unwalkable Census Tract: Proportion of Sidewalks")
uw2<-make_map(tract_unwalkable, "red", unwalkable_edges, 'prop_greenness', "PuRd", "Unwalkable Census Tract: Proportion of Greenness")
uw3<-make_map(tract_unwalkable, "red", unwalkable_edges, 'building_to_street_ratio', "PuRd", "Unwalkable Census Tract: Building-to-Street Ratio")
tmap_arrange(w1,w2,w3,uw1,uw2,uw3)
# //TASK //////////////////////////////////////////////////////////////////////
Create boxplots for the proportion of each category (building, sky, road, sidewalk, greenness, and any additional categories of interest) and the building-to-street ratio for walkable and unwalkable tracts. Each plot should compare walkable and unwalkable tracts. In total, you will produce 6 or more boxplots. Provide a brief description of your findings.
# TASK ////////////////////////////////////////////////////////////////////////
# Create boxplot(s) using ggplot2 package.
vars_to_plot <- c(
"prop_building",
"prop_sky",
"prop_road",
"prop_sidewalk",
"prop_greenness",
"building_to_street_ratio"
)
edges_seg_output <- edges_seg_output %>%
mutate(
tract_type = ifelse(is_walkable == TRUE, "walkable", "unwalkable")
)
plot_df <- edges_seg_output %>%
select(tract_type, all_of(vars_to_plot)) %>%
pivot_longer(
cols = all_of(vars_to_plot),
names_to = "variable",
values_to = "value"
)
ggplot(plot_df, aes(x = tract_type, y = value, fill = tract_type)) +
geom_boxplot(alpha = 0.8) +
facet_wrap(~ variable, scales = "free_y") +
scale_fill_manual(values = c("red", "lightgreen")) +
labs(
x = "Tract Type",
y = "Proportion / Ratio",
fill = "Tract Type"
) +
theme_minimal(base_size = 13)
# //TASK //////////////////////////////////////////////////////////////////////
Perform t-tests on the mean proportion of each category (building, sky, road, sidewalk, greenness, and any additional categories of interest) as well as the building-to-street ratio between street segments in the walkable and unwalkable tracts. This will result in 6 or more t-test results. Provide a brief description of your findings.
# TASK ////////////////////////////////////////////////////////////////////////
# Perform t-tests and report both the differences in means and their statistical significance.
# As long as you can deliver the message clearly, you can use any format/package you want.
cols_to_test <- c(
"prop_building",
"prop_sky",
"prop_road",
"prop_sidewalk",
"prop_greenness",
"building_to_street_ratio"
)
test_func <- function(data, vars) {
results <- lapply(vars, function(v) {
w <- data %>% filter(tract_type == "walkable") %>% pull(v)
u <- data %>% filter(tract_type == "unwalkable") %>% pull(v)
t_out <- t.test(w, u)
data.frame(
variable = v,
mean_walkable = mean(w, na.rm = TRUE),
mean_unwalkable = mean(u, na.rm = TRUE),
mean_diff = mean(w, na.rm = TRUE) - mean(u, na.rm = TRUE),
p_value = t_out$p.value
)
})
do.call(rbind, results)
}
# Run
t_test_results <- test_func(edges_seg_output, cols_to_test)
# View
print(t_test_results)
## variable mean_walkable mean_unwalkable mean_diff
## 1 prop_building 0.11225617 0.28643016 -0.17417400
## 2 prop_sky 0.34096407 0.20382502 0.13713905
## 3 prop_road 0.51311640 0.53326304 -0.02014664
## 4 prop_sidewalk 0.05776996 0.06520475 -0.00743479
## 5 prop_greenness 0.35135364 0.28870766 0.06264598
## 6 building_to_street_ratio 0.19081552 0.45973186 -0.26891634
## p_value
## 1 6.362724e-20
## 2 2.803339e-17
## 3 2.407518e-03
## 4 6.903160e-02
## 5 2.141336e-03
## 6 3.056429e-18
# //TASK //////////////////////////////////////////////////////////////////////
Analysing the boxplots shows that the largest differences appear in
the building-to-street ratio and the proportion of building, both of
which are considerably higher in unwalkable areas. These tracts likely
reflect dense, auto-oriented development with busy streets and a lower
sense of pedestrian comfort. Walkable areas, by contrast, show more
visible sky suggesting lower building heights and higher presence of
greenery, likely tied to consistent investment in street trees and
vegetation.
The t-tests reinforce these visual patterns. Unwalkable tracts have
significantly higher building proportions and building-to-street ratios,
while walkable tracts display substantially more visible sky and
greenness. Sidewalk proportions do not differ in a meaningful way, and
road proportions vary only slightly despite statistical
significance.
Together, these spatial, visual, and statistical insights show that perception of walkability emerges not from the mere presence of sidewalks or roads but from a coordinated urban form with human-scale building placement, continuous green infrastructure, well-proportioned streets, and coherent design patterns that support comfortable pedestrian movement.