8  Exploring correlations

After examining the relationship between two categorical variables (using the chi-square test) and between a categorical and a continuous variable (using the t-test), we will now explore the relationship between two continuous variables using correlation and simple regression.

To explore this, we will examine data on economic voting. Economic voting is a theory suggesting that voters reward or punish the incumbent government based on the state of the economy, and we aim to test this theory to see if it holds.

Our research question is: Is there a relationship between the state of the economy and the performance of the incumbent party in the United States? To investigate this, I am using an example developed by François Briatte, the previous instructor of this course, which uses data from Larry Bartels on economic voting in the United States.

# If you haven't already, install the following packages with install.packages("package_name")

library(tidyverse) # Because we always need it
library(broom) # To work with regression results
library(stargazer) # To print regression results nicely


# Import dataset

us_elections <- read_csv("data/us_elections.csv")

us_elections
# A tibble: 17 × 10
    year incumbent_margin income_growth incumbent  candidate_winner
   <dbl>            <dbl>         <dbl> <chr>      <chr>           
 1  1948             4.48          4.67 Truman     Truman          
 2  1952           -10.8           2.42 Stevenson  Eisenhower      
 3  1956            15.4           0.54 Eisenhower Eisenhower      
 4  1960            -0.17         -0.15 Nixon      Kennedy         
 5  1964            22.6           3.35 Johnson    Johnson         
 6  1968            -0.7           1.36 Humphrey   Nixon           
 7  1972            23.2           2.49 Nixon      Nixon           
 8  1976            -2.06          1.26 Ford       Carter          
 9  1980            -9.74         -0.96 Carter     Reagan          
10  1984            18.2           2.85 Reagan     Reagan          
11  1988             7.72          1.24 Bush Sr    Bush Sr         
12  1992            -5.56          0.57 Bush Sr    Clinton B       
13  1996             8.51          1.32 Clinton B  Clinton B       
14  2000             0.51          1.57 Gore       Bush Jr         
15  2004             2.46          1.19 Bush Jr    Bush Jr         
16  2008            -7.27         -0.79 McCain     Obama           
17  2012             3.84          0.32 Obama      Obama           
# ℹ 5 more variables: candidate_loser <chr>, party_incumbent <chr>,
#   winner_incumbent <lgl>, party_winning <chr>, tenure <dbl>

In this dataset, we have only 17 observations on each election in the US from 1948 to 2012. To operationalize these two concepts, state of the economy and performance of the incumbent party, we rely on two variables available in this dataset : the incumbent party’s margin of victory and the growth of disposable income per capita in the two last quarters before the election. The incumbent party’s margin of victory is the difference between the incumbent party’s vote share and the vote share of the main opposition party. Real disposable income here is a measure of people’s purchasing power after accounting for taxes and inflation.

A first way to look at the relationship between those two variables is to compute the correlation between them. To look at correlations we use the cor() function that takes two continuous variables as argument. Note that in the case we have missing values in our data, the cor function will not work. In that case, you can add use = "complete.obs" as argument in the cor()function.

cor(us_elections$incumbent_margin, us_elections$income_growth)
[1] 0.5072114

The result is a correlation coefficient of 0.5. A correlation coefficient measures the strength and direction of a linear relationship between two variables. The correlation coefficient ranges between -1 and 1, where -1 indicates a perfect negative linear relationship, 1 indicates a perfect positive linear relationship, and 0 indicates no linear relationship. Correlations coefficients give you a indicator of the intensity of the relationships between two continuous variables. Here we seen that the coefficient is ~ 0.5, which indicates a moderate positive relationship between the two variables. As the state of the economy grows, the incumbent party’s margin of victory increases.

We can also test whether this correlation is statistically significant. To do so, we can use the cor.test() function, working in the same way as the t.test() function. Here, we are testing the null hypothesis that the correlation between the two variables is equal to 0 and the p-value is the probability of observing a correlation at least as strong if the null hypothesis were true. The p-value here is 0.038, which is below the conventional threshold of 0.05, so we can reject the null hypothesis and conclude that the correlation is statistically significant.

cor.test(us_elections$incumbent_margin, us_elections$income_growth)

    Pearson's product-moment correlation

data:  us_elections$incumbent_margin and us_elections$income_growth
t = 2.2794, df = 15, p-value = 0.0377
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
 0.03513119 0.79423161
