Tutorial #3. Linear regressions and statistical tests

Introduction to programming for data analysis

Authors
Affiliation

Ulrich Aiounou

AMSE, Aix Marseille University

Ewen Gallic

AMSE, Aix Marseille University

Kla Kouadio

AMSE, Aix Marseille University

Pierre Michel

AMSE, Aix Marseille University

M1 Economics / Intro to Programming / tutorial-3

Disclaimer

This HTML page was generated with Claude Sonnet 5 based on exercises we developed from a LaTeX document.

If you would prefer to work with a more traditional PDF version, you can download it here: Download the PDF.

Part of the content was created by Pierre Michel (AMSE) and Morgan Raux (AMSE) who kindly shared their work.

This tutorial focuses on regression models and statistical hypothesis testing. After completing Tutorial #2, you should be able to create basic data visualizations and compute summary statistics. Building on these skills, you are now ready to run regression analyses and apply statistical modeling techniques.

In Tutorial #1, you downloaded datasets from Eurostat, merged them into a single dataset in R, and exported the result as a CSV file.

Objectives of the tutorial

In this tutorial, you will use the dataset from exercise 2 of Tutorial #1 to:

  1. create visualizations to examine the relationship between the two main variables (per capita GDP and overall life satisfaction),
  2. estimate regression models, including both linear and nonlinear specifications for panel data.

Did not finish tutorial 1?

Didn’t get to finish combining the data in Tutorial 1? Grab the ready-to-use file below and save it to the data/out/ folder of your project.

Q1. Import the dataset that you saved at the end of exercise 2 of Tutorial #1.

library(tidyverse)
library(stargazer)
tb <- ...

The file lives in your data/out/ folder — build the relative path from your project root.

library(readr)
# Import data path from your project repository structure
tb <- read_csv("data/out/gdp-lifesat.csv")
Rows: 231 Columns: 4
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (1): country
dbl (3): year, gdp, life_satisf

ℹ 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.
tb
# A tibble: 231 × 4
   country  year   gdp life_satisf
   <chr>   <dbl> <dbl>       <dbl>
 1 AL       2018  4730         5.5
 2 AL       2021  5190         5.7
 3 AL       2022  5500         6  
 4 AT       2013 43080         7.8
 5 AT       2018 45140         8  
 6 AT       2021 44580         8  
 7 AT       2022 46430         7.9
 8 AT       2023 45670         7.7
 9 AT       2024 45140         7.6
10 AT       2025 45390         7.7
# ℹ 221 more rows

Scatterplots

A scatterplot visually represents the relationship between two variables. In the current dataset, it is useful to examine how per capita GDP relates to overall life satisfaction.

GDP and life satisfaction

Q2. Create a scatterplot to display the relationship between per capita GDP and overall life satisfaction. Label the x-axis “Per capita GDP” and the y-axis “Overall Life Satisfaction”. Use the title “Per capita GDP vs. Overall Life Satisfaction” for the graph.

Map x = gdp and y = life_satisf inside aes(), then use geom_point().

ggplot(data = tb, mapping = aes(x = gdp, y = life_satisf)) +
    geom_point(alpha = 0.6) +
    labs(
        x = "Per capita GDP",
        y = "Overall Life Satisfaction",
        title = "Per capita GDP vs. Overall Life Satisfaction"
    ) +
    theme_minimal()
Warning: Removed 3 rows containing missing values or values outside the scale range
(`geom_point()`).

Q3. Add a linear regression line to the scatterplot.

geom_smooth() adds a fitted trend line; set method = "lm" for a straight linear fit, and se = TRUE to show the confidence band.

ggplot(data = tb, mapping = aes(x = gdp, y = life_satisf)) +
    geom_point(alpha = 0.6) +
    geom_smooth(method = "lm", color = "blue", se = TRUE) +
    labs(
        x = "Per capita GDP",
        y = "Overall Life Satisfaction",
        title = "Per capita GDP vs. Overall Life Satisfaction (Linear)"
    ) +
    theme_minimal()
`geom_smooth()` using formula = 'y ~ x'
Warning: Removed 3 rows containing non-finite outside the scale range
(`stat_smooth()`).
Warning: Removed 3 rows containing missing values or values outside the scale range
(`geom_point()`).

Q4. Add a quadratic curve to the scatterplot.

Still method = "lm", but change the formula argument to include a squared term: y ~ x + I(x^2).

