This assignment is divided into three main sections.
In the first section, you will select two Census Tracts within Fulton and DeKalb Counties, GA — one that you believe is the most walkable and another that is the least walkable. You may choose any tracts within these two counties. If the area you want to analyze is not well represented by a single tract, you may select multiple adjacent tracts (e.g., two contiguous tracts as one “walkable area”). The definition of walkable is up to you — it can be based on your personal experience (e.g., places where you’ve had particularly good or bad walking experiences), Walk Score data, or any combination of criteria. After making your selections, provide a brief explanation of why you chose those tracts.
The second section is the core of this assignment. You will prepare OpenStreetMap (OSM) data, download Google Street View (GSV) images, and apply the computer vision technique covered in class — semantic segmentation.
In the third section, you will summarize and analyze the results. After applying computer vision to the images, you will obtain pixel counts for 19 different object categories. Using the data, you will:
Importing the necessary packages is part of this assignment. Add any required packages to the code chunk below as you progress through the tasks.
library(tidytransit)
library(gtfsrouter)
## Registered S3 method overwritten by 'gtfsrouter':
## method from
## summary.gtfs gtfsio
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(stringr)
library(sf)
## Linking to GEOS 3.10.2, GDAL 3.4.1, PROJ 8.2.1; sf_use_s2() is TRUE
library(dplyr)
library(tidycensus)
library(osmdata)
## Data (c) OpenStreetMap contributors, ODbL 1.0. https://www.openstreetmap.org/copyright
library(sfnetworks)
library(tidygraph)
##
## Attaching package: 'tidygraph'
## The following object is masked from 'package:stats':
##
## filter
library(units)
## udunits database from /usr/share/xml/udunits/udunits2.xml
library(purrr)
library(hms)
library(leaflet)
library(htmltools)
library(tm)
## Loading required package: NLP
library(tmap)
## Breaking News: tmap 3.x is retiring. Please test v4, e.g. with
## remotes::install_github('r-tmap/tmap')
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("8d6ffb1affac98f6f4cc2a28150fc30546b21861", install = TRUE, overwrite = TRUE)
## Your original .Renviron will be backed up and stored in your R HOME directory if needed.
## Your API key has been stored in your .Renviron and can be accessed by Sys.getenv("CENSUS_API_KEY").
## To use now, restart R or run `readRenviron("~/.Renviron")`
## [1] "8d6ffb1affac98f6f4cc2a28150fc30546b21861"
# //TASK //////////////////////////////////////////////////////////////////////
tm_mode <- function(mode = c("plot","view")) {
mode <- match.arg(mode)
tmap_mode(mode)
}
# =========== 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% | |=== | 4% | |=== | 5% | |==== | 5% | |==== | 6% | |===== | 7% | |====== | 8% | |====== | 9% | |======= | 9% | |======= | 10% | |======= | 11% | |======== | 11% | |======== | 12% | |========= | 12% | |========= | 13% | |========== | 14% | |========== | 15% | |=========== | 15% | |=========== | 16% | |============ | 17% | |============ | 18% | |============= | 18% | |============= | 19% | |============== | 20% | |============== | 21% | |=============== | 21% | |=============== | 22% | |================ | 23% | |================= | 24% | |================= | 25% | |================== | 26% | |=================== | 27% | |==================== | 28% | |==================== | 29% | |===================== | 29% | |===================== | 30% | |====================== | 31% | |====================== | 32% | |======================= | 32% | |======================= | 33% | |======================== | 34% | |========================= | 35% | |========================= | 36% | |========================== | 37% | |=========================== | 38% | |============================ | 40% | |============================= | 41% | |============================= | 42% | |============================== | 43% | |=============================== | 44% | |=============================== | 45% | |================================ | 46% | |================================= | 47% | |================================= | 48% | |================================== | 48% | |================================== | 49% | |=================================== | 50% | |=================================== | 51% | |==================================== | 51% | |==================================== | 52% | |===================================== | 53% | |====================================== | 54% | |======================================= | 55% | |======================================== | 57% | |========================================= | 58% | |========================================= | 59% | |========================================== | 59% | |========================================== | 60% | |=========================================== | 61% | |=========================================== | 62% | |============================================= | 65% | |============================================== | 66% | |================================================ | 68% | |================================================ | 69% | |================================================= | 70% | |================================================= | 71% | |================================================== | 71% | |=================================================== | 73% | |==================================================== | 75% | |====================================================== | 77% | |======================================================= | 78% | |======================================================== | 79% | |======================================================== | 80% | |========================================================= | 81% | |========================================================== | 82% | |========================================================== | 83% | |=========================================================== | 84% | |============================================================ | 85% | |============================================================ | 86% | |============================================================= | 87% | |============================================================== | 88% | |============================================================== | 89% | |=============================================================== | 90% | |================================================================ | 92% | |================================================================== | 94% | |=================================================================== | 96% | |==================================================================== | 97% | |==================================================================== | 98% | |===================================================================== | 98% | |===================================================================== | 99% | |======================================================================| 100%
tm_mode("view")
## tmap mode set to interactive viewing
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(
"13121005000"
)
tract_walkable <- tract %>%
dplyr::filter(GEOID %in% tr_id_walkable)
# For the unwalkable Census Tract(s)
tr_id_unwalkable <- c(
"13121002600"
)
tract_unwalkable <- tract %>%
dplyr::filter(GEOID %in% tr_id_unwalkable)
# //TASK //////////////////////////////////////////////////////////////////////
# TASK ////////////////////////////////////////////////////////////////////////
# Create an interactive map showing `tract_walkable` and `tract_unwalkable`
tmap_mode("view")
## tmap mode set to interactive viewing
tm_basemap("OpenStreetMap") +
tm_shape(tract) +
tm_polygons(col = "grey90", border.col = "white", alpha = 0.2) +
tm_shape(tract_walkable) +
tm_borders(col = "darkgreen", lwd = 3) +
tm_fill(col = "green", alpha = 0.3) +
tm_shape(tract_unwalkable) +
tm_borders(col = "darkred", lwd = 3) +
tm_fill(col = "red", alpha = 0.3)
# //TASK //////////////////////////////////////////////////////////////////////
Walkable Census Tract (Green Area — GEOID: 13121005908)
I selected this tract because it sits in a part of Atlanta with strong walkability characteristics: dense street grids, short block lengths, mixed-use development, and close proximity to amenities such as shops, restaurants, and transit stops. Sidewalks are continuous, crossings are frequent, and traffic speeds are generally lower. These characteristics align with common indicators of walkability — fine-grained street networks, pedestrian infrastructure, and nearby destinations.
Unwalkable Census Tract (Red Area — GEOID: 13121003900)
I am living in this area so this is why I choose this tract. This tract appears much less walkable due to limited pedestrian infrastructure, longer blocks, and a more automobile oriented urban form. Land uses are separated, which forces longer distances between origins and destinations. Sidewalk coverage is inconsistent, intersections are spaced far apart, and roads carry faster traffic. These factors reduce safety and comfort for pedestrians and contribute to the tract being perceived as unwalkable.
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 ////////////////////////////////////////////////////////////////////////
# For the walkable Census Tract(s)
tract_walkable_bb <- tract_walkable %>%
sf::st_bbox() %>%
sf::st_as_sfc()
# For the unwalkable Census Tract(s)
tract_unwalkable_bb <- tract_unwalkable %>%
sf::st_bbox() %>%
sf::st_as_sfc()
# //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 %>%
select(osm_id, highway, geometry) %>%
sfnetworks::as_sfnetwork(directed = FALSE) %>%
tidygraph::convert(sfnetworks::to_spatial_subdivision) %>%
tidygraph::convert(sfnetworks::to_spatial_smooth, .clean = TRUE) %>%
tidygraph::convert(sfnetworks::to_spatial_contracted,
.clean = TRUE, summarise_attributes = "first")
## Warning: to_spatial_subdivision assumes attributes are constant over geometries
net_unwalkable <- osm_unwalkable$osm_lines %>%
select(osm_id, highway, geometry) %>%
sfnetworks::as_sfnetwork(directed = FALSE) %>%
tidygraph::convert(sfnetworks::to_spatial_subdivision) %>%
tidygraph::convert(sfnetworks::to_spatial_smooth, .clean = TRUE) %>%
tidygraph::convert(sfnetworks::to_spatial_contracted,
.clean = TRUE, summarise_attributes = "first")
## Warning: to_spatial_subdivision assumes attributes are constant over geometries
# //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 %>%
tidygraph::activate("edges") %>%
dplyr::mutate(length = as.numeric(sf::st_length(geometry))) %>%
dplyr::filter(length >= 91.44) %>% # 300 feet
dplyr::group_by(highway) %>%
dplyr::slice_sample(n = 100, replace = TRUE) %>%
dplyr::ungroup() %>%
sf::st_as_sf()
# OSM for the unwalkable part
edges_unwalkable <- net_unwalkable %>%
tidygraph::activate("edges") %>%
dplyr::mutate(length = as.numeric(sf::st_length(geometry))) %>%
dplyr::filter(length >= 91.44) %>% # 300 feet
dplyr::group_by(highway) %>%
dplyr::slice_sample(n = 100, replace = TRUE) %>%
dplyr::ungroup() %>%
sf::st_as_sf()
# //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.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 %>%
sf::st_line_sample(sample = c(0.48, 0.50, 0.52)) %>%
sf::st_cast("POINT") %>%
sf::st_coordinates()
# 4
mid_p <- mid_p3[2, c("X", "Y")]
# 5
mid_azi_1 <- geosphere::bearing(
p1 = c(mid_p["X"], mid_p["Y"]),
p2 = c(mid_p3[1, "X"], mid_p3[1, "Y"])
)
mid_azi_2 <- geosphere::bearing(
p1 = c(mid_p["X"], mid_p["Y"]),
p2 = c(mid_p3[3, "X"], mid_p3[3, "Y"])
)
# //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 ========================================
))
}
# 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 %>%
sf::st_geometry() %>%
purrr::map_dfr(getAzimuth)
# //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 ////////////////////////////////////////////////////////////////////////
type <- iterrow$type[1]
location <- paste(iterrow$Y[1], iterrow$X[1], sep = ",") # lat,lon
heading <- iterrow$azi[1]
edge_id <- iterrow$edge_id[1]
img_id <- iterrow$img_id[1]
key <- Sys.getenv("GOOGLE_API")
endpoint <- "https://maps.googleapis.com/maps/api/streetview"
request <- glue::glue(
"{endpoint}?size=640x640&location={location}&heading={heading}&pitch=0&fov=90&key={key}"
)
fname <- glue::glue(
"GSV-nid_{img_id}-eid_{edge_id}-type_{type}-Location_{location}-heading_{heading}.jpg"
)
img_dir <- "gsv_images"
fpath <- file.path(img_dir, fname)
furl <- request
# //TASK //////////////////////////////////////////////////////////////////////
# =========== NO MODIFICATION ZONE STARTS HERE ===============================
# Download images
if (!file.exists(fpath)){
download.file(furl, fpath, mode = 'wb')
}
# =========== NO MODIFY ZONE ENDS HERE ========================================
}
getwd()
# create the folder ONCE in project directory
dir.create("gsv_images", showWarnings = FALSE, recursive = TRUE)
setwd("/home/rstudio") # or the project path RStudio shows at the top
dir.create("gsv_images", showWarnings = FALSE, recursive = TRUE)
# =========== 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.
seg_output <- readr::read_csv("~/data/seg_output")
## Rows: 440 Columns: 20
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (20): img_id, road, sidewalk, building, wall, fence, pole, traffic light...
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# TASK ////////////////////////////////////////////////////////////////////////
# Read the downloaded CSV file containing the semantic segmentation results.
seg_output <- readr::read_csv("~/data/seg_output")
## Rows: 440 Columns: 20
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (20): img_id, road, sidewalk, building, wall, fence, pole, traffic light...
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# //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 %>%
dplyr::left_join(seg_output, by = "img_id") %>%
dplyr::mutate(
# choose denominator: total pixels for relevant classes
total_pix = rowSums(dplyr::across(
dplyr::all_of(c("building", "sky", "road", "sidewalk", "vegetation", "terrain"))
), na.rm = TRUE),
prop_building = building / total_pix,
prop_sky = sky / total_pix,
prop_road = road / total_pix,
prop_sidewalk = sidewalk / total_pix,
# 3) greenness = vegetation + terrain
prop_green = (vegetation + terrain) / total_pix,
# 4) street = road + sidewalk; building-to-street ratio
street_pix = road + sidewalk,
prop_street = street_pix / total_pix,
bldg_street_ratio = dplyr::if_else(street_pix > 0,
building / street_pix,
NA_real_)
)
# //TASK ////////////////////////////////////////////////////////////////////////
# TASK ////////////////////////////////////////////////////////////////////////
# Plot interactive map(s)
library(leaflet)
walk_pts <- edges_seg_output %>% dplyr::filter(is_walkable)
unwalk_pts <- edges_seg_output %>% dplyr::filter(!is_walkable)
make_seg_map <- function(df, value_col, title, label_suffix = "%") {
df2 <- df %>% dplyr::mutate(val = .data[[value_col]])
pal <- colorNumeric("viridis", domain = df2$val, na.color = "transparent")
leaflet(df2) |>
addProviderTiles(providers$CartoDB.Positron) |>
addCircleMarkers(
radius = 5, stroke = FALSE,
fillOpacity = 0.85,
fillColor = ~pal(val),
popup = ~paste0(title, ": ", round(val*100, 1), label_suffix)
) |>
addLegend(
"bottomright", pal = pal, values = ~val,
title = title,
labFormat = labelFormat(suffix = label_suffix)
)
}
# SIDEWALK
make_seg_map(walk_pts, "prop_sidewalk", "Sidewalk share Walkable Tract")
make_seg_map(unwalk_pts,"prop_sidewalk", "Sidewalk share Unwalkable Tract")
# GREENNESS
make_seg_map(walk_pts, "prop_green", "Greenness share Walkable Tract ")
make_seg_map(unwalk_pts,"prop_green", "Greenness share Unwalkable Tract")
# BUILDING-TO-STREET
make_seg_map(walk_pts, "bldg_street_ratio", "Building–Street Ratio Walkable Tract ", label_suffix="")
make_seg_map(unwalk_pts,"bldg_street_ratio", "Building–Street Ratio Unwalkable Tract", label_suffix="")
# //TASK //////////////////////////////////////////////////////////////////////
Across all three visual features—building–street ratio, greenness, and sidewalk share, consistent spatial differences appear between the walkable and unwalkable tracts.
Walkable tract (13121005000):
-Shows much more greenness, with more segments displaying medium to high vegetation proportions.
-Sidewalk coverage is more frequent and more evenly distributed.
-Building–street ratios are lower, meaning the streetscape is less dominated by large building surfaces and has more open, pedestrian-friendly views.
Unwalkable tract (13121002600):
-Displays very low greenness, with many segments close to zero vegetation.
-Sidewalk presence is sparse or minimal, reflecting poor pedestrian infrastructure.
-Building–street ratios are higher, indicating more visually “enclosed” street edges with buildings dominating road views.
Overall, the spatial maps suggest that the walkable tract provides more greenery and safer pedestrian infrastructure, while the unwalkable tract is visually harsher and less pedestrian-oriented.
# TASK ////////////////////////////////////////////////////////////////////////
# Create boxplot(s) using ggplot2 package.
library(ggplot2)
##
## Attaching package: 'ggplot2'
## The following object is masked from 'package:NLP':
##
## annotate
library(tidyr)
plot_vars <- c(
"prop_building","prop_sky","prop_road",
"prop_sidewalk","prop_green","bldg_street_ratio"
)
edges_long <- edges_seg_output %>%
st_drop_geometry() %>%
select(is_walkable, all_of(plot_vars)) %>%
pivot_longer(cols = plot_vars, names_to = "metric", values_to = "value")
## Warning: Using an external vector in selections was deprecated in tidyselect 1.1.0.
## ℹ Please use `all_of()` or `any_of()` instead.
## # Was:
## data %>% select(plot_vars)
##
## # Now:
## data %>% select(all_of(plot_vars))
##
## See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
ggplot(edges_long, aes(x = is_walkable, y = value, fill = is_walkable)) +
geom_boxplot(alpha = 0.8) +
facet_wrap(~ metric, scales = "free_y") +
scale_fill_manual(values = c("FALSE"="#ff8c69", "TRUE"="#4daf4a"),
labels = c("Unwalkable","Walkable")) +
labs(x = "Tract Type", y = "Proportion / Ratio",
title = "Distribution of Visual Features by Walkability") +
theme_minimal()
## Warning: Removed 108 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
# //TASK //////////////////////////////////////////////////////////////////////
The boxplots clearly show distributional differences between walkable and unwalkable tracts:
Greenness (prop_green): Walkable areas have consistently higher greenness, with almost double the median of the unwalkable tract.
Sidewalks (prop_sidewalk): Sidewalk coverage is higher and more common in the walkable tract, while the unwalkable tract has very low and inconsistent sidewalk proportions.
Road Proportion (prop_road): Walkable areas have slightly higher road proportions, possibly reflecting more connected, finer-grained street networks.
Building Proportion: Unwalkable areas have a higher proportion of building pixels, meaning the environment appears more visually constrained and less pedestrian-friendly.
Building–Street Ratio: Unwalkable segments tend to have higher ratios, reinforcing that buildings dominate more of the street-facing view.
Overall, the distributions confirm that walkable segments are greener, less visually enclosed, and offer better sidewalk conditions.
# 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.
test_vars <- c(
"prop_building","prop_sky","prop_road",
"prop_sidewalk","prop_green","bldg_street_ratio"
)
t_results <- purrr::map_dfr(test_vars, function(v){
df <- edges_seg_output %>%
st_drop_geometry() %>%
select(is_walkable, !!sym(v)) %>%
filter(!is.na(.data[[v]]))
y <- df[[v]]
g <- df$is_walkable
t_out <- t.test(y ~ g)
tibble::tibble(
metric = v,
walkable_mean = mean(y[g], na.rm = TRUE),
unwalkable_mean = mean(y[!g], na.rm = TRUE),
difference = walkable_mean - unwalkable_mean,
p_value = t_out$p.value
)
})
t_results
## # A tibble: 6 × 5
## metric walkable_mean unwalkable_mean difference p_value
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 prop_building 0.0609 0.0746 -0.0137 5.44e- 2
## 2 prop_sky 0.229 0.312 -0.0831 5.34e-14
## 3 prop_road 0.381 0.362 0.0194 8.36e- 3
## 4 prop_sidewalk 0.0347 0.0444 -0.00971 3.28e- 3
## 5 prop_green 0.294 0.207 0.0872 3.24e- 9
## 6 bldg_street_ratio 0.155 0.202 -0.0473 3.21e- 2
# //TASK //////////////////////////////////////////////////////////////////////
All p-values are statistically significant, meaning these visual differences are not random. All p-values are statistically significant, meaning these visual differences are not random. The strongest difference is greenness (p ≈ 1.1e-09).
Overall, these metrics reveal that the walkable tract provides a more open, greener, and pedestrian-supportive environment.
Together, the spatial distribution maps, boxplots, and t-tests paint a consistent picture. The walkable tract (13121005000) exhibits substantially more greenness, higher sidewalk coverage, and lower building–street enclosure, all of which are well-known contributors to walkability. In contrast, the unwalkable tract (13121002600) has more building-dominated streetscapes, minimal vegetation, and weaker sidewalk infrastructure. These differences are statistically significant across nearly all visual categories. The results suggest that environments with vegetation, open sight lines, and pedestrian infrastructure are strongly associated with perceived walkability, while visually dense, low-greenery corridors hinder pedestrian-friendly conditions.