library(tidyverse)
library(mice)
library(ggcorrplot)

The Data

We’re using midwest — a dataset built into ggplot2 with demographic and economic data for counties across five midwestern US states.

data(midwest)
head(midwest)
## # A tibble: 6 × 28
##     PID county   state  area poptotal popdensity popwhite popblack popamerindian
##   <int> <chr>    <chr> <dbl>    <int>      <dbl>    <int>    <int>         <int>
## 1   561 ADAMS    IL    0.052    66090      1271.    63917     1702            98
## 2   562 ALEXAND… IL    0.014    10626       759      7054     3496            19
## 3   563 BOND     IL    0.022    14991       681.    14477      429            35
## 4   564 BOONE    IL    0.017    30806      1812.    29344      127            46
## 5   565 BROWN    IL    0.018     5836       324.     5264      547            14
## 6   566 BUREAU   IL    0.05     35688       714.    35157       50            65
## # ℹ 19 more variables: popasian <int>, popother <int>, percwhite <dbl>,
## #   percblack <dbl>, percamerindan <dbl>, percasian <dbl>, percother <dbl>,
## #   popadults <int>, perchsd <dbl>, percollege <dbl>, percprof <dbl>,
## #   poppovertyknown <int>, percpovertyknown <dbl>, percbelowpoverty <dbl>,
## #   percchildbelowpovert <dbl>, percadultpoverty <dbl>,
## #   percelderlypoverty <dbl>, inmetro <int>, category <chr>

Your outcome variable for modeling is percbelowpoverty (percentage of the county population below the poverty line).


Part 1: Assess Data Quality