ggplot(data = tb, mapping = aes(x = gdp, y = life_satisf)) +
    geom_point(alpha = 0.6) +
    geom_smooth(
        method = "lm", formula = y ~ x + I(x^2), color = "red", se = TRUE
    ) +
    labs(
        x = "Per capita GDP",
        y = "Overall Life Satisfaction",
        title = "Per capita GDP vs. Overall Life Satisfaction (Quadratic)"
    ) +
    theme_minimal()
Warning: Removed 3 rows containing non-finite outside the scale range
(`stat_smooth()`).
Warning: Removed 3 rows containing missing values or values outside the scale range
(`geom_point()`).

Changes in GDP and life satisfaction

Q5. Create two new columns, gdp_yoy and life_satisf_yoy, which represent the within-country year-on-year changes in GDP and life satisfaction, respectively.

This is panel data: to compute a year-on-year change correctly, you must first sort observations chronologically within each country.

arrange(country, year) followed by group_by(country), then use lag() inside mutate() to access the previous year’s value.

Because this is panel data, we must sort chronologically and group by country before calculating changes to avoid mixed metrics between borders:

tb <- tb |>
    arrange(country, year) |>
    group_by(country) |>
    mutate(
        gdp_yoy = gdp - lag(gdp),
        life_satisf_yoy = life_satisf - lag(life_satisf)
    ) |>
    ungroup()
tb
# A tibble: 231 × 6
   country  year   gdp life_satisf gdp_yoy life_satisf_yoy
   <chr>   <dbl> <dbl>       <dbl>   <dbl>           <dbl>
 1 AL       2018  4730         5.5      NA         NA     
 2 AL       2021  5190         5.7     460          0.200 
 3 AL       2022  5500         6       310          0.300 
 4 AT       2013 43080         7.8      NA         NA     
 5 AT       2018 45140         8      2060          0.200 
 6 AT       2021 44580         8      -560          0     
 7 AT       2022 46430         7.9    1850         -0.1000
 8 AT       2023 45670         7.7    -760         -0.200 
 9 AT       2024 45140         7.6    -530         -0.100 
10 AT       2025 45390         7.7     250          0.100 
# ℹ 221 more rows

Q6. Create a scatterplot to illustrate the relationship between gdp_yoy and life_satisf_yoy.

  • Place the year-on-year variation in life satisfaction on the y-axis and label it “Yearly Change in Life Satisfaction”.
  • Place the year-on-year variation in per capita GDP on the x-axis and label it “Yearly Change in Per Capita GDP”.
  • Title the graph “Relationship Between Changes in GDP and Life Satisfaction”.
  • Add a linear regression line to the scatterplot.
ggplot(data = tb, mapping = aes(x = gdp_yoy, y = life_satisf_yoy)) +
    geom_point(alpha = 0.5) +
    geom_smooth(method = "lm", color = "darkgreen") +
    labs(
        x = "Yearly Change in Per Capita GDP",
        y = "Yearly Change in Life Satisfaction",
        title = "Relationship Between Changes in GDP and Life Satisfaction"
    ) +
    theme_minimal()
`geom_smooth()` using formula = 'y ~ x'
Warning: Removed 40 rows containing non-finite outside the scale range
(`stat_smooth()`).
Warning: Removed 40 rows containing missing values or values outside the scale range
(`geom_point()`).

OLS Regression

Q7. Examine and evaluate the following instructions. Then, explain what they do.

model_lm <- lm(life_satisf ~ gdp, data = tb)
summary(model_lm)

The lm() command calculates a standard pooled ordinary least squares (OLS) linear model mapping the effect of independent variable gdp onto dependent metric life_satisf. Calling summary() prints the descriptive evaluation properties: regression estimates (\(\hat{\beta}\) parameters), standard errors, \(t\)-statistics, associated \(p\)-values, and overall model diagnostics (\(R^2\) variance performance).


Call:
lm(formula = life_satisf ~ gdp, data = tb)

Residuals:
    Min      1Q  Median      3Q     Max 
-1.8927 -0.2946  0.1098  0.3622  1.0140 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 6.566e+00  6.162e-02  106.55   <2e-16 ***
gdp         1.669e-05  1.537e-06   10.86   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.5457 on 226 degrees of freedom
  (3 observations deleted due to missingness)
Multiple R-squared:  0.3428,    Adjusted R-squared:  0.3399 
F-statistic: 117.9 on 1 and 226 DF,  p-value: < 2.2e-16

Q8. Examine and evaluate the following instructions. Then, explain what they do.

library(stargazer)
stargazer(model_lm, type = "latex", out = "tables/model_lm.tex")

The function converts the raw statistical model object metrics directly into a formatted LaTeX tabular canvas, printing and writing the compiled outputs to the external filepath location: tables/model_lm.tex.