sample estimates:
      cor 
0.5072114 

But still here, we are not able to say how much the incumbent party’s margin of victory increases when the state of the economy grows. This is where correlation often falls short and linear regression comes in. Linear regression is a statistical method that models the relationship between a dependent variable and one or more independent variables. In our case, we want to model the relationship between the incumbent party’s margin of victory and the growth of disposable income per capita. Linear regression is considered a workhorse in quantitative social sciences. It’s safe to say that publishing quantitative results can be challenging without using this or a more sophisticated model. Whether you’re a quantitative researcher aiming to employ it or a qualitative researcher seeking to comprehend a quantitative article relevant to your work, understanding how regression operates is crucial.

When dealing with a single independent variable, we refer to it as simple linear regression, which is our primary focus today. In the next session, we will delve into multiple linear regression, involving more than one independent variable.

In linear regression, our objective is to establish a linear relationship between one or more independent variables and a continuous dependent variable. However, when working with a dependent variable that is not continuous, other models become relevant, and these will be explored in the upcoming semester. One such model is logistic regression, which is employed when the dependent variable is binary (e.g., whether individuals have voted for Trump or not).

Our regression equation for each incumbent party is a given year \({i}\) is written below.

\[ margin_{i} = \beta_{0} + \beta_{1}income_{i} + \epsilon_{i} \] The goal of the regression is to estimate the parameters of our model, which are \(\beta_{0}\) and \(\beta_{1}\). The error term \(\epsilon_{i}\) is not a parameter: once the model is estimated, the residuals give us an estimate of it.

To estimate the parameters of our model, we use the method of ordinary least squares (OLS). It allows to fit a line through the data points in a way that minimizes the sum of the squared prediction errors. In other words, it minimizes the distance between the line and the data points. This is why it is called “least squares”. This might still sound abstract to you so let’s look at the data !

elec_plot <- us_elections |>
  ggplot(aes(income_growth, incumbent_margin)) +
  # Create a scatter plot
  geom_point() +
  # Add a line at 0 on the y-axis
  geom_hline(yintercept = 0, lty = "dashed") +
  # Add president names
  ggrepel::geom_text_repel(aes(label = str_c(incumbent, "-", year))) +
  # Change axix labels
  labs(y = "Incumbent party popular vote margin (%)",
       x = "Q14/Q15 growth in real disposable income per capita (%)") +
  theme_minimal()

elec_plot 

The goal of linear regression is to find the best-fitting straight line that represents the relationship between two variables based on the given data points. The “best fit” line is determined by minimizing the distance between the line and the actual data points. This distance is measured using residuals, which are the vertical distances from each data point to the line. The line that results in the smallest sum of squared residuals provides the best approximation of the relationship between the variables. For example, if we examine the plot below, we can observe that the red and blue lines do not fit the data points well, whereas the black line represents a better fit.

elec_plot  +
  # One first "wrong" regression line
  geom_abline(
    intercept = -6,
    slope = 10,
    color = "blue",
    linetype = "dashed",
    size = 1,
    alpha = 0.4
  ) +
  # A second "wrong" regression line
  geom_abline(
    intercept = 0,
    slope = 1.5,
    color = "red",
    linetype = "dashed",
    size = 1,
    alpha = 0.4
  ) +
  # The true regression line
  geom_smooth(
    method = "lm",
    linetype = "dashed",
    color = "black",
    se = FALSE,
    size = 1,
    alpha = 0.4
  )

Linear regression not only provides us with the best-fitting line but also offers valuable information about the uncertainty associated with the estimated model parameters. This uncertainty can be represented by a confidence interval around the regression line. Below, you can see this with the grey area around our regression line: for each value of income growth, it shows the range of plausible values for the average predicted margin. The wider the grey area, the more uncertain our predictions are.

elec_plot +
  geom_smooth(method = "lm", linetype = "dashed", color = "black", se = TRUE, size = 1, alpha = 0.4)

8.1 Running a regression in R

Now we have a better idea of what a regression is, let’s run our first model. In R it is really easy to do ! You need to use the stats::lm() function. You need to insert a formula with on the left, the variable you want to explain and the right, the independent variables and in between, a ~. Before running the model, we have to make sure to know whether we have missing values or not in our data because the model will just drop them out and it is better to be aware of this.