# Summary Statistics
glimpse(midwest)
## Rows: 437
## Columns: 28
## $ PID                  <int> 561, 562, 563, 564, 565, 566, 567, 568, 569, 570,…
## $ county               <chr> "ADAMS", "ALEXANDER", "BOND", "BOONE", "BROWN", "…
## $ state                <chr> "IL", "IL", "IL", "IL", "IL", "IL", "IL", "IL", "…
## $ area                 <dbl> 0.052, 0.014, 0.022, 0.017, 0.018, 0.050, 0.017, …
## $ poptotal             <int> 66090, 10626, 14991, 30806, 5836, 35688, 5322, 16…
## $ popdensity           <dbl> 1270.9615, 759.0000, 681.4091, 1812.1176, 324.222…
## $ popwhite             <int> 63917, 7054, 14477, 29344, 5264, 35157, 5298, 165…
## $ popblack             <int> 1702, 3496, 429, 127, 547, 50, 1, 111, 16, 16559,…
## $ popamerindian        <int> 98, 19, 35, 46, 14, 65, 8, 30, 8, 331, 51, 26, 17…
## $ popasian             <int> 249, 48, 16, 150, 5, 195, 15, 61, 23, 8033, 89, 3…
## $ popother             <int> 124, 9, 34, 1139, 6, 221, 0, 84, 6, 1596, 20, 7, …
## $ percwhite            <dbl> 96.71206, 66.38434, 96.57128, 95.25417, 90.19877,…
## $ percblack            <dbl> 2.57527614, 32.90043290, 2.86171703, 0.41225735, …
## $ percamerindan        <dbl> 0.14828264, 0.17880670, 0.23347342, 0.14932156, 0…
## $ percasian            <dbl> 0.37675897, 0.45172219, 0.10673071, 0.48691813, 0…
## $ percother            <dbl> 0.18762294, 0.08469791, 0.22680275, 3.69733169, 0…
## $ popadults            <int> 43298, 6724, 9669, 19272, 3979, 23444, 3583, 1132…
## $ perchsd              <dbl> 75.10740, 59.72635, 69.33499, 75.47219, 68.86152,…
## $ percollege           <dbl> 19.63139, 11.24331, 17.03382, 17.27895, 14.47600,…
## $ percprof             <dbl> 4.355859, 2.870315, 4.488572, 4.197800, 3.367680,…
## $ poppovertyknown      <int> 63628, 10529, 14235, 30337, 4815, 35107, 5241, 16…
## $ percpovertyknown     <dbl> 96.27478, 99.08714, 94.95697, 98.47757, 82.50514,…
## $ percbelowpoverty     <dbl> 13.151443, 32.244278, 12.068844, 7.209019, 13.520…
## $ percchildbelowpovert <dbl> 18.011717, 45.826514, 14.036061, 11.179536, 13.02…
## $ percadultpoverty     <dbl> 11.009776, 27.385647, 10.852090, 5.536013, 11.143…
## $ percelderlypoverty   <dbl> 12.443812, 25.228976, 12.697410, 6.217047, 19.200…
## $ inmetro              <int> 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0…
## $ category             <chr> "AAR", "LHR", "AAR", "ALU", "AAR", "AAR", "LAR", …
summary(midwest)
##       PID             county          state          area        
##  Min.   : 561   Length   :437   Length   :437   Min.   :0.00500  
##  1st Qu.: 670   N.unique :320   N.unique :  5   1st Qu.:0.02400  
##  Median :1221   N.blank  :  0   N.blank  :  0   Median :0.03000  
##  Mean   :1437   Min.nchar:  3   Min.nchar:  2   Mean   :0.03317  
##  3rd Qu.:2059   Max.nchar: 14   Max.nchar:  2   3rd Qu.:0.03800  
##  Max.   :3052                                   Max.   :0.11000  
##     poptotal         popdensity          popwhite          popblack      
##  Min.   :   1701   Min.   :   85.05   Min.   :    416   Min.   :      0  
##  1st Qu.:  18840   1st Qu.:  622.41   1st Qu.:  18630   1st Qu.:     29  
##  Median :  35324   Median : 1156.21   Median :  34471   Median :    201  
##  Mean   :  96130   Mean   : 3097.74   Mean   :  81840   Mean   :  11024  
##  3rd Qu.:  75651   3rd Qu.: 2330.00   3rd Qu.:  72968   3rd Qu.:   1291  
##  Max.   :5105067   Max.   :88018.40   Max.   :3204947   Max.   :1317147  
##  popamerindian        popasian         popother        percwhite    
##  Min.   :    4.0   Min.   :     0   Min.   :     0   Min.   :10.69  
##  1st Qu.:   44.0   1st Qu.:    35   1st Qu.:    20   1st Qu.:94.89  
##  Median :   94.0   Median :   102   Median :    66   Median :98.03  
##  Mean   :  343.1   Mean   :  1310   Mean   :  1613   Mean   :95.56  
##  3rd Qu.:  288.0   3rd Qu.:   401   3rd Qu.:   345   3rd Qu.:99.07  
##  Max.   :10289.0   Max.   :188565   Max.   :384119   Max.   :99.82  
##    percblack       percamerindan        percasian        percother      
##  Min.   : 0.0000   Min.   : 0.05623   Min.   :0.0000   Min.   :0.00000  
##  1st Qu.: 0.1157   1st Qu.: 0.15793   1st Qu.:0.1737   1st Qu.:0.09102  
##  Median : 0.5389   Median : 0.21502   Median :0.2972   Median :0.17844  
##  Mean   : 2.6763   Mean   : 0.79894   Mean   :0.4872   Mean   :0.47906  
##  3rd Qu.: 2.6014   3rd Qu.: 0.38362   3rd Qu.:0.5212   3rd Qu.:0.48050  
##  Max.   :40.2100   Max.   :89.17738   Max.   :5.0705   Max.   :7.52427  
##    popadults          perchsd        percollege        percprof      
##  Min.   :   1287   Min.   :46.91   Min.   : 7.336   Min.   : 0.5203  
##  1st Qu.:  12271   1st Qu.:71.33   1st Qu.:14.114   1st Qu.: 2.9980  
##  Median :  22188   Median :74.25   Median :16.798   Median : 3.8142  
##  Mean   :  60973   Mean   :73.97   Mean   :18.273   Mean   : 4.4473  
##  3rd Qu.:  47541   3rd Qu.:77.20   3rd Qu.:20.550   3rd Qu.: 4.9493  
##  Max.   :3291995   Max.   :88.90   Max.   :48.079   Max.   :20.7913  
##  poppovertyknown   percpovertyknown percbelowpoverty percchildbelowpovert
##  Min.   :   1696   Min.   :80.90    Min.   : 2.180   Min.   : 1.919      
##  1st Qu.:  18364   1st Qu.:96.89    1st Qu.: 9.199   1st Qu.:11.624      
##  Median :  33788   Median :98.17    Median :11.822   Median :15.270      
##  Mean   :  93642   Mean   :97.11    Mean   :12.511   Mean   :16.447      
##  3rd Qu.:  72840   3rd Qu.:98.60    3rd Qu.:15.133   3rd Qu.:20.352      
##  Max.   :5023523   Max.   :99.86    Max.   :48.691   Max.   :64.308      
##  percadultpoverty percelderlypoverty    inmetro            category  
##  Min.   : 1.939   Min.   : 3.547     Min.   :0.0000   Length   :437  
##  1st Qu.: 7.668   1st Qu.: 8.912     1st Qu.:0.0000   N.unique : 16  
##  Median :10.008   Median :10.869     Median :0.0000   N.blank  :  0  
##  Mean   :10.919   Mean   :11.389     Mean   :0.3432   Min.nchar:  3  
##  3rd Qu.:13.182   3rd Qu.:13.412     3rd Qu.:1.0000   Max.nchar:  3  
##  Max.   :43.312   Max.   :31.162     Max.   :1.0000
sum(is.na(midwest))
## [1] 0