% Table created by stargazer v.5.2.3 by Marek Hlavac, Social Policy Institute. E-mail: marek.hlavac at gmail.com
% Date and time: Wed, Sep 16, 2026 - 19:15:27
\begin{table}[!htbp] \centering 
  \caption{} 
  \label{} 
\begin{tabular}{@{\extracolsep{5pt}}lc} 
\\[-1.8ex]\hline 
\hline \\[-1.8ex] 
 & \multicolumn{1}{c}{\textit{Dependent variable:}} \\ 
\cline{2-2} 
\\[-1.8ex] & life\_satisf \\ 
\hline \\[-1.8ex] 
 gdp & 0.00002$^{***}$ \\ 
  & (0.00000) \\ 
  & \\ 
 Constant & 6.566$^{***}$ \\ 
  & (0.062) \\ 
  & \\ 
\hline \\[-1.8ex] 
Observations & 228 \\ 
R$^{2}$ & 0.343 \\ 
Adjusted R$^{2}$ & 0.340 \\ 
Residual Std. Error & 0.546 (df = 226) \\ 
F Statistic & 117.908$^{***}$ (df = 1; 226) \\ 
\hline 
\hline \\[-1.8ex] 
\textit{Note:}  & \multicolumn{1}{r}{$^{*}$p$<$0.1; $^{**}$p$<$0.05; $^{***}$p$<$0.01} \\ 
\end{tabular} 
\end{table} 

Fixed effects regression

Q9. Extend the previous regression model by including country fixed effects to account for unobserved, time-invariant differences across countries.

model_country_fe <- ...
summary(model_country_fe)

Add country as a right-hand-side term, wrapped in factor() so it is treated as a categorical variable (a set of dummies) rather than a numeric one.

We append country variables as dummy indices using the factor() wrapper:

model_country_fe <- lm(life_satisf ~ gdp + factor(country), data = tb)
summary(model_country_fe)

Call:
lm(formula = life_satisf ~ gdp + factor(country), data = tb)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.98823 -0.11981  0.01423  0.14025  0.68751 

Coefficients:
                    Estimate Std. Error t value Pr(>|t|)    
(Intercept)        5.662e+00  1.582e-01  35.799  < 2e-16 ***
gdp                1.386e-05  5.183e-06   2.674 0.008141 ** 
factor(country)AT  1.528e+00  2.784e-01   5.486 1.28e-07 ***
factor(country)BE  1.328e+00  2.705e-01   4.911 1.93e-06 ***
factor(country)BG -1.042e-01  1.882e-01  -0.553 0.580648    
factor(country)CH  1.124e+00  4.342e-01   2.588 0.010396 *  
factor(country)CY  1.002e+00  2.190e-01   4.577 8.47e-06 ***
factor(country)CZ  1.357e+00  2.043e-01   6.641 3.12e-10 ***
factor(country)DE  7.717e-01  2.748e-01   2.808 0.005494 ** 
factor(country)DK  1.232e+00  3.233e-01   3.810 0.000187 ***
factor(country)EE  1.114e+00  2.042e-01   5.456 1.49e-07 ***
factor(country)EL  7.478e-01  1.976e-01   3.784 0.000206 ***
factor(country)ES  1.128e+00  2.167e-01   5.204 4.99e-07 ***
factor(country)FI  1.668e+00  2.713e-01   6.149 4.43e-09 ***
factor(country)FR  8.954e-01  2.493e-01   3.592 0.000416 ***
factor(country)HR  9.840e-01  1.935e-01   5.084 8.72e-07 ***
factor(country)HU  8.995e-01  1.934e-01   4.651 6.15e-06 ***
factor(country)IE  7.898e-01  4.341e-01   1.820 0.070367 .  
factor(country)IS  1.612e+00  3.609e-01   4.466 1.36e-05 ***
factor(country)IT  9.716e-01  2.313e-01   4.201 4.07e-05 ***
factor(country)LT  1.070e+00  1.984e-01   5.393 2.02e-07 ***
factor(country)LU  2.534e-01  5.403e-01   0.469 0.639610    
factor(country)LV  8.855e-01  1.949e-01   4.544 9.74e-06 ***
factor(country)ME  4.324e-01  1.976e-01   2.188 0.029881 *  
factor(country)MK  7.026e-02  1.973e-01   0.356 0.722157    
factor(country)MT  1.282e+00  2.291e-01   5.594 7.55e-08 ***
factor(country)NL  1.324e+00  2.958e-01   4.475 1.31e-05 ***
factor(country)NO  1.153e+00  3.613e-01   3.192 0.001649 ** 
factor(country)PL  1.715e+00  1.933e-01   8.871 5.03e-16 ***
factor(country)PT  9.763e-01  2.085e-01   4.683 5.33e-06 ***
factor(country)RO  1.772e+00  1.897e-01   9.338  < 2e-16 ***
factor(country)RS  2.428e-01  1.869e-01   1.299 0.195356    
factor(country)SE  1.264e+00  2.883e-01   4.384 1.92e-05 ***
factor(country)SI  1.492e+00  2.102e-01   7.100 2.36e-11 ***
factor(country)SK  1.260e+00  1.979e-01   6.366 1.40e-09 ***
factor(country)TR -1.239e-01  1.915e-01  -0.647 0.518443    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.27 on 192 degrees of freedom
  (3 observations deleted due to missingness)