model <- lm(incumbent_margin ~ income_growth, data = us_elections)

Once we have run the model, we have to interpret the results. In R, you have different ways to look at and to manipulate the results of a regression model. To have a quick look at the results, you can use the summary() function. It provides you with the main results of the model.

summary(model)

Call:
lm(formula = incumbent_margin ~ income_growth, data = us_elections)

Residuals:
    Min      1Q  Median      3Q     Max 
-18.860  -5.343  -1.036   4.537  14.883 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)  
(Intercept)    -0.8726     3.1754  -0.275   0.7872  
income_growth   3.6707     1.6104   2.279   0.0377 *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 9.431 on 15 degrees of freedom
Multiple R-squared:  0.2573,    Adjusted R-squared:  0.2077 
F-statistic: 5.196 on 1 and 15 DF,  p-value: 0.0377
  • Our estimate for the intercept - 0.873. This means that if the Q14/Q15 growth in real disposable income per capita is 0, the predicted incumbent party popular vote margin is -0.873. However, this estimate is very uncertain (standard error of 3.18, p-value of 0.79): it is not statistically different from 0, so we cannot conclude that the incumbent party loses the election on average when there is no economic growth.

  • Our estimate for for our first coefficient is 3.67. This is the slope of the regression line. That means that for each percentage point increase in growth in real disposable income per capita, the incumbent party popular vote margin increases by 3.67 percentage points.

  • Std. error is the standard error of the coefficient. It is a measure of the uncertainty of the estimate. The smaller the standard error, the more precise the estimate.

  • t value is the t statistic. It is computed by dividing the coefficient by its standard error (3.67/1.61 = 2.28). It measures how many standard errors the coefficient is away from 0. With large samples, a coefficient is statistically significant at the 5% level when the absolute t value is above 1.96. With only 15 degrees of freedom, as here, the threshold is higher: about 2.13 (qt(0.975, 15)).

  • p.value :2 * (1 - pt(abs(2.28), 15)), df being the degree of freddom which our number of observations - our number of coefficients (17-2)

  • Looking at the p-value, we see that the p-value of our main esimate is inferior to 0.05, showing that it is statistically significant at the 5% level.

  • When we run a regression, we also care about how good is our model to explain the variation of our outcome variable. The \(R^{2}\) is the share of the variance of our dependent variable that our model is able to explain. It goes from 0 to 1. If the \(R^{2}\) = 1, our model entirely explains Y (that will never happens). In social sciences we are happy with \(R^{2}\) of 0.2…

8.2 Extracting regression results

While just looking at the summary of the model is a good way to get a first idea of the results, you can also extract the results in a more detailed way with the broom package that has super handy functions to get the results of the regression model in tidy format. The tidy() function allows you to extract the coefficient of the models and to get their confidence intervals and p values as a dataframe. This can be used to create coefficient plots, which is a good way to visualize the results of the regression model, especially when you have multiple variables. If the confidence interval bar crosses the dashed 0 line, it indicates that our coefficient will not be statistically significant. Remember that the sizes of coefficients are not directly comparable because they are not calculated on the same scale. The magnitude of each coefficient depends on the units of the corresponding independent variable. Therefore, comparing coefficients without considering their scales could lead to misleading interpretations.

summary(model)

Call:
lm(formula = incumbent_margin ~ income_growth, data = us_elections)

Residuals:
    Min      1Q  Median      3Q     Max 
-18.860  -5.343  -1.036   4.537  14.883 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)  
(Intercept)    -0.8726     3.1754  -0.275   0.7872  
income_growth   3.6707     1.6104   2.279   0.0377 *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 9.431 on 15 degrees of freedom
Multiple R-squared:  0.2573,    Adjusted R-squared:  0.2077 
F-statistic: 5.196 on 1 and 15 DF,  p-value: 0.0377
model_coeffs <- tidy(model, conf.int = TRUE)
model_coeffs
# A tibble: 2 × 7
  term          estimate std.error statistic p.value conf.low conf.high
  <chr>            <dbl>     <dbl>     <dbl>   <dbl>    <dbl>     <dbl>