ANSWER: This is a clean dataset: 437 counties, 28 variables, and zero missing values. It also carries the “good dataset” traits from class, since each row is one county and each column is one measured trait. Two things stand out in the summary. The percent columns are mostly redundant with their population-count columns (percwhite vs popwhite), and the racial percentages are wildly skewed. percwhite sits near 90% for most counties while percamerindan and percasian rarely clear a couple percent, so a handful of counties drag those ranges way out.

# Visualize missing data structure with md.pattern()
md.pattern(midwest, rotate.names = TRUE)
##  /\     /\
## {  `---'  }
## {  O   O  }
## ==>  V <==  No need for mice. This data set is completely observed.
##  \  \|/  /
##   `-----'

##     PID county state area poptotal popdensity popwhite popblack popamerindian
## 437   1      1     1    1        1          1        1        1             1
##       0      0     0    0        0          0        0        0             0
##     popasian popother percwhite percblack percamerindan percasian percother
## 437        1        1         1         1             1         1         1
##            0        0         0         0             0         0         0
##     popadults perchsd percollege percprof poppovertyknown percpovertyknown
## 437         1       1          1        1               1                1
##             0       0          0        0               0                0
##     percbelowpoverty percchildbelowpovert percadultpoverty percelderlypoverty
## 437                1                    1                1                  1
##                    0                    0                0                  0
##     inmetro category  
## 437       1        1 0
##           0        0 0

ANSWER: There is nothing to fix. md.pattern() returns a single row of all 1s and a final row of all 0s, which is its way of saying every observation is complete. No imputation needed here.


Part 2: Explore — One Graph

Make one exploratory graph of this dataset. Your choice of variables, geom, and aesthetics — just make sure it tells you something genuine about the data.