Multiple R-squared:  0.8633,    Adjusted R-squared:  0.8384 
F-statistic: 34.64 on 35 and 192 DF,  p-value: < 2.2e-16

Q10. Extend the country fixed-effects model by adding year fixed effects to control for common shocks across all countries in a given year.

model_country_time_fe <- ...
summary(model_country_time_fe)

Same idea as Q9, but add a second factor() term for year alongside factor(country).

model_country_time_fe <- lm(
    life_satisf ~ gdp + factor(country) + factor(year), 
    data = tb
)
summary(model_country_time_fe)

Call:
lm(formula = life_satisf ~ gdp + factor(country) + factor(year), 
    data = tb)

Residuals:
    Min      1Q  Median      3Q     Max 
-0.7411 -0.1381  0.0010  0.1385  0.5717 

Coefficients:
                    Estimate Std. Error t value Pr(>|t|)    
(Intercept)        5.511e+00  1.498e-01  36.802  < 2e-16 ***
gdp               -8.119e-06  5.897e-06  -1.377 0.170211    
factor(country)AT  2.380e+00  2.910e-01   8.180 4.33e-14 ***
factor(country)BE  2.135e+00  2.810e-01   7.596 1.44e-12 ***
factor(country)BG -1.667e-02  1.727e-01  -0.097 0.923212    
factor(country)CH  2.781e+00  4.791e-01   5.803 2.75e-08 ***
factor(country)CY  1.465e+00  2.149e-01   6.815 1.27e-10 ***
factor(country)CZ  1.687e+00  1.953e-01   8.642 2.50e-15 ***
factor(country)DE  1.578e+00  2.855e-01   5.528 1.08e-07 ***
factor(country)DK  2.327e+00  3.460e-01   6.726 2.08e-10 ***
factor(country)EE  1.444e+00  1.952e-01   7.401 4.53e-12 ***
factor(country)EL  1.002e+00  1.860e-01   5.387 2.15e-07 ***
factor(country)ES  1.572e+00  2.120e-01   7.418 4.11e-12 ***
factor(country)FI  2.479e+00  2.820e-01   8.791 9.86e-16 ***
factor(country)FR  1.573e+00  2.543e-01   6.185 3.85e-09 ***
factor(country)HR  1.181e+00  1.803e-01   6.549 5.50e-10 ***
factor(country)HU  1.094e+00  1.801e-01   6.075 6.84e-09 ***
factor(country)IE  2.427e+00  4.780e-01   5.078 9.23e-07 ***
factor(country)IS  2.858e+00  3.897e-01   7.335 6.63e-12 ***
factor(country)IT  1.528e+00  2.312e-01   6.610 3.94e-10 ***
factor(country)LT  1.334e+00  1.871e-01   7.130 2.15e-11 ***
factor(country)LU  2.379e+00  6.023e-01   3.950 0.000111 ***
factor(country)LV  1.103e+00  1.822e-01   6.052 7.73e-09 ***
factor(country)ME  5.159e-01  1.805e-01   2.859 0.004741 ** 
factor(country)MK  4.698e-02  1.795e-01   0.262 0.793826    
factor(country)MT  1.822e+00  2.283e-01   7.981 1.45e-13 ***
factor(country)NL  2.273e+00  3.125e-01   7.275 9.37e-12 ***
factor(country)NO  2.441e+00  3.917e-01   6.232 3.02e-09 ***
factor(country)PL  1.908e+00  1.800e-01  10.601  < 2e-16 ***
factor(country)PT  1.300e+00  1.994e-01   6.520 6.45e-10 ***
factor(country)RO  1.898e+00  1.749e-01  10.850  < 2e-16 ***
factor(country)RS  2.785e-01  1.708e-01   1.630 0.104740    
factor(country)SE  2.172e+00  3.032e-01   7.164 1.77e-11 ***
factor(country)SI  1.880e+00  2.032e-01   9.251  < 2e-16 ***
factor(country)SK  1.518e+00  1.864e-01   8.141 5.50e-14 ***
factor(country)TR -5.671e-02  1.751e-01  -0.324 0.746409    
factor(year)2018   2.699e-01  6.316e-02   4.273 3.07e-05 ***
factor(year)2021   2.511e-01  6.822e-02   3.681 0.000304 ***
factor(year)2022   2.698e-01  6.933e-02   3.892 0.000138 ***
factor(year)2023   3.817e-01  6.917e-02   5.519 1.14e-07 ***
factor(year)2024   3.920e-01  7.074e-02   5.542 1.01e-07 ***
factor(year)2025   4.545e-01  7.390e-02   6.151 4.60e-09 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.2448 on 186 degrees of freedom
  (3 observations deleted due to missingness)