1 (Intercept)     -0.873      3.18    -0.275  0.787    -7.64       5.90
2 income_growth    3.67       1.61     2.28   0.0377    0.238      7.10
model_coeffs |>
  ggplot(aes(x = term, y = estimate)) +
  # Create dot for our estimates with a bar for confidence intervals
  geom_pointrange(aes(ymin = conf.low, ymax = conf.high))  +
  # Create a dashed line at 0
  geom_hline(yintercept = 0, linetype = "dashed") +
  # Add labels coefficient number as label
  geom_label(aes(label = round(estimate, 2)), vjust = -1) +
  coord_flip() + # invert y/x axes
  theme_minimal() 

To extract model fit statistics, you can use the glance() function. This function provides you with the \(R^{2}\), the adjusted \(R^{2}\).

glance(model)
# A tibble: 1 × 12
  r.squared adj.r.squared sigma statistic p.value    df logLik   AIC   BIC
      <dbl>         <dbl> <dbl>     <dbl>   <dbl> <dbl>  <dbl> <dbl> <dbl>
1     0.257         0.208  9.43      5.20  0.0377     1  -61.2  128.  131.
# ℹ 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>

One last useful function from the broom package is augment(). This will give you the predictions of our model (our fitted values) and our residuals (the errors). This is useful to calculate different diagnostics that we will see in a further session.

augment(model)
# A tibble: 17 × 8
   incumbent_margin income_growth .fitted .resid   .hat .sigma  .cooksd
              <dbl>         <dbl>   <dbl>  <dbl>  <dbl>  <dbl>    <dbl>
 1             4.48          4.67  16.3   -11.8  0.377    8.91 0.758   
 2           -10.8           2.42   8.01  -18.9  0.0911   8.21 0.221   
 3            15.4           0.54   1.11   14.3  0.0788   8.91 0.107   
 4            -0.17         -0.15  -1.42    1.25 0.126    9.76 0.00146 
 5            22.6           3.35  11.4    11.2  0.173    9.19 0.178   
 6            -0.7           1.36   4.12   -4.82 0.0588   9.67 0.00867 
 7            23.2           2.49   8.27   14.9  0.0956   8.82 0.145   
 8            -2.06          1.26   3.75   -5.81 0.0592   9.63 0.0127  
 9            -9.74         -0.96  -4.40   -5.34 0.217    9.63 0.0567  
10            18.2           2.85   9.59    8.62 0.123    9.45 0.0667  
11             7.72          1.24   3.68    4.04 0.0593   9.70 0.00615 
12            -5.56          0.57   1.22   -6.78 0.0774   9.58 0.0235  
13             8.51          1.32   3.97    4.54 0.0589   9.68 0.00769 
14             0.51          1.57   4.89   -4.38 0.0600   9.69 0.00733 
15             2.46          1.19   3.50   -1.04 0.0597   9.76 0.000407
16            -7.27         -0.79  -3.77   -3.50 0.195    9.71 0.0206  
17             3.84          0.32   0.302   3.54 0.0908   9.71 0.00773 
# ℹ 1 more variable: .std.resid <dbl>

augment() is also useful to combine your regression results with your initial dataframe and do some graphs of your results.

augment(model, us_elections) |>
  ggplot(aes(x = income_growth, y = incumbent_margin, label = incumbent)) +
  geom_point(colour = "black", alpha = .25) +
  geom_smooth(method = "lm",
              color = "black",
              linetype = "dashed")  +
  geom_segment(aes(xend = income_growth, yend = .fitted),
               alpha = 0.5,
               color = "blue") +
  ggrepel::geom_text_repel() +
  theme_minimal()

The blue segments on the plot are the so-called residuals. These are the vertical distance between the ‘true’ value of our data to their fitted values on the regression line. The model is fitted by minimizing the sum of the squares of these distances. With this visualization, we can also say something about how our model is doing to predict certain points. For instance, our model is doing a bad job at predicting the results of the elections won by Eisenhower in 1952 and 1956.

9 Regression with categorical independent variables

We have seen that we can use continuous variables in our regression. But we can also use categorical variables. For instance, we can use the party_incumbent variable to see whether the margin of the incumbent party differs depending on whether it is the Democratic or the Republican Party. Categorical variables have to be interepreted differently than continuous variables. The coefficient of a categorical variable is the difference between the mean of the outcome variable for the group of interest and the mean of the outcome variable for the reference group. Here, the reference group is Democrat. The coefficient of the party variable is the difference between the mean of the outcome variable for Republican and the mean of the outcome variable for Democrat. Here, our coefficient is 3.436, meaning that when the incumbent party is the Republican Party, its margin is on average 3.436 percentage points higher than when it is the Democratic Party. However, this difference is not statistically significant (p-value of 0.52). Consequently, we cannot conclude that Republican incumbents have higher margins than Democratic ones.