ggplot(midwest, aes(x = percollege, y = percbelowpoverty, color = state)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm", se = FALSE, color = "black")

ANSWER: I wanted to know whether more educated counties are less poor. The cloud slopes down: counties with a higher share of college graduates tend to have lower poverty, though the spread is wide and plenty of well-educated counties still show middling poverty rates. Education looks related, but it is clearly not the whole story.


Part 3: Check Linearity

Your candidate numeric predictors are: percollege, percpovertyknown, percwhite, percblack, percamerindan, percasian, popdensity, and percadults. Make scatterplots of each against percbelowpoverty.

# Note: the help page (?midwest) confirms there is no `percadults` column.
# The closest match, `popadults`, is a raw head count rather than a rate, so I
# leave it out and check the eight predictors that actually exist as percentages.
predictors <- c("percollege", "percpovertyknown", "percwhite", "percblack",
                "percamerindan", "percasian", "popdensity")

midwest %>%
  select(percbelowpoverty, all_of(predictors)) %>%
  pivot_longer(-percbelowpoverty, names_to = "predictor", values_to = "value") %>%
  ggplot(aes(x = value, y = percbelowpoverty)) +
  geom_point(alpha = 0.4) +
  geom_smooth(method = "lm", se = FALSE, color = "red") +
  facet_wrap(~ predictor, scales = "free_x")

ANSWER: percollege, percwhite, and percpovertyknown show reasonably straight downward trends, and percamerindan and percblack show usable upward ones. popdensity is the worst offender: nearly every county piles up at low density with a few extreme cities, so its relationship is a nonlinear smear, not a line. percasian is similar and barely trends at all. I’ll drop popdensity and percasian from the model for weak, non-linear relationships.


Part 4: Check Multicollinearity

# Select your candidate numeric predictors and outcome
model_vars <- midwest %>% select(percbelowpoverty, all_of(predictors))
corr_matrix <- cor(model_vars, use = "complete.obs")
ggcorrplot(corr_matrix, lab = TRUE, type = "lower")

ANSWER: Two pairs are too cozy. percwhite and percblack correlate at about -0.76, and percollege and percasian at about 0.75. Keeping both members of either pair would make their coefficients unstable. I already cut percasian in Part 3, which handles the second pair. For the first, I keep percwhite (it has the stronger tie to poverty) and drop percblack.


Part 5: Fit the Multiple Regression Model

Based on your decisions in Parts 3 and 4, fit a multiple regression model predicting percbelowpoverty. Include at least one categorical predictor (state is available and has five levels).

midwest_model <- lm(percbelowpoverty ~ percollege + percwhite + percamerindan +
                      percpovertyknown + state, data = midwest)
summary(midwest_model)
## 
## Call:
## lm(formula = percbelowpoverty ~ percollege + percwhite + percamerindan + 
##     percpovertyknown + state, data = midwest)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -10.7833  -2.4632  -0.6051   2.0349  15.8659 
## 
## Coefficients:
##                  Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      88.07482    7.96604  11.056  < 2e-16 ***
## percollege       -0.36676    0.03470 -10.569  < 2e-16 ***
## percwhite        -0.24196    0.03677  -6.580 1.38e-10 ***
## percamerindan     0.14194    0.05715   2.484   0.0134 *  
## percpovertyknown -0.46794    0.07318  -6.394 4.23e-10 ***
## stateIN          -2.63045    0.58392  -4.505 8.58e-06 ***
## stateMI           1.44790    0.59885   2.418   0.0160 *  
## stateOH          -0.29790    0.58677  -0.508   0.6119    
## stateWI          -0.45083    0.64444  -0.700   0.4846    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 4.004 on 428 degrees of freedom
## Multiple R-squared:  0.4066, Adjusted R-squared:  0.3955 
## F-statistic: 36.66 on 8 and 428 DF,  p-value: < 2.2e-16

ANSWER:

  1. All 437 counties were used. Nothing was dropped, because the dataset has no missing values.

  2. Holding the others constant:

    • percollege (-0.37): each extra percentage point of college graduates is linked to a 0.37-point lower poverty rate.
    • percwhite (-0.24): a one-point rise in percent white is linked to a 0.24-point lower poverty rate.
    • percamerindan (+0.14): a one-point rise here is linked to a 0.14-point higher poverty rate, the only predictor pushing poverty up.
    • percpovertyknown (-0.47): counties that measured poverty status for a larger share of residents report lower poverty. This is more a data-coverage effect than a social cause.
    • state: the reference category is Illinois. Indiana counties average about 2.63 points lower poverty than comparable Illinois counties, while Michigan counties average about 1.45 points higher.
  3. R-squared is 0.41. These predictors together explain roughly 41% of the county-to-county variation in poverty rate. Respectable for social data, but most of the variation still lives outside this model.

  4. Significant at p < 0.05: percollege, percwhite, percpovertyknown, percamerindan, and the Indiana and Michigan state contrasts. The Ohio and Wisconsin contrasts are not significantly different from Illinois.


Part 6: Check Model Assumptions

plot(midwest_model, which = 1)

plot(midwest_model, which = 2)

ANSWER: The residual plot is mostly a flat, even band around zero, so linearity and constant variance hold up well through the middle. The trouble is in the tails: a few high-poverty counties (like the ones flagged by number) sit far above the line, and the Q-Q plot bows up sharply at the top right. That means the residuals are right-skewed with heavier-than-normal tails, driven by a small set of very high-poverty counties. The model is solid for typical counties but underpredicts the extremes.


Part 7: Make It Beautiful — One Polished Graph

Take one graph that’s meaningful for understanding poverty rates in this dataset and make it publication-ready using theme() and other ggplot customization tools. Your polished graph must include at minimum:

ggplot(midwest, aes(x = percollege, y = percbelowpoverty)) +
  geom_point(aes(color = percbelowpoverty), alpha = 0.75, size = 2) +
  geom_smooth(method = "lm", se = FALSE, color = "#1b3a5c", linewidth = 1.1) +
  scale_color_gradient(low = "#7fb3d5", high = "#922b21") +
  labs(
    title = "More College Graduates, Less County Poverty",
    subtitle = "437 counties across five midwestern states",
    x = "Adults with a college degree (%)",
    y = "Population below the poverty line (%)",
    color = "Poverty rate (%)"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", size = 16),
    plot.subtitle = element_text(color = "gray40"),
    panel.grid.minor = element_blank(),
    legend.position = "bottom",
    plot.background = element_rect(fill = "white", color = NA)
  )

ANSWER: The story is the same downward slope from Part 2, dressed for presentation. I mapped color to the poverty rate itself so the hardest-hit counties glow red and reinforce the y-axis, then dropped the minor gridlines and moved the legend to the bottom to keep the eye on the data instead of the frame. The plain-English title states the finding outright, since a reader should get the point before reading a single axis label.


Part 8: Written Reflection

Answer each of the following in complete sentences. Be specific!

8A. Classification and Machine Learning

In the Teachable Machine activity, you trained a classification model by providing examples. In your own words, explain what a classification model is trying to do (how is it different from the regression models you’ve been building?), and describe one real-world situation where you’d reach for classification instead of regression. What could go wrong if a classification model trained on a narrow set of examples is deployed in a much broader context?

ANSWER: A classification model sorts an input into one of a set of labeled buckets. It answers “which category,” not “how much.” That is the split from regression: my poverty model predicts a number on a continuous scale, while a classifier predicts a class like spam or not-spam. I’d reach for classification to flag whether an incoming email is phishing, since the useful output is a yes-or-no decision, not a quantity. The danger with narrow training data is that the model only knows the examples it saw. Teachable Machine trained on my face in my lighting will confidently mislabel a stranger in a dark room, because it never learned the range it’s now being asked to judge. Deployed broadly, that shows up as biased, overconfident predictions on any group the examples left out.


8B. The Python Demo — Decision Trees

A real data scientist showed you how to build a decision tree model in Python. Reflect on that demo: what was one thing about how decision trees work that surprised you or made you think differently about how models make predictions? How did seeing a working data scientist’s actual code and workflow compare to what you expected data science to look like?

ANSWER: What surprised me was how literal a decision tree is. It’s just a stack of yes/no questions, and you can trace any single prediction straight down the branches, which feels far more readable than the wall of coefficients in my regression output. Watching a real workflow was the bigger reality check. I pictured data science as writing one clean model that works. Instead the demo was mostly loading data, printing it, fixing a bug, re-running a cell, and checking the output again before any modeling happened. The actual skill looked less like math and more like patient tinkering.