Multiple R-squared:  0.8912,    Adjusted R-squared:  0.8672 
F-statistic: 37.16 on 41 and 186 DF,  p-value: < 2.2e-16

Q11. Examine and evaluate the following instructions. Then, explain what they do.

stargazer(
    model_lm, model_country_fe, model_country_time_fe,
    type = "latex",
    out = "tables/regression_results.tex",
    title = "Correlation between per capita GDP and life satisfaction",
    column.labels = c("Baseline OLS Reg.", "Country FE", "Country and Year FE"),
    covariate.labels = c("per capita GDP"),
    add.lines = list(
        c('Country FE', '-', 'Yes', 'Yes'),
        c('Year FE', '-', '-', 'Yes')
    )
)

This block merges the baseline, country FE, and two-way FE models into a single side-by-side LaTeX compilation table. It replaces the long list of individual dummy coefficients with neat summary rows using custom indicators in add.lines, saves the resulting output inside tables/regression_results.tex, and updates the column titles appropriately.


% Table created by stargazer v.5.2.3 by Marek Hlavac, Social Policy Institute. E-mail: marek.hlavac at gmail.com
% Date and time: Wed, Sep 16, 2026 - 19:15:27
\begin{table}[!htbp] \centering 
  \caption{Correlation between per capita GDP and life satisfaction} 
  \label{} 
\begin{tabular}{@{\extracolsep{5pt}}lccc} 
\\[-1.8ex]\hline 
\hline \\[-1.8ex] 
 & \multicolumn{3}{c}{\textit{Dependent variable:}} \\ 
\cline{2-4} 
\\[-1.8ex] & \multicolumn{3}{c}{life\_satisf} \\ 
 & Baseline OLS Reg. & Country FE & Country and Year FE \\ 