us_elections
# A tibble: 17 × 10
    year incumbent_margin income_growth incumbent  candidate_winner
   <dbl>            <dbl>         <dbl> <chr>      <chr>           
 1  1948             4.48          4.67 Truman     Truman          
 2  1952           -10.8           2.42 Stevenson  Eisenhower      
 3  1956            15.4           0.54 Eisenhower Eisenhower      
 4  1960            -0.17         -0.15 Nixon      Kennedy         
 5  1964            22.6           3.35 Johnson    Johnson         
 6  1968            -0.7           1.36 Humphrey   Nixon           
 7  1972            23.2           2.49 Nixon      Nixon           
 8  1976            -2.06          1.26 Ford       Carter          
 9  1980            -9.74         -0.96 Carter     Reagan          
10  1984            18.2           2.85 Reagan     Reagan          
11  1988             7.72          1.24 Bush Sr    Bush Sr         
12  1992            -5.56          0.57 Bush Sr    Clinton B       
13  1996             8.51          1.32 Clinton B  Clinton B       
14  2000             0.51          1.57 Gore       Bush Jr         
15  2004             2.46          1.19 Bush Jr    Bush Jr         
16  2008            -7.27         -0.79 McCain     Obama           
17  2012             3.84          0.32 Obama      Obama           
# ℹ 5 more variables: candidate_loser <chr>, party_incumbent <chr>,
#   winner_incumbent <lgl>, party_winning <chr>, tenure <dbl>
model2 <- lm(incumbent_margin ~ party_incumbent, us_elections)
summary(model2)

Call:
lm(formula = incumbent_margin ~ party_incumbent, data = us_elections)

Residuals:
    Min      1Q  Median      3Q     Max 
-13.179  -7.824  -1.819   6.181  20.251 

Coefficients:
                          Estimate Std. Error t value Pr(>|t|)
(Intercept)                  2.329      3.815   0.610    0.551
party_incumbentRepublican    3.436      5.243   0.655    0.522

Residual standard error: 10.79 on 15 degrees of freedom
Multiple R-squared:  0.02783,   Adjusted R-squared:  -0.03698 
F-statistic: 0.4294 on 1 and 15 DF,  p-value: 0.5222

So far, we have tested the effect of two variables in two different models. However, we can also test the effect of two variables in a multiple linear regression model that allows us to control for the effect of other variables.

model3 <- lm(incumbent_margin ~ income_growth + tenure, us_elections)

summary(model3)

Call:
lm(formula = incumbent_margin ~ income_growth + tenure, data = us_elections)

Residuals:
    Min      1Q  Median      3Q     Max 
-7.3524 -3.9170 -0.2879  2.5490  9.5650 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept)     9.9291     2.4632   4.031  0.00124 ** 
income_growth   5.4818     0.9193   5.963 3.47e-05 ***
tenure         -1.7636     0.2885  -6.113 2.68e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 5.097 on 14 degrees of freedom
Multiple R-squared:  0.7976,    Adjusted R-squared:  0.7686 
F-statistic: 27.58 on 2 and 14 DF,  p-value: 1.393e-05

Lastly, we can also look at our results in a regression table with the stargazer package.

stargazer::stargazer(
  model,
  model2,
  model3, 
  type = "text",
  title = "Economic voting : regression table",
  digits = 2,
  omit.stat = "f",
  dep.var.labels = "Margin of incumbent",
  covariate.labels = c("Disposable Income"),
  # Put significance stars at 0.05, 0.01 and 0.001
  star.cutoffs = c(0.05, 0.01, 0.001)
)

Economic voting : regression table
=======================================================================
                                       Dependent variable:             
                          ---------------------------------------------
                                       Margin of incumbent             
                               (1)             (2)            (3)      
-----------------------------------------------------------------------
Disposable Income             3.67*                         5.48***    
                              (1.61)                         (0.92)    
                                                                       
