This is an R Markdown document. Markdown is a simple formatting syntax for authoring HTML, PDF, and MS Word documents. For more details on using R Markdown see http://rmarkdown.rstudio.com.
When you click the Knit button a document will be generated that includes both content as well as the output of any embedded R code chunks within the document. You can embed an R code chunk like this:
Exploring Walkability Through Street View and Computer Vision
With the rapid expansion of street view services and computer vision technology, it has become possible to automatically quantify streetscapes at the street level.
As an early adopter of this technological advancement, your task is to use OpenStreetMap (OSM), Google Street View (GSV), and computer vision to explore which built environment characteristics contribute to walkable neighborhoods.
This assignment provides a code template, which you can download here. The template includes detailed guidance on how to use it, the tasks you will complete, and the step-by-step workflow.
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.2
## ✔ ggplot2 4.0.0 ✔ tibble 3.3.0
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.1
## ✔ purrr 1.1.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(magrittr)
##
## Attaching package: 'magrittr'
##
## The following object is masked from 'package:purrr':
##
## set_names
##
## The following object is masked from 'package:tidyr':
##
## extract
library(osmdata)
## Data (c) OpenStreetMap contributors, ODbL 1.0. https://www.openstreetmap.org/copyright
library(sfnetworks)
library(units)
## Warning: package 'units' was built under R version 4.5.2
## udunits database from C:/Users/akaamah3/AppData/Local/Programs/R/R-4.5.1/library/units/share/udunits/udunits2.xml
library(sf)
## Linking to GEOS 3.13.1, GDAL 3.11.0, PROJ 9.6.0; sf_use_s2() is TRUE
library(tidygraph)
##
## Attaching package: 'tidygraph'
##
## The following object is masked from 'package:stats':
##
## filter
library(tmap)
library(here)
## here() starts at C:/Users/akaamah3/Documents/SCaRP Course Materials_Fall2025/Into_to_Urban_Analytics/CP8883_working_with_R
library(progress)
library(nominatimlite)
library(tidycensus)
library(dplyr)
library(broom)
library(leaflet)
library(RColorBrewer)
library(osmdata)
ttm()
## ℹ tmap modes "plot" - "view"
## ℹ toggle with `tmap::ttm()`
# Set up your api key here
census_api_key(Sys.getenv("CENSUS_API"), install = FALSE)
## To install your API key for use in future sessions, run this function with `install = TRUE`.
# =========== 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)
## Getting data from the 2019-2023 5-year ACS
## Downloading feature geometry from the Census website. To cache shapefiles for use in future sessions, set `options(tigris_use_cache = TRUE)`.
## | | | 0% | | | 1% | |= | 1% | |= | 2% | |== | 2% | |== | 3% | |=== | 5% | |==== | 5% | |==== | 6% | |===== | 7% | |====== | 8% | |======= | 10% | |======= | 11% | |======== | 11% | |========= | 12% | |========= | 13% | |========== | 14% | |=========== | 15% | |=========== | 16% | |============ | 17% | |============= | 18% | |============= | 19% | |============== | 20% | |=============== | 21% | |=============== | 22% | |================ | 22% | |================ | 23% | |================= | 24% | |================= | 25% | |================== | 25% | |================== | 26% | |=================== | 27% | |=================== | 28% | |==================== | 28% | |==================== | 29% | |===================== | 30% | |===================== | 31% | |====================== | 31% | |====================== | 32% | |======================= | 33% | |======================== | 34% | |======================== | 35% | |========================= | 36% | |========================== | 37% | |=========================== | 38% | |=========================== | 39% | |============================ | 39% | |============================ | 40% | |============================= | 41% | |============================= | 42% | |============================== | 42% | |============================== | 43% | |=============================== | 44% | |=============================== | 45% | |================================ | 45% | |================================ | 46% | |================================= | 47% | |================================== | 48% | |================================== | 49% | |=================================== | 50% | |==================================== | 51% | |==================================== | 52% | |===================================== | 53% | |====================================== | 54% | |====================================== | 55% | |======================================= | 56% | |======================================== | 57% | |======================================== | 58% | |========================================= | 58% | |========================================= | 59% | |========================================== | 60% | |========================================== | 61% | |=========================================== | 61% | |============================================ | 62% | |============================================ | 63% | |============================================= | 64% | |============================================== | 65% | |============================================== | 66% | |=============================================== | 67% | |================================================ | 68% | |================================================ | 69% | |================================================= | 69% | |================================================= | 70% | |================================================== | 71% | |================================================== | 72% | |=================================================== | 72% | |=================================================== | 73% | |==================================================== | 74% | |==================================================== | 75% | |===================================================== | 75% | |===================================================== | 76% | |====================================================== | 77% | |====================================================== | 78% | |======================================================= | 78% | |======================================================= | 79% | |======================================================== | 80% | |======================================================== | 81% | |========================================================= | 81% | |========================================================= | 82% | |========================================================== | 83% | |=========================================================== | 84% | |=========================================================== | 85% | |============================================================ | 86% | |============================================================= | 87% | |============================================================== | 88% | |============================================================== | 89% | |=============================================================== | 89% | |=============================================================== | 90% | |================================================================ | 91% | |================================================================ | 92% | |================================================================= | 92% | |================================================================= | 93% | |================================================================== | 94% | |=================================================================== | 95% | |=================================================================== | 96% | |==================================================================== | 97% | |===================================================================== | 98% | |===================================================================== | 99% | |======================================================================| 100%
tmap_mode("view")
## ℹ tmap modes "plot" - "view"
tm_basemap("OpenStreetMap") +
tm_shape(tract) +
tm_polygons(fill_alpha = 0.2)
# =========== NO MODIFY ZONE ENDS HERE ========================================
# 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("13121010601", "13121008301")
tract_walkable <- tract %>%
filter(GEOID %in% tr_id_walkable)
# For the unwalkable Census Tract(s)
tr_id_unwalkable <- c("13089023212", "13089020500")
tract_unwalkable <- tract %>%
filter(GEOID %in% tr_id_unwalkable)
# //TASK //////////////////////////////////////////////////////////////////////
# TASK ////////////////////////////////////////////////////////////////////////
# Create an interactive map showing `tract_walkable` and `tract_unwalkable`
tmap_mode("view")
## ℹ tmap modes "plot" - "view"
tm_basemap("OpenStreetMap") +
tm_shape(tract) +
tm_polygons(alpha = 0.2) +
tm_shape(tract_walkable) +
tm_borders(col = "green", lwd = 3) +
tm_shape(tract_unwalkable) +
tm_borders(col = "red", lwd = 3)
##
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_polygons()`: use `fill_alpha` instead of `alpha`.
# //TASK //////////////////////////////////////////////////////////////////////
These Census Tracts are located in Midtown and Old Fourth Ward, which are among the most walkable neighborhoods in Atlanta. They have:
High population density, indicating a mix of apartments and multifamily housing. High intersection density and a fine-grained street grid, which makes walking easier. Proximity to MARTA rail stations (North Avenue, Midtown). Mixed land uses, including shops, restaurants, workplaces, and residential buildings. Access to major pedestrian infrastructure such as the Atlanta BeltLine Eastside Trail. These urban design patterns strongly support walkability.
These tracts fall in suburban DeKalb County, characterized by:
Low population density, mainly single-family homes. Disconnected street networks, often with cul-de-sacs and few sidewalks. No access to MARTA rail and limited bus service. Auto-oriented land use, such as strip malls, big parking lots, and wide arterials. Long distances between residential areas and stores/services. These characteristics result in very low walkability and strong car dependence.
# 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 <- st_bbox(tract %>% filter(GEOID %in% tr_id_walkable))
# For the unwalkable Census Tract(s)
tract_unwalkable_bb <- st_bbox(tract %>% filter(GEOID %in% tr_id_unwalkable))
# =========== 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 %>%
select(osm_id, highway) %>%
as_sfnetwork(directed = FALSE) %>%
convert(to_spatial_subdivision) %>% # split lines at intersections
convert(to_spatial_smooth, summarise_attributes = "first")
## Warning: to_spatial_subdivision assumes attributes are constant over geometries
net_unwalkable <- osm_unwalkable$osm_lines %>%
select(osm_id, highway) %>%
as_sfnetwork(directed = FALSE) %>%
convert(to_spatial_subdivision) %>%
convert(to_spatial_smooth, summarise_attributes = "first")
## Warning: to_spatial_subdivision assumes attributes are constant over geometries
# 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 %>%
activate(edges) %>%
mutate(length = st_length(geometry)) %>%
filter(length > units::set_units(91.44, "m")) %>%
group_by(highway) %>%
slice_sample(n = 100, replace = TRUE) %>%
ungroup() %>%
as_tibble() %>% # convert edge component to tibble
st_as_sf() # convert to sf (preserves geometry)
# OSM for the unwalkable part
edges_unwalkable <- net_unwalkable %>%
activate(edges) %>%
mutate(length = st_length(geometry)) %>%
filter(length > units::set_units(91.44, "m")) %>%
group_by(highway) %>%
slice_sample(n = 100, replace = TRUE) %>%
ungroup() %>%
as_tibble() %>%
st_as_sf()
# =========== 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.Collecting 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.
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(sample = c(0.48, 0.50, 0.52)) %>%
st_cast("POINT") %>%
st_coordinates()
# 4
mid_p <- mid_p3[2, ]
azimuth <- function(p_from, p_to) {
(atan2(p_to["X"] - p_from["X"],
p_to["Y"] - p_from["Y"]) * 180 / pi + 360) %% 360
}
# 5
mid_azi_1 <- azimuth(mid_p3[2, ], mid_p3[3, ]) # forward direction
mid_azi_2 <- azimuth(mid_p3[2, ], mid_p3[1, ]) # backward direction
# //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 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() %>% # extract geometries
map_dfr(getAzimuth) # apply getAzimuth() to each geometry and row-bind results
# =========== 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 # "mid1" or "mid2"
location <- paste0(iterrow$Y, ",", iterrow$X) # lat,lng format
heading <- iterrow$azi # azimuth
edge_id <- iterrow$edge_id
img_id <- iterrow$img_id
key <- Sys.getenv("GOOGLE_API") # assumes you stored your GSV API key as an environment variable
endpoint <- "https://maps.googleapis.com/maps/api/streetview"
furl <- paste0(
endpoint,
"?size=640x640", # image size
"&location=", location,
"&heading=", heading,
"&fov=90", # field of view (default 90)
"&pitch=0", # camera pitch
"&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("GSV_images", fname) # assumes a folder named "GSV_images" exists
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Download images
if (!file.exists(fpath)){
download.file(furl, fpath, mode = 'wb')
}
# =========== NO MODIFY ZONE ENDS HERE ========================================
}
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
for (i in seq(1,nrow(edges_azi))){
getImage(edges_azi[i,])
}
# =========== NO MODIFY ZONE ENDS HERE ========================================
Use this Google Colab script to apply the pretrained semantic segmentation model to your GSV images.
zip::zip("gsv_images.zip", files = list.files("GSV_images", full.names = TRUE))
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("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.
# Join segmentation output and compute all proportions
edges_seg_output <- edges_azi %>%
left_join(seg_output, by = "img_id") %>%
# Pixel totals safely computed
mutate(
total_pixels = rowSums(across(where(is.numeric)), na.rm = TRUE),
# Required proportions
prop_building = building / total_pixels,
prop_sky = sky / total_pixels,
prop_road = road / total_pixels,
prop_sidewalk = sidewalk / total_pixels,
prop_greenness = (vegetation + terrain) / total_pixels,
# Required: building-to-street ratio (road + sidewalk)
building_to_street_ratio = ifelse(
(road + sidewalk) == 0,
NA_real_,
building / (road + sidewalk)
)
)
# //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 ////////////////////////////////////////////////////////////////////////
# 6 interactive maps:
# - sidewalk (walkable + unwalkable)
# - greenness (walkable + unwalkable)
# - building-to-street ratio (walkable + unwalkable)
# Convert to sf if needed
edges_sf <- st_as_sf(edges_seg_output)
# Define a helper function to make maps quickly
make_map <- function(data, value_col, title_text) {
pal <- colorNumeric("YlOrRd", domain = data[[value_col]], na.color = "transparent")
leaflet(data) %>%
addTiles() %>%
addCircleMarkers(
radius = 4,
stroke = FALSE,
fillOpacity = 0.8,
color = ~pal(data[[value_col]])
) %>%
addLegend(
pal = pal,
values = data[[value_col]],
title = title_text
)
}
# --- Sidewalk proportion maps ---
walkable_sidewalk <- edges_sf %>% filter(is_walkable == 1)
unwalkable_sidewalk <- edges_sf %>% filter(is_walkable == 0)
map_sidewalk_walkable <- make_map(walkable_sidewalk, "prop_sidewalk", "Sidewalk Proportion (Walkable)")
map_sidewalk_unwalkable <- make_map(unwalkable_sidewalk, "prop_sidewalk", "Sidewalk Proportion (Unwalkable)")
# --- Greenness maps ---
walkable_green <- edges_sf %>% filter(is_walkable == 1)
unwalkable_green <- edges_sf %>% filter(is_walkable == 0)
map_green_walkable <- make_map(walkable_green, "prop_greenness", "Greenness Proportion (Walkable)")
map_green_unwalkable <- make_map(unwalkable_green, "prop_greenness", "Greenness Proportion (Unwalkable)")
# --- Building-to-street ratio maps ---
walkable_bs <- edges_sf %>% filter(is_walkable == 1)
unwalkable_bs <- edges_sf %>% filter(is_walkable == 0)
map_bs_walkable <- make_map(walkable_bs, "building_to_street_ratio", "Building-to-Street Ratio (Walkable)")
map_bs_unwalkable <- make_map(unwalkable_bs, "building_to_street_ratio", "Building-to-Street Ratio (Unwalkable)")
# Display maps
map_sidewalk_walkable
map_sidewalk_unwalkable
map_green_walkable
map_green_unwalkable
map_bs_walkable
map_bs_unwalkable
# //TASK //////////////////////////////////////////////////////////////////////
The “Building-to-Street Ratio” measures how closely buildings are positioned to the street. A higher ratio (closer to 1.0 or 1.4 on the scale) indicates that buildings are closer together and form a defined street wall, which is a key characteristic of pedestrian-friendly, walkable neighborhoods.
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.
# Columns we will plot
metrics <- c(
"prop_building",
"prop_sky",
"prop_road",
"prop_sidewalk",
"prop_greenness",
"building_to_street_ratio"
)
# Reshape data for ggplot
plot_df <- edges_sf %>%
select(is_walkable, all_of(metrics)) %>%
mutate(
is_walkable = factor(is_walkable, labels = c("Unwalkable", "Walkable"))
) %>%
pivot_longer(
cols = all_of(metrics),
names_to = "metric",
values_to = "value"
)
# Clean names for pretty facet labels
metric_labels <- c(
prop_building = "Building Proportion",
prop_sky = "Sky Proportion",
prop_road = "Road Proportion",
prop_sidewalk = "Sidewalk Proportion",
prop_greenness = "Greenness",
building_to_street_ratio = "Building-to-Street Ratio"
)
# Final boxplot
ggplot(plot_df, aes(x = is_walkable, y = value, fill = is_walkable)) +
geom_boxplot(outlier.alpha = 0.2) +
facet_wrap(~ metric, scales = "free_y",
labeller = labeller(metric = metric_labels)) +
scale_fill_brewer(palette = "Set2") +
labs(
x = "Tract Type",
y = "Value",
title = "Comparison of Street-View Features: Walkable vs. Unwalkable Tracts"
) +
theme_bw(base_size = 14) +
theme(
legend.position = "none",
strip.text = element_text(size = 12, face = "bold")
)
Observations from Building-to-Street Ratio plots show that Walkable streets have a significantly higher median Building-to-Street Ratio compared to unwalkable streets. However, Unwalkable streets have a much higher median Road Proportion. The building proportion in walkable streets also has a higher median Building Proportion in the street-view imagery. The plots again revealed that walkable streets have a slightly higher median Greenness. This could often reflect on the presence of street trees, small parks, and landscaping in walkable neighborhoods. The sidewalk proportion plot also suggests that walkable streets have higher median than unwalkable neighborhood. Unwalkable streets have a higher median Sky Proportion. This could suggest the presence of fewer tall buildings and a more open sky view,as compared to walkable streets in dense urban areas.
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.
# Variables to test
metrics <- c(
"prop_building",
"prop_sky",
"prop_road",
"prop_sidewalk",
"prop_greenness",
"building_to_street_ratio"
)
# Ensure walkability is a factor
edges_sf <- edges_sf %>%
mutate(is_walkable = factor(is_walkable, labels = c("Unwalkable", "Walkable")))
# Function to run t-test for one variable
run_t_test <- function(var) {
# Perform Welch's t-test
test <- t.test(edges_sf[[var]] ~ edges_sf$is_walkable, var.equal = FALSE)
# Get means
means <- edges_sf %>%
group_by(is_walkable) %>%
summarise(mean_value = mean(.data[[var]], na.rm = TRUE)) %>%
pull(mean_value)
# Return a tibble with all required statistics
tibble(
variable = var,
mean_unwalkable = means[1],
mean_walkable = means[2],
mean_difference = means[2] - means[1],
t_statistic = as.numeric(test$statistic),
df = as.numeric(test$parameter),
p_value = test$p.value
)
}
# Apply t-test to all metrics and combine results
t_test_results <- bind_rows(lapply(metrics, run_t_test))
# View results
t_test_results
## # A tibble: 6 × 7
## variable mean_unwalkable mean_walkable mean_difference t_statistic df
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 prop_building 0.0204 0.0223 0.00187 -0.877 1108.
## 2 prop_sky 0.218 0.183 -0.0352 4.94 1088.
## 3 prop_road 0.372 0.358 -0.0139 2.55 1124.
## 4 prop_sidewalk 0.0251 0.0267 0.00156 -0.981 1088.
## 5 prop_greenness 0.336 0.386 0.0505 -5.14 1085.
## 6 building_to_s… 0.0552 0.0593 0.00409 -0.676 1127.
## # ℹ 1 more variable: p_value <dbl>
# //TASK //////////////////////////////////////////////////////////////////////
The t-test results indicate that walkable tracts differ significantly from unwalkable tracts in different key street-view features. Walkable tracts have significantly lower sky and road proportions (p < 0.01) and a significantly higher greenness proportion (p < 0.001). This implies the existance of denser canopies, narrower streets, and more vegetation along walkable streets. However, differences in building proportion, sidewalk proportion, and building-to-street ratio are small and not statistically significant (p > 0.05). Overall, these results highlight that walkable streets tend to be greener and more enclosed, with less visible sky and road surface. This is essentially consistent with urban design characteristics that promote walkability.