\\[-1.8ex] & (1) & (2) & (3)\\ 
\hline \\[-1.8ex] 
 per capita GDP & 0.00002$^{***}$ & 0.00001$^{***}$ & $-$0.00001 \\ 
  & (0.00000) & (0.00001) & (0.00001) \\ 
  & & & \\ 
 factor(country)AT &  & 1.528$^{***}$ & 2.380$^{***}$ \\ 
  &  & (0.278) & (0.291) \\ 
  & & & \\ 
 factor(country)BE &  & 1.328$^{***}$ & 2.135$^{***}$ \\ 
  &  & (0.270) & (0.281) \\ 
  & & & \\ 
 factor(country)BG &  & $-$0.104 & $-$0.017 \\ 
  &  & (0.188) & (0.173) \\ 
  & & & \\ 
 factor(country)CH &  & 1.124$^{**}$ & 2.781$^{***}$ \\ 
  &  & (0.434) & (0.479) \\ 
  & & & \\ 
 factor(country)CY &  & 1.002$^{***}$ & 1.465$^{***}$ \\ 
  &  & (0.219) & (0.215) \\ 
  & & & \\ 
 factor(country)CZ &  & 1.357$^{***}$ & 1.687$^{***}$ \\ 
  &  & (0.204) & (0.195) \\ 
  & & & \\ 
 factor(country)DE &  & 0.772$^{***}$ & 1.578$^{***}$ \\ 
  &  & (0.275) & (0.286) \\ 
  & & & \\ 
 factor(country)DK &  & 1.232$^{***}$ & 2.327$^{***}$ \\ 
  &  & (0.323) & (0.346) \\ 
  & & & \\ 
 factor(country)EE &  & 1.114$^{***}$ & 1.444$^{***}$ \\ 
  &  & (0.204) & (0.195) \\ 
  & & & \\ 
 factor(country)EL &  & 0.748$^{***}$ & 1.002$^{***}$ \\ 
  &  & (0.198) & (0.186) \\ 
  & & & \\ 
 factor(country)ES &  & 1.128$^{***}$ & 1.572$^{***}$ \\ 
  &  & (0.217) & (0.212) \\ 
  & & & \\ 
 factor(country)FI &  & 1.668$^{***}$ & 2.479$^{***}$ \\ 
  &  & (0.271) & (0.282) \\ 
  & & & \\ 
 factor(country)FR &  & 0.895$^{***}$ & 1.573$^{***}$ \\ 
  &  & (0.249) & (0.254) \\ 
  & & & \\ 
 factor(country)HR &  & 0.984$^{***}$ & 1.181$^{***}$ \\ 
  &  & (0.194) & (0.180) \\ 
  & & & \\ 
 factor(country)HU &  & 0.899$^{***}$ & 1.094$^{***}$ \\ 
  &  & (0.193) & (0.180) \\ 
  & & & \\ 
 factor(country)IE &  & 0.790$^{*}$ & 2.427$^{***}$ \\ 
  &  & (0.434) & (0.478) \\ 
  & & & \\ 
 factor(country)IS &  & 1.612$^{***}$ & 2.858$^{***}$ \\ 
  &  & (0.361) & (0.390) \\ 
  & & & \\ 
 factor(country)IT &  & 0.972$^{***}$ & 1.528$^{***}$ \\ 
  &  & (0.231) & (0.231) \\ 
  & & & \\ 
 factor(country)LT &  & 1.070$^{***}$ & 1.334$^{***}$ \\ 
  &  & (0.198) & (0.187) \\ 
  & & & \\ 
 factor(country)LU &  & 0.253 & 2.379$^{***}$ \\ 
  &  & (0.540) & (0.602) \\ 
  & & & \\ 
 factor(country)LV &  & 0.886$^{***}$ & 1.103$^{***}$ \\ 
  &  & (0.195) & (0.182) \\ 
  & & & \\ 
 factor(country)ME &  & 0.432$^{**}$ & 0.516$^{***}$ \\ 
  &  & (0.198) & (0.180) \\ 
  & & & \\ 
 factor(country)MK &  & 0.070 & 0.047 \\ 
  &  & (0.197) & (0.179) \\ 
  & & & \\ 
 factor(country)MT &  & 1.282$^{***}$ & 1.822$^{***}$ \\ 
  &  & (0.229) & (0.228) \\ 
  & & & \\ 
 factor(country)NL &  & 1.324$^{***}$ & 2.273$^{***}$ \\ 
  &  & (0.296) & (0.312) \\ 
  & & & \\ 
 factor(country)NO &  & 1.153$^{***}$ & 2.441$^{***}$ \\ 
  &  & (0.361) & (0.392) \\ 
  & & & \\ 
 factor(country)PL &  & 1.715$^{***}$ & 1.908$^{***}$ \\ 
  &  & (0.193) & (0.180) \\ 
  & & & \\ 
 factor(country)PT &  & 0.976$^{***}$ & 1.300$^{***}$ \\ 
  &  & (0.208) & (0.199) \\ 
  & & & \\ 
 factor(country)RO &  & 1.772$^{***}$ & 1.898$^{***}$ \\ 
  &  & (0.190) & (0.175) \\ 
  & & & \\ 
 factor(country)RS &  & 0.243 & 0.278 \\ 
  &  & (0.187) & (0.171) \\ 
  & & & \\ 
 factor(country)SE &  & 1.264$^{***}$ & 2.172$^{***}$ \\ 
  &  & (0.288) & (0.303) \\ 
  & & & \\ 
 factor(country)SI &  & 1.492$^{***}$ & 1.880$^{***}$ \\ 
  &  & (0.210) & (0.203) \\ 
  & & & \\ 
 factor(country)SK &  & 1.260$^{***}$ & 1.518$^{***}$ \\ 
  &  & (0.198) & (0.186) \\ 
  & & & \\ 
 factor(country)TR &  & $-$0.124 & $-$0.057 \\ 
  &  & (0.192) & (0.175) \\ 
  & & & \\ 
 factor(year)2018 &  &  & 0.270$^{***}$ \\ 
  &  &  & (0.063) \\ 
  & & & \\ 
 factor(year)2021 &  &  & 0.251$^{***}$ \\ 
  &  &  & (0.068) \\ 
  & & & \\ 
 factor(year)2022 &  &  & 0.270$^{***}$ \\ 
  &  &  & (0.069) \\ 
  & & & \\ 
 factor(year)2023 &  &  & 0.382$^{***}$ \\ 
  &  &  & (0.069) \\ 
  & & & \\ 
 factor(year)2024 &  &  & 0.392$^{***}$ \\ 
  &  &  & (0.071) \\ 
  & & & \\ 
 factor(year)2025 &  &  & 0.455$^{***}$ \\ 
  &  &  & (0.074) \\ 
  & & & \\ 
 Constant & 6.566$^{***}$ & 5.662$^{***}$ & 5.511$^{***}$ \\ 
  & (0.062) & (0.158) & (0.150) \\ 
  & & & \\ 
