Tutorial #2. Data visualization and summary statistics

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-2

tutorial-2/
  data/
    out/ gdp-lifesat.csv
  scripts/

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.

In the previous tutorial, 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 work with the dataset from the second exercise of Tutorial #1 to:

  1. explore it through basic summary statistics,
  2. create your first data visualizations in R using ggplot2.

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.

💡 Clean session

If you need to have a clean session in RStudio, first remove all the objects in memory by evaluating the following instruction:

rm(list = ls())

Then, on the menu “Session”, click on “Restart R”.

Setup

Prepare a project for the second tutorial. Adopt the tree structure presented below. Note that launching your session via project.Rproj sets your active working directory to tutorial-2/.

tutorial-2/
  data/
    raw/
    tmp/
    out/
      gdp-lifesat.csv
  scripts/
  figs/
  tables/
  references/
  README.md
  project.Rproj

Q1. Import the dataset you exported at the end of Tutorial #1 (gdp-lifesat.csv, assumed to be copied into your data entry structure).

tb <- read_csv(...)

The file lives in your data/out/ folder — build the relative path from your project root (tutorial-2/).

library(readr)
# Assuming the file was moved to the data output folder of the project root
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

Plot Distributions

Q2. Read the following code, evaluate it, and explain what it does.

p_density <- ggplot(
  data = tb, 
  mapping = aes(x = gdp)
) +
  geom_density(fill = "blue", alpha = 0.1) +
  labs(
    x = "GDP", 
    y = "Density",
    title = "Density Plot of GDP"
  ) +
  theme_minimal() +
  theme(
    panel.grid.major = element_blank(), 
    panel.grid.minor = element_blank()
  )
p_density

This block initialises a graphic via ggplot2 using the tb dataset. It maps the continuous metric gdp to the x-axis and draws an empirical density distribution via geom_density(), filled with light blue (10% opacity). It adds clean labels and applies a minimal theme. The background grid lines are removed.

Q3. Using the ggsave() function, export this graph as a PDF file in the figs folder of your project environment.

ggsave("figs/density_plot_gdp.pdf", plot = p_density, width = 7, height = 5)

Q4. Plot the distribution of the variable measuring life satisfaction using a histogram:

  • Label the x-axis as “Overall life satisfaction”.
  • Remove the y-axis title.
  • Add the plot title “Histogram of Overall life satisfaction”.

Use geom_histogram(), and set y = NULL inside labs() to remove an axis title.

ggplot(data = tb, mapping = aes(x = life_satisf)) +
  geom_histogram(bins = 15, fill = "darkgray", color = "white") +
  labs(
    x = "Overall life satisfaction",
    y = NULL, # Removes the y-axis title
    title = "Histogram of Overall life satisfaction"
  ) +
  theme_minimal()
Warning: Removed 3 rows containing non-finite outside the scale range
(`stat_bin()`).

Summary Statistics

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

summary_stats_base <- tb |>
  select(gdp, life_satisf) |>
  summary()

print(summary_stats_base)

This pipeline of instructions extracts the features gdp and life_satisf from the data frame tb and passes them directly into R’s base summary function summary(). This returns the descriptive quantities (Minimum, 1st Quartile, Median, Mean, 3rd Quartile, Maximum) alongside missing item flags (NAs) per variable.

      gdp          life_satisf   
 Min.   :  4730   Min.   :4.800  
 1st Qu.: 16000   1st Qu.:6.800  
 Median : 23310   Median :7.200  
 Mean   : 32362   Mean   :7.108  
 3rd Qu.: 44210   3rd Qu.:7.600  
 Max.   :107570   Max.   :8.100  
                  NAs    :3      

Q6. Create a table of summary statistics for the variables gdp and life_satisf. The table should include the following statistics: mean, standard deviation, minimum, maximum. Round all values to two decimal places. The resulting table must have two rows (one for gdp, one for life_satisf). Name this table summary_stats.