party_incumbentRepublican                     3.44                     
                                             (5.24)                    
                                                                       
tenure                                                      -1.76***   
                                                             (0.29)    
                                                                       
Constant                      -0.87           2.33           9.93**    
                              (3.18)         (3.81)          (2.46)    
                                                                       
-----------------------------------------------------------------------
Observations                    17             17              17      
R2                             0.26           0.03            0.80     
Adjusted R2                    0.21           -0.04           0.77     
Residual Std. Error       9.43 (df = 15) 10.79 (df = 15) 5.10 (df = 14)
=======================================================================
Note:                                     *p<0.05; **p<0.01; ***p<0.001

10 Your turn !

So far, we have looked at the effect of the state of the economy and incumbent performance at the aggregate level with looking at different election results. But we could also look at this relationship at the individual level. For this, we can use the European Social Survey data that contains data on different socio-political attitudes of European citizens. Here, import the ESS9e03_1.dta file in the data folder and look at the relationship between satisfaction towards national government measured by the stfgov variable and satisfaction towards the economy in the country (socio-tropic evaluation) measured by the stfeco variable. You can also try with the hincfel variable as a categorical variable measuring how people feel about their household income (ego-tropic evaluation).

library(haven)

ess <- read_dta("data/ESS9e03_1.dta")

ess <- ess |> 
  select(stfeco, stfgov, hincfel)

m1 <- lm(stfgov ~ stfeco, data = ess)

summary(m1)

Call:
lm(formula = stfgov ~ stfeco, data = ess)

Residuals:
    Min      1Q  Median      3Q     Max 
-7.4693 -1.1379  0.0633  1.3295  8.8621 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) 1.137915   0.021304   53.41   <2e-16 ***
stfeco      0.633134   0.003666  172.68   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 1.964 on 47015 degrees of freedom
  (2502 observations deleted due to missingness)
Multiple R-squared:  0.3881,    Adjusted R-squared:  0.3881 
F-statistic: 2.982e+04 on 1 and 47015 DF,  p-value: < 2.2e-16
ess |> count(hincfel)
# A tibble: 5 × 2
  hincfel                                          n
  <dbl+lbl>                                    <int>
1     1 [Living comfortably on present income] 15691
2     2 [Coping on present income]             22228
3     3 [Difficult on present income]           7972
4     4 [Very difficult on present income]      2930
5 NA(b) [Don't know]                             698
m2 <- lm(stfgov ~ hincfel, data = ess)
summary(m2)

Call:
lm(formula = stfgov ~ hincfel, data = ess)

Residuals:
    Min      1Q  Median      3Q     Max 
-5.0093 -1.8639  0.1361  1.9907  6.7088 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  5.58195    0.02856  195.48   <2e-16 ***
hincfel     -0.57268    0.01341  -42.71   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 2.466 on 47181 degrees of freedom
  (2336 observations deleted due to missingness)
Multiple R-squared:  0.03723,   Adjusted R-squared:  0.03721 
F-statistic:  1824 on 1 and 47181 DF,  p-value: < 2.2e-16
ess <- ess |> 
  mutate(
    hincfel_cat = case_when(
      hincfel == 1 ~ "Comfortably",
      hincfel == 2 ~ "Coping",
      hincfel == 3 ~ "Difficult",
      hincfel == 4 ~ "Very difficult"
    )
  )

m3 <- lm(stfgov ~ hincfel_cat, data = ess)

summary(m3)

Call:
lm(formula = stfgov ~ hincfel_cat, data = ess)

Residuals:
   Min     1Q Median     3Q    Max 
-5.021 -1.877  0.123  1.980  6.696 

Coefficients:
                          Estimate Std. Error t value Pr(>|t|)    
(Intercept)                5.02047    0.01994  251.77   <2e-16 ***
hincfel_catCoping         -0.59814    0.02608  -22.93   <2e-16 ***
hincfel_catDifficult      -1.14349    0.03455  -33.10   <2e-16 ***
hincfel_catVery difficult -1.71625    0.05109  -33.59   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 2.466 on 47179 degrees of freedom
  (2336 observations deleted due to missingness)
Multiple R-squared:  0.03726,   Adjusted R-squared:  0.0372 
F-statistic: 608.6 on 3 and 47179 DF,  p-value: < 2.2e-16