\hline \\[-1.8ex] 
Country FE & - & Yes & Yes \\ 
Year FE & - & - & Yes \\ 
Observations & 228 & 228 & 228 \\ 
R$^{2}$ & 0.343 & 0.863 & 0.891 \\ 
Adjusted R$^{2}$ & 0.340 & 0.838 & 0.867 \\ 
Residual Std. Error & 0.546 (df = 226) & 0.270 (df = 192) & 0.245 (df = 186) \\ 
F Statistic & 117.908$^{***}$ (df = 1; 226) & 34.641$^{***}$ (df = 35; 192) & 37.156$^{***}$ (df = 41; 186) \\ 
\hline 
\hline \\[-1.8ex] 
\textit{Note:}  & \multicolumn{3}{r}{$^{*}$p$<$0.1; $^{**}$p$<$0.05; $^{***}$p$<$0.01} \\ 
\end{tabular} 
\end{table} 

Non-linear regression

We herein consider non-linear regression models with a dummy as dependent variable, such as probit and logit regression models.

Q12. Compute the median of life satisfaction.

median_life_satisf <- ...
median_life_satisf <- median(tb$life_satisf, na.rm = TRUE)
median_life_satisf
[1] 7.2

Q13. In the dataset, create a binary variable named life_satisf_above_med, which takes the value 1 if life satisfaction is above the median and 0 otherwise.

tb <- ...
tb <- tb |>
    mutate(
        life_satisf_above_med = ifelse(
            life_satisf > median_life_satisf, 
            1,
            0
        )
    )
tb
# A tibble: 231 × 7
   country  year   gdp life_satisf gdp_yoy life_satisf_yoy life_satisf_above_med
   <chr>   <dbl> <dbl>       <dbl>   <dbl>           <dbl>                 <dbl>
 1 AL       2018  4730         5.5      NA         NA                          0
 2 AL       2021  5190         5.7     460          0.200                      0
 3 AL       2022  5500         6       310          0.300                      0
 4 AT       2013 43080         7.8      NA         NA                          1
 5 AT       2018 45140         8      2060          0.200                      1
 6 AT       2021 44580         8      -560          0                          1
 7 AT       2022 46430         7.9    1850         -0.1000                     1
 8 AT       2023 45670         7.7    -760         -0.200                      1
 9 AT       2024 45140         7.6    -530         -0.100                      1
10 AT       2025 45390         7.7     250          0.100                      1
# ℹ 221 more rows

Q14. Run a probit regression of life_satisf_above_med on gdp, and display the summary of the results.

model_probit <- ...
summary(model_probit)

With a binary dependent variable, lm() is no longer appropriate — use glm() with family = binomial(link = "probit").

We utilize generalized linear model modeling via glm() specifying link definitions:

model_probit <- glm(
    life_satisf_above_med ~ gdp, 
    data = tb,
    family = binomial(link = "probit")
)
summary(model_probit)

Call:
glm(formula = life_satisf_above_med ~ gdp, family = binomial(link = "probit"), 
    data = tb)

Coefficients:
              Estimate Std. Error z value Pr(>|z|)    
(Intercept) -1.221e+00  1.840e-01  -6.636 3.23e-11 ***
gdp          3.955e-05  5.742e-06   6.888 5.66e-12 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 316.06  on 227  degrees of freedom
Residual deviance: 236.64  on 226  degrees of freedom
  (3 observations deleted due to missingness)
AIC: 240.64

Number of Fisher Scoring iterations: 6

Q15. Run a logit regression of life_satisf_above_med on gdp, and display the summary of the results.

model_logit <- ...
summary(model_logit)

Same as Q14, but change the link function to "logit".

model_logit <- glm(
    life_satisf_above_med ~ gdp, 
    data = tb,
    family = binomial(link = "logit")
)
summary(model_logit)

Call:
glm(formula = life_satisf_above_med ~ gdp, family = binomial(link = "logit"), 
    data = tb)

Coefficients:
              Estimate Std. Error z value Pr(>|z|)    
(Intercept) -2.342e+00  3.459e-01  -6.771 1.28e-11 ***
gdp          7.912e-05  1.146e-05   6.903 5.10e-12 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 316.06  on 227  degrees of freedom
Residual deviance: 229.90  on 226  degrees of freedom
  (3 observations deleted due to missingness)
AIC: 233.9

Number of Fisher Scoring iterations: 5

Statistical tests

Q16. Compute the median of per capita GDP.

median_gdp <- ...
median_gdp <- median(tb$gdp, na.rm = TRUE)
median_gdp
[1] 23310

Q17. In the dataset, create a binary variable named gdp_above_med, which takes the value 1 if per capita GDP is above the median and 0 otherwise.

tb <- ...
tb <- tb |>
    mutate(gdp_above_med = ifelse(gdp > median_gdp, 1, 0))
tb
# A tibble: 231 × 8
   country  year   gdp life_satisf gdp_yoy life_satisf_yoy life_satisf_above_med
   <chr>   <dbl> <dbl>       <dbl>   <dbl>           <dbl>                 <dbl>
 1 AL       2018  4730         5.5      NA         NA                          0
 2 AL       2021  5190         5.7     460          0.200                      0
 3 AL       2022  5500         6       310          0.300                      0
 4 AT       2013 43080         7.8      NA         NA                          1
 5 AT       2018 45140         8      2060          0.200                      1
 6 AT       2021 44580         8      -560          0                          1
 7 AT       2022 46430         7.9    1850         -0.1000                     1
 8 AT       2023 45670         7.7    -760         -0.200                      1
 9 AT       2024 45140         7.6    -530         -0.100                      1
10 AT       2025 45390         7.7     250          0.100                      1
# ℹ 221 more rows
# ℹ 1 more variable: gdp_above_med <dbl>

Q18. Examine and evaluate the following instructions. Then, explain what they do.

group1 <- tb |>
    filter(gdp_above_med == 0) |>
    pull(life_satisf)
    
group2 <- tb |>
    filter(gdp_above_med == 1) |>
    pull(life_satisf)

These two instructions split the dataset into two independent numeric vectors: group1 contains the life satisfaction values for countries with below-median GDP, and group2 contains the life satisfaction values for countries with above-median GDP. pull() extracts a single column as a plain vector, rather than as a one-column data frame.

Q19. Perform a Student’s t-test to compare the mean life satisfaction between observations with GDP above the median and those with GDP at or below the median.

t_test_result <- ...
print(t_test_result)

Use t.test() on the two vectors created in Q18, with alternative = "two.sided" since we have no prior expectation on the direction of the difference.

t_test_result <- t.test(group2, group1, alternative = "two.sided")
print(t_test_result)

    Welch Two Sample t-test

data:  group2 and group1
t = 11.183, df = 164.12, p-value < 2.2e-16
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 0.6587468 0.9412532
sample estimates:
mean of x mean of y 
 7.507895  6.707895 

Q20. Extract the test statistic and the p-value from the results of the t-test.

test_statistic <- ...
p_value <- ...

The object returned by t.test() is a list — access its named elements with the $ operator, just like a column of a data frame.

Using list element indexing on the returned object properties:

test_statistic <- t_test_result$statistic
p_value <- t_test_result$p.value
test_statistic
       t 
11.18289 
p_value
[1] 6.100442e-22

Q21. Examine and evaluate the following instructions. Then, explain what they do.

latex_table <- paste0(
    "\\begin{tabular}{l c}\n",
    "\\hline\n",
    "& Difference in life satisfaction across groups" , 
    " \\\\ \n",
    "Test Statistic & ", 
    format(test_statistic, digits = 3), 
    " \\\\ \n",
    "P-value & ", 
    format.pval(p_value, digits = 3), 
    " \\\\ \n",
    "\\hline\n",
    "\\end{tabular}"
)
file_path <- "tables/table_results_t_test_1_v1.tex"
writeLines(latex_table, file_path)

paste0() glues character strings together with no separator — here, it is building a LaTeX tabular environment line by line, as a single block of text.

This block manually builds a small LaTeX table (a tabular environment with two rows: the test statistic and the p-value) as a character string, using paste0() to concatenate the LaTeX syntax with the formatted numeric results. format() and format.pval() control the number of significant digits displayed. writeLines() then writes this character string to the file tables/table_results_t_test_1_v1.tex, one line at a time. This is a manual alternative to stargazer(), useful for results (like a t-test) that are not regression model objects.

\begin{tabular}{l c}
\hline
& Difference in life satisfaction across groups \\ 
Test Statistic & 11.2 \\ 
P-value & <2e-16 \\ 
\hline
\end{tabular}

Introduction to Programming for Data Analysis — Master 1 in Economics