This tutorial uses descriptive statistics and visualizations to examine rurality, climate normals, and American Community Survey data. The climate normals and ACS values are already aggregated over time or space, so the results below are summaries of data that have already been summarized.
glimpse() provides a quick look at the number of
observations, variable names, and data types in each dataset.
## Rows: 2,672
## Columns: 4
## $ GEOID <chr> "37141920300", "37141990100", "37071031600", "37071031…
## $ ruca_code <chr> "Metropolitan high commuting", "Not coded", "Metropoli…
## $ nc_rural_center <chr> "Rural", "Rural", "Not Rural", "Not Rural", "Rural", "…
## $ geometry <MULTIPOLYGON [°]> MULTIPOLYGON (((-78.15648 3..., MULTIPOLY…
## Rows: 469,758
## Columns: 2
## $ summer_temp <dbl> 56.25781, 56.25312, 56.27656, 56.40313, 56.46875, 57.36875…
## $ geometry <POINT [°]> POINT (-124.6875 48.0625), POINT (-124.6875 48.10417…
## Rows: 2,672
## Columns: 54
## $ STATEFP <chr> "37", "37", "37", "37", "37", "37", "37", "37", "37",…
## $ COUNTYFP <chr> "031", "031", "031", "031", "031", "031", "031", "119…
## $ TRACTCE <chr> "970402", "970502", "970805", "970903", "970101", "97…
## $ GEOID <chr> "37031970402", "37031970502", "37031970805", "3703197…
## $ NAME <chr> "9704.02", "9705.02", "9708.05", "9709.03", "9701.01"…
## $ NAMELSAD <chr> "Census Tract 9704.02", "Census Tract 9705.02", "Cens…
## $ MTFCC <chr> "G5020", "G5020", "G5020", "G5020", "G5020", "G5020",…
## $ FUNCSTAT <chr> "S", "S", "S", "S", "S", "S", "S", "S", "S", "S", "S"…
## $ ALAND <dbl> 2395384, 6094915, 139248627, 5864308, 28018671, 97430…
## $ AWATER <dbl> 1553995, 4244211, 421209, 30151466, 35773987, 1568180…
## $ INTPTLAT <chr> "+34.7239933", "+34.7517653", "+34.7817392", "+34.701…
## $ INTPTLON <chr> "-076.7087383", "-076.7387561", "-077.0184797", "-076…
## $ tract_name <chr> "Census Tract 9704.02, Carteret County, North Carolin…
## $ total_pop <dbl> 1309, 2177, 1694, 1211, 965, 4244, 2306, 2720, 6076, …
## $ median_age <dbl> 39.3, 56.1, 43.3, 62.1, 48.4, 55.9, 42.3, 28.3, 29.7,…
## $ pct_white <dbl> 64.17, 93.11, 85.36, 85.47, 86.22, 72.64, 72.94, 6.32…
## $ pct_black <dbl> 21.01, 2.30, 0.00, 0.66, 3.21, 17.86, 9.71, 45.96, 30…
## $ pct_aian <dbl> 0.15, 0.00, 0.41, 0.41, 0.00, 2.00, 0.00, 0.00, 0.05,…
## $ pct_asian <dbl> 0.00, 0.00, 1.89, 0.00, 0.00, 0.00, 4.68, 2.90, 1.50,…
## $ pct_nhpi <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.17, 1.32, 0.00,…
## $ pct_other_race <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 1.06, 0.22, 0.00, 0.00,…
## $ pct_two_races <dbl> 12.15, 0.32, 10.68, 12.80, 0.83, 3.79, 8.24, 1.69, 0.…
## $ pct_hisp <dbl> 2.52, 4.27, 1.65, 0.66, 9.74, 2.64, 4.03, 41.80, 63.8…
## $ pct_family_hh <dbl> 39.31, 64.53, 73.30, 64.78, 65.03, 64.45, 62.60, 61.2…
## $ married_fam <dbl> 19.08, 55.75, 55.45, 61.55, 45.08, 55.44, 42.31, 28.3…
## $ male_hh <dbl> 3.71, 0.00, 3.81, 0.48, 9.84, 3.64, 2.88, 9.40, 8.66,…
## $ female_hh <dbl> 16.52, 8.77, 14.03, 2.75, 10.11, 5.37, 17.40, 23.56, …
## $ pct_nonfam_hh <dbl> 60.69, 35.47, 26.70, 35.22, 34.97, 35.55, 37.40, 38.7…
## $ nonfam_male_hh <dbl> 37.00, 16.13, 15.26, 14.22, 18.31, 17.25, 13.37, 13.7…
## $ nonfam_fem_hh <dbl> 23.69, 19.34, 11.44, 21.00, 16.67, 18.30, 24.04, 25.0…
## $ av_hh_size <dbl> 1.64, 2.05, 2.31, 1.96, 2.37, 2.03, 2.21, 3.01, 2.89,…
## $ pct_less_hs <dbl> 17.40, 7.95, 9.49, 0.00, 8.64, 18.64, 6.50, 19.05, 29…
## $ pct_hs <dbl> 52.21, 53.92, 69.15, 41.87, 67.83, 59.63, 61.34, 60.9…
## $ pc_bach <dbl> 30.38, 38.13, 21.36, 58.13, 23.54, 21.73, 32.17, 19.9…
## $ pct_unemp <dbl> 5.67, 0.41, 0.86, 1.47, 1.45, 0.13, 0.95, 5.91, 8.52,…
## $ med_hh_inc <dbl> 45391, 86583, 66548, 96198, 58750, 67195, 63393, 4844…
## $ gini_index <dbl> 0.4094, 0.4039, 0.4377, 0.4689, 0.5036, 0.4248, 0.462…
## $ pct_owner <dbl> 41.87, 87.64, 81.47, 86.11, 81.42, 70.29, 62.98, 25.1…
## $ pct_renter <dbl> 58.13, 12.36, 18.53, 13.89, 18.58, 29.71, 37.02, 74.8…
## $ pct_vacant <dbl> 19.57, 10.09, 18.90, 78.84, 45.29, 14.68, 8.29, 6.22,…
## $ med_year_built <dbl> 1962, 1985, 1997, 1988, 1965, 1994, 1984, 1983, 1986,…
## $ pct_gas <dbl> 18.95, 13.02, 7.36, 13.41, 31.15, 2.30, 3.56, 49.89, …
## $ pct_electric <dbl> 77.21, 85.66, 85.01, 84.81, 53.83, 96.26, 95.67, 49.7…
## $ med_house_val <dbl> 234100, 329900, 208800, 669300, 168500, 262600, 23640…
## $ med_rent <dbl> 781, 1241, 1133, 1489, 1000, 762, 1184, 1355, 1309, 2…
## $ pct_pov <dbl> 26.02, 4.10, 9.63, 4.21, 16.63, 9.50, 18.35, 16.09, 1…
## $ pct_drive_work <dbl> 90.34, 89.37, 87.40, 75.43, 78.46, 85.78, 94.20, 86.3…
## $ pct_public_trans <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 4.12, 3.60,…
## $ bike_walk <dbl> 3.22, 0.00, 2.95, 0.00, 2.25, 0.00, 1.40, 2.22, 0.00,…
## $ pct_wfh <dbl> 6.44, 7.95, 8.17, 24.57, 8.36, 10.30, 4.12, 5.43, 11.…
## $ av_commute <dbl> 17, 20, 26, 28, 32, 17, 23, 19, 25, 24, 25, 23, 25, 2…
## $ pct_no_veh <dbl> 27.53, 0.57, 0.00, 3.07, 4.92, 2.97, 3.94, 7.52, 7.90…
## $ no_health_insur <dbl> 19.39, 7.21, 15.13, 4.29, 7.85, 16.02, 6.16, 14.72, 4…
## $ geometry <MULTIPOLYGON [°]> MULTIPOLYGON (((-76.74294 3..., MULTIPOL…
Each row represents one North Carolina census tract. I can tell
because every observation has a census tract GEOID, one
RUCA category, and a multipolygon geometry defining the tract’s
boundary.
ruca_code?ruca_code is a categorical variable. The labels contain
information about the type of place and commuting relationship, but they
are not a continuous numeric scale. Counts, percentages, and the mode
are appropriate. A mean or standard deviation would not make sense.
rural_summary <- rurality |>
st_drop_geometry() |>
count(ruca_code, name = "n") |>
mutate(percent = 100 * n / sum(n)) |>
arrange(desc(n))
rural_summary |>
gt() |>
tab_header(title = "North Carolina Census Tracts by RUCA Category") |>
cols_label(
ruca_code = "RUCA category",
n = "Number of tracts",
percent = "Percent"
) |>
fmt_number(columns = percent, decimals = 1)| North Carolina Census Tracts by RUCA Category | ||
| RUCA category | Number of tracts | Percent |
|---|---|---|
| Metropolitan core | 1436 | 53.7 |
| Metropolitan high commuting | 406 | 15.2 |
| Rural area | 225 | 8.4 |
| Micropolitan core | 218 | 8.2 |
| Micropolitan high commuting | 134 | 5.0 |
| Metropolitan low commuting | 87 | 3.3 |
| Micropolitan low commuting | 79 | 3.0 |
| Small town core | 39 | 1.5 |
| Small town high commuting | 20 | 0.7 |
| Small town low commuting | 16 | 0.6 |
| Not coded | 12 | 0.4 |
ruca_code?The mode is Metropolitan core, which is the most common RUCA category in the dataset.
ggplot(rural_summary, aes(x = reorder(ruca_code, n), y = n)) +
geom_col(fill = "#4C78A8") +
coord_flip() +
labs(
title = "North Carolina Census Tracts by RUCA Category",
x = "RUCA category",
y = "Number of census tracts"
) +
theme_minimal()tm_shape(rurality) +
tm_polygons(
fill = "ruca_code",
col = "white",
lwd = 0.25,
fill.scale = tm_scale_categorical(values = "brewer.paired"),
fill.legend = tm_legend(
title = "RUCA category",
position = tm_pos_out("right", "center"),
frame = FALSE
)
) +
tm_title("Rural-Urban Commuting Areas in North Carolina", size = 1.5) +
tm_layout(frame = TRUE)Metropolitan-core tracts cluster around Charlotte, the Triangle, the Triad, Asheville, Wilmington, and other cities. Rural and small-town categories are more common between metropolitan areas and in parts of eastern and western North Carolina. The table shows which categories are common, but the map shows that those categories are geographically clustered rather than randomly scattered.
Each row represents one point in a regularly spaced geographic grid
across the contiguous United States. The POINT geometry
gives the location, and summer_temp gives the 1991–2020
average summer temperature at that location.
climate_stats <- climate_values |>
summarise(
n = n(),
num_na = sum(is.na(summer_temp)),
mean_temp = mean(summer_temp, na.rm = TRUE),
median_temp = median(summer_temp, na.rm = TRUE),
sd_temp = sd(summer_temp, na.rm = TRUE),
mean_dev = mean(
abs(summer_temp - mean(summer_temp, na.rm = TRUE)),
na.rm = TRUE
),
min_temp = min(summer_temp, na.rm = TRUE),
q1_temp = quantile(summer_temp, 0.25, na.rm = TRUE),
q3_temp = quantile(summer_temp, 0.75, na.rm = TRUE),
max_temp = max(summer_temp, na.rm = TRUE),
skewness = e1071::skewness(summer_temp, na.rm = TRUE),
kurtosis = e1071::kurtosis(summer_temp, na.rm = TRUE)
)
climate_stats |>
pivot_longer(
cols = everything(),
names_to = "Statistic",
values_to = "Value"
) |>
gt() |>
tab_header(title = "U.S. Summer Temperatures, 1991–2020") |>
fmt_number(columns = Value, decimals = 2)| U.S. Summer Temperatures, 1991–2020 | |
| Statistic | Value |
|---|---|
| n | 469,758.00 |
| num_na | 0.00 |
| mean_temp | 71.86 |
| median_temp | 71.55 |
| sd_temp | 7.64 |
| mean_dev | 6.26 |
| min_temp | 29.91 |
| q1_temp | 66.53 |
| q3_temp | 77.87 |
| max_temp | 98.04 |
| skewness | −0.09 |
| kurtosis | −0.25 |
The average summer temperature is about 71.9°F, and the median is about 71.6°F. Their similarity and the skewness near zero suggest an almost symmetric distribution. A standard deviation of about 7.6°F shows meaningful variation across the country. Temperatures range from roughly 29.9°F to 98.0°F, although those extremes represent a small share of grid locations. The slightly negative kurtosis indicates a distribution that is a little flatter than a normal distribution rather than one dominated by unusually heavy tails.
ggplot(climate_values, aes(x = summer_temp)) +
geom_histogram(
binwidth = 1,
fill = "lightgray",
color = "black",
alpha = 0.8
) +
labs(
title = "Distribution of Average Summer Temperatures, 1991–2020",
x = "Summer temperature (°F)",
y = "Number of grid points"
) +
theme_minimal()ggplot(climate_values, aes(x = summer_temp)) +
stat_ecdf(geom = "step", color = "#2C7FB8", linewidth = 0.8) +
labs(
title = "Cumulative Distribution of Average Summer Temperatures",
x = "Summer temperature (°F)",
y = "Cumulative proportion"
) +
theme_minimal()ggplot(climate_values, aes(y = summer_temp)) +
geom_boxplot(fill = "lightgray", width = 0.35) +
labs(
title = "Boxplot of Average Summer Temperatures",
x = NULL,
y = "Summer temperature (°F)"
) +
theme_minimal()ggplot(climate_values, aes(x = "", y = summer_temp)) +
geom_violin(fill = "lightgray", bw = 1.2) +
geom_boxplot(width = 0.1, outlier.alpha = 0.25) +
labs(
title = "Violin Plot of Average Summer Temperatures",
x = NULL,
y = "Summer temperature (°F)"
) +
theme_minimal() +
theme(
axis.text.x = element_blank(),
axis.ticks.x = element_blank()
)The histogram shows the overall shape of the distribution and where temperatures are most concentrated. The cumulative frequency plot makes it easy to find the proportion of locations below a particular temperature. The boxplot summarizes the median, quartiles, spread, and possible outliers. The violin plot combines the compact boxplot summary with a density shape, so it shows where observations are especially concentrated.
tm_shape(climate_normals) +
tm_dots(
fill = "summer_temp",
size = 0.015,
fill.scale = tm_scale_intervals(
values = "brewer.yl_or_rd",
style = "pretty",
n = 7
),
fill.legend = tm_legend(
title = "Summer temperature (°F)",
position = tm_pos_out("right", "center"),
frame = FALSE
)
) +
tm_title("Average U.S. Summer Temperature, 1991–2020", size = 1.5) +
tm_layout(frame = TRUE)Summer temperatures generally increase toward the South, with especially warm conditions across the southern and southwestern United States. Cooler values appear farther north and in high-elevation areas such as the Rocky Mountains. The map shows that temperature is spatially structured by latitude, elevation, and regional climate—patterns that cannot be seen in the national summary statistics alone.
I selected pct_pov, the percentage of residents living
below the poverty line. This is useful for seeing both the overall
distribution of poverty across North Carolina census tracts and the
places where high rates are concentrated.
poverty_stats <- acs_tract_nc |>
st_drop_geometry() |>
summarise(
n = n(),
num_na = sum(is.na(pct_pov)),
mean = mean(pct_pov, na.rm = TRUE),
median = median(pct_pov, na.rm = TRUE),
sd = sd(pct_pov, na.rm = TRUE),
mean_dev = mean(
abs(pct_pov - mean(pct_pov, na.rm = TRUE)),
na.rm = TRUE
),
minimum = min(pct_pov, na.rm = TRUE),
q1 = quantile(pct_pov, 0.25, na.rm = TRUE),
q3 = quantile(pct_pov, 0.75, na.rm = TRUE),
maximum = max(pct_pov, na.rm = TRUE),
skewness = e1071::skewness(pct_pov, na.rm = TRUE),
kurtosis = e1071::kurtosis(pct_pov, na.rm = TRUE)
)
poverty_stats |>
pivot_longer(
cols = everything(),
names_to = "Statistic",
values_to = "Value"
) |>
gt() |>
tab_header(title = "Census-Tract Poverty Rates in North Carolina") |>
fmt_number(columns = Value, decimals = 2)| Census-Tract Poverty Rates in North Carolina | |
| Statistic | Value |
|---|---|
| n | 2,672.00 |
| num_na | 0.00 |
| mean | 13.94 |
| median | 11.57 |
| sd | 10.48 |
| mean_dev | 7.97 |
| minimum | 0.00 |
| q1 | 6.34 |
| q3 | 19.11 |
| maximum | 100.00 |
| skewness | 1.49 |
| kurtosis | 3.73 |
ggplot(acs_tract_nc |> st_drop_geometry(), aes(x = pct_pov)) +
geom_histogram(
binwidth = 2,
fill = "#F16913",
color = "white",
boundary = 0
) +
geom_vline(
xintercept = poverty_stats$median,
color = "#54278F",
linewidth = 1,
linetype = "dashed"
) +
labs(
title = "Distribution of Census-Tract Poverty Rates",
subtitle = "Dashed line shows the median tract",
x = "Population below the poverty line (%)",
y = "Number of census tracts"
) +
theme_minimal()tm_shape(acs_tract_nc) +
tm_polygons(
fill = "pct_pov",
col = "white",
lwd = 0.25,
fill.scale = tm_scale_intervals(
values = "brewer.or_rd",
style = "quantile",
n = 5
),
fill.legend = tm_legend(
title = "Below poverty line (%)",
position = tm_pos_in("left", "bottom"),
frame = TRUE,
frame.r = 5,
bg.color = "white"
)
) +
tm_title("Poverty Rate by North Carolina Census Tract", size = 1.6) +
tm_layout(
frame = TRUE,
frame.r = 12,
text.fontfamily = "sans"
)The median census-tract poverty rate is 11.6%, while the mean is 13.9%. The histogram shows a right-skewed distribution: most tracts have relatively low or moderate poverty rates, but a smaller number of tracts have much higher rates. This means the statewide mean does not describe every tract equally well, so the median and full distribution are important.
Poverty rates are spatially clustered rather than evenly spread across North Carolina. Higher-rate tracts appear in several rural parts of eastern North Carolina and in particular neighborhoods within cities, while many suburban tracts fall into lower categories. The map also shows differences within the same metropolitan area that would disappear in a county- or statewide average. Because the map uses quantiles, the colors show relative differences among North Carolina tracts rather than fixed policy thresholds.