To get one row per variable, first reshape the two columns into a long format with pivot_longer().

Then group_by(var) and summarise() with mean(), sd(), min(), max(), each wrapped in round(..., 2).

Using tidyverse strategies to explicitly construct long summaries:

library(tidyr)

summary_stats <- tb |>
  select(gdp, life_satisf) |>
  pivot_longer(
    cols = everything(), names_to = "var", values_to = "val"
  ) |>
  group_by(var) |>
  summarise(
    mean = round(mean(val, na.rm = TRUE), 2),
    sd   = round(sd(val, na.rm = TRUE), 2),
    min  = round(min(val, na.rm = TRUE), 2),
    max  = round(max(val, na.rm = TRUE), 2)
  )
summary_stats
# A tibble: 2 × 5
  var             mean       sd    min      max
  <chr>          <dbl>    <dbl>  <dbl>    <dbl>
1 gdp         32362.   23512.   4730   107570  
2 life_satisf     7.11     0.67    4.8      8.1

Q7. Examine and evaluate the following instruction. Then, explain what it does.

names(summary_stats) <- c("var", "mean", "sd", "min", "max")
summary_stats

It replaces the names of the columns in the summary_stats summary table with those provided in the vector (c()). Here the names happen to already match, so the table is unchanged — but this instruction is what you would use to rename columns coming out of a pipeline with less convenient default names.

# A tibble: 2 × 5
  var             mean       sd    min      max
  <chr>          <dbl>    <dbl>  <dbl>    <dbl>
1 gdp         32362.   23512.   4730   107570  
2 life_satisf     7.11     0.67    4.8      8.1

Q8. Print the content of the object summary_stats as a LaTeX-formatted table. To do so, use the {stargazer} package. Adapt the following code:

install.packages("stargazer")
library(stargazer)
?stargazer
summary_stats_latex <- stargazer(...)

Save the LaTeX table into a file named summary_stats.tex inside your output data subfolder.

library(stargazer)

Please cite as: 
 Hlavac, Marek (2022). stargazer: Well-Formatted Regression and Summary Statistics Tables.
 R package version 5.2.3. https://CRAN.R-project.org/package=stargazer 
stargazer(
  summary_stats,
  summary = FALSE, 
  type = "latex", 
  out = "data/out/summary_stats.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:21
\begin{table}[!htbp] \centering 
  \caption{} 
  \label{} 
\begin{tabular}{@{\extracolsep{5pt}} cccccc} 
\\[-1.8ex]\hline 
\hline \\[-1.8ex] 
 & var & mean & sd & min & max \\ 
\hline \\[-1.8ex] 
1 & gdp & 32361.69 & 23511.96 & 4730 & 107570 \\ 
2 & life\_satisf & 7.11 & 0.67 & 4.8 & 8.1 \\ 
\hline \\[-1.8ex] 
\end{tabular} 
\end{table} 

Rank countries by GDP in 2023

Q9. Create an object called tb_2023 that contains only the rows of the dataset where the year is 2023.

tb_2023 <- tb |>
    ...
tb_2023 <- tb |> 
  filter(year == 2023)
tb_2023
# A tibble: 33 × 4
   country  year   gdp life_satisf
   <chr>   <dbl> <dbl>       <dbl>
 1 AT       2023 45670         7.7
 2 BE       2023 44190         7.7
 3 BG       2023 10950         5.9
 4 CH       2023 82440         7.8
 5 CY       2023 29320         7.4
 6 CZ       2023 21680         7.4
 7 DE       2023 43770         7  
 8 DK       2023 56770         7.5
 9 EE       2023 21580         7.2
10 EL       2023 18600         6.9
# ℹ 23 more rows

Q10. Examine and evaluate the following instruction. Then, explain what it does.

p <- ggplot(
  data = tb_2023, 
  mapping = aes(x = reorder(country, gdp), y = gdp)
) +
  geom_bar(stat = "identity") +
  coord_flip() +
  labs(
    title = "GDP by Country in 2023",
    x = "Country",
    y = "GDP"
  ) +
  theme_minimal()
p

This instruction prints a sorted horizontal bar chart. reorder(country, gdp) orders the categorical country axis sequentially by their corresponding value metric (that of variable gdp). geom_bar(stat = "identity") maps raw values directly to lengths, and coord_flip() rotates the layout by 90 degrees, swapping the x- and y-axes.

Q11. Create an object called data_filtered that contains only the rows of the dataset where the year is either 2013 or 2023.

data_filtered <- ...

Use %in% with a two-element vector, rather than two chained == conditions.

data_filtered <- tb |> 
  filter(year %in% c(2013, 2023))
data_filtered
# A tibble: 67 × 4
   country  year   gdp life_satisf
   <chr>   <dbl> <dbl>       <dbl>
 1 AT       2013 43080         7.8
 2 AT       2023 45670         7.7
 3 BE       2013 39440         7.5
 4 BE       2023 44190         7.7
 5 BG       2013  7600         4.8
 6 BG       2023 10950         5.9
 7 CH       2013 74650         8  
 8 CH       2023 82440         7.8
 9 CY       2013 20830         6.2
10 CY       2023 29320         7.4
# ℹ 57 more rows

Q12. Examine and evaluate the following instruction. Then, explain what it does.

data_filtered |>
  arrange(year, desc(gdp))

It sorts rows first chronologically by year, then sorts the observations in descending order of cross-sectional gdp within each temporal block.

# A tibble: 67 × 4
   country  year    gdp life_satisf
   <chr>   <dbl>  <dbl>       <dbl>
 1 LU       2013 102010         7.4
 2 CH       2013  74650         8  
 3 NO       2013  60260         7.9
 4 IS       2013  52880         8  
 5 DK       2013  49750         8  
 6 IE       2013  44830         7.5
 7 NL       2013  44230         7.8
 8 SE       2013  43770         7.9
 9 AT       2013  43080         7.8
10 FI       2013  41020         8  
# ℹ 57 more rows

Q13. Create a barplot ranking countries in decreasing order of GDP, with two panels: the left panel for year 2013, the right panel for year 2023. The plot should have:

  1. the x-axis titled “GDP”,
  2. no y-axis title,
  3. the main title “GDP by Country in 2013 and 2023”.

Use facet_wrap(~year) on top of the same bar chart recipe as Q10, then swap which axis gets the NULL label.

ggplot(data = data_filtered, mapping = aes(x = reorder(country, gdp), y = gdp)) +
  geom_bar(stat = "identity") +
  coord_flip() +
  facet_wrap(~year) + # Splitting into panels by year
  labs(
    x = NULL,
    y = "GDP",
    title = "GDP by Country in 2013 and 2023"
  ) +
  theme_minimal()

Rank countries by change in GDP between 2013 and 2023

Q14. Create a new variable called change_gdp that measures the within-country change in GDP between 2013 and 2023.

tb <- tb |> ...

This is a within-country comparison across years, so you need one row per country: reshape the data with pivot_wider() so that each year’s GDP becomes its own column.

pivot_wider(names_from = year, values_from = gdp, names_prefix = "gdp_") gives you columns gdp_2013 and gdp_2023. Then simply subtract them with mutate().

To extract delta values properly across years from wide or long frames:

tb_change <- tb |>
  filter(year %in% c(2013, 2023)) |>
  select(country, year, gdp) |>
  pivot_wider(names_from = year, values_from = gdp, names_prefix = "gdp_") |>
  mutate(change_gdp = gdp_2023 - gdp_2013)
tb_change
# A tibble: 34 × 4
   country gdp_2013 gdp_2023 change_gdp
   <chr>      <dbl>    <dbl>      <dbl>
 1 AT         43080    45670       2590
 2 BE         39440    44190       4750
 3 BG          7600    10950       3350
 4 CH         74650    82440       7790
 5 CY         20830    29320       8490
 6 CZ         17970    21680       3710
 7 DE         40160    43770       3610
 8 DK         49750    56770       7020
 9 EE         17630    21580       3950
10 EL         15800    18600       2800
# ℹ 24 more rows

Q15. Examine and evaluate the following instruction. Then, explain what it does.

tb_2023 <- tb |> filter(year == 2023)
tb_2023

It isolates cross-sectional observations from 2023 from the data frame tb and assigns the resulting data frame in an object named tb_2023.

# A tibble: 33 × 4
   country  year   gdp life_satisf
   <chr>   <dbl> <dbl>       <dbl>
 1 AT       2023 45670         7.7
 2 BE       2023 44190         7.7
 3 BG       2023 10950         5.9
 4 CH       2023 82440         7.8
 5 CY       2023 29320         7.4
 6 CZ       2023 21680         7.4
 7 DE       2023 43770         7  
 8 DK       2023 56770         7.5
 9 EE       2023 21580         7.2
10 EL       2023 18600         6.9
# ℹ 23 more rows

Q16. Create a barplot ranking countries in decreasing order of their change in GDP (between 2013 and 2023). The plot should have:

  • the x-axis titled “Change in GDP”,
  • no y-axis title,
  • the main title “Change in GDP by Country between 2013 and 2023”.
ggplot(data = tb_change, mapping = aes(x = reorder(country, change_gdp), y = change_gdp)) +
  geom_bar(stat = "identity") +
  coord_flip() +
  labs(
    x = NULL,
    y = "Change in GDP",
    title = "Change in GDP by Country between 2013 and 2023"
  ) +
  theme_minimal()
Warning: Removed 1 row containing missing values or values outside the scale range
(`geom_bar()`).

Evolution of GDP in France and Italy

Q17. Create an object called tb_france_italy that contains rows of the dataset tb where the country is either “France” or “Italy”.

tb_france_italy <- tb |> ...
tb_france_italy <- tb |> 
  filter(country %in% c("FR", "IT"))
tb_france_italy
# A tibble: 14 × 4
   country  year   gdp life_satisf
   <chr>   <dbl> <dbl>       <dbl>
 1 FR       2013 34990         7.1
 2 FR       2018 36570         7.3
 3 FR       2021 36460         6.8
 4 FR       2022 37280         7  
 5 FR       2023 37730         7.1
 6 FR       2024 38180         7.1
 7 FR       2025 38360         7.1
 8 IT       2013 28920         6.6
 9 IT       2018 30470         7  
10 IT       2021 30760         7.2
11 IT       2022 32310         7.2
12 IT       2023 32630         7.2
13 IT       2024 32900         7.2
14 IT       2025 33080         7.1

Q18. Using the dataset tb_france_italy, create a line plot showing the year-by-year evolution of GDP for France and Italy (one line per country). Include a legend to indicate which line corresponds to each country.

Bonus: use blue (#0055A4) for France and green (#009246) for Italy. Place the legend below the graph. Title the y-axis “GDP”.

Map color = country (and group = country) inside aes() so ggplot2 draws one line per country and builds the legend automatically.

For custom colors, use scale_color_manual(values = c("FR" = ..., "IT" = ...)). For the legend position, set theme(legend.position = "bottom").

ggplot(data = tb_france_italy, mapping = aes(x = year, y = gdp, color = country, group = country)) +
  geom_line(linewidth = 1) +
  geom_point() +
  scale_color_manual(values = c("FR" = "#0055A4", "IT" = "#009246")) +
  labs(
    x = "Year",
    y = "GDP",
    title = "Evolution of GDP in France and Italy"
  ) +
  theme_minimal() +
  theme(legend.position = "bottom")


Introduction to Programming for Data Analysis — Master 1 in Economics