Exercise: make it board-ready, then catch the tricks

Session 6 — Applied Data Science 2026

Published

02 11 2026

NoteThe setting

The board liked your charts from last week — but they were charts for you. Now they want one they can put in front of their investors, and they want to know whether the charts a consultant sent them can be trusted.

Alone or in pairs. Two parts, as in the lecture: A and B, then C, a short exchange with a fellow student.

ImportantStart here

Install the one new package, if you have not already:

install.packages("gghighlight")

Create session06.qmd in your project, copy both CSV files from the session page into data/, and start with this setup chunk:

```{r}
#| label: setup
#| include: false
library(tidyverse)
library(scales)
library(gghighlight)
library(here)

i_am("session06.qmd")

prices   <- read_csv(here("data", "hotels-europe_price.csv"))
features <- read_csv(here("data", "hotels-europe_features.csv"))

hotels <- prices |>
  filter(year == 2017, month == 11, weekend == 0) |>
  inner_join(features, by = "hotel_id") |>
  filter(city == city_actual, accommodation_type == "Hotel")

theme_board <- function(base_size = 13) {
  theme_minimal(base_size = base_size) +
    theme(
      plot.title            = element_text(face = "bold"),
      plot.title.position   = "plot",
      plot.caption          = element_text(colour = "grey45", hjust = 0),
      plot.caption.position = "plot",
      panel.grid.minor      = element_blank(),
      legend.position       = "top",
      legend.justification  = "left"
    )
}
```

Part A — Communication

A1 · The finding first

Board: “Where does Vienna stand among Europe’s cities — is it expensive?”

Before you write any code, compute what you need and write the one sentence your chart will say. It goes into the title later.

cities <- hotels |>
  group_by(city) |>
  summarise(n = n(), median_price = median(price), .groups = "drop")

europe_median <- median(hotels$price)

Where is Vienna’s median, compared with the European median? How many cities are more expensive?

A2 · The makeover

A colleague sent you this:

cities_mean <- hotels |>
  group_by(city) |>
  summarise(mean_price = mean(price))

ggplot(data = cities_mean, mapping = aes(x = city, y = mean_price, fill = city)) +
  geom_col()

Turn it into a chart that answers the board’s question in ten seconds. Work through these in order and render after each one:

  1. The right number: median, not mean (session 5).
  2. Readable: cities on the y axis, ordered by price.
  3. Emphasis: highlight Vienna with gghighlight(); the other 43 cities stay, in grey.
  4. A reference: a dashed line at the European median, labelled with annotate("label") on a white background, so that no line runs through the text.
  5. Say it: a title with your sentence from A1, a subtitle with what is measured, a caption with the source and n.
  6. House style: + theme_board().
ggplot(data = cities, mapping = aes(x = median_price,
                             y = fct_reorder(city, median_price))) +
  geom_col() +
  gghighlight(city == "Vienna") +
  geom_vline(xintercept = europe_median, linetype = "dashed") +
  annotate("label", x = europe_median, y = 3, label = "European median",
           hjust = -0.05, colour = "grey30", fill = "white", label.size = 0)

fill = city is gone: with gghighlight() the highlighted bar keeps its colour and you no longer need a legend. If you want a particular colour, put it in geom_col(fill = "#00395B").

labs(title = …, subtitle = …, caption = …, x = NULL, y = NULL). For n in the caption, compute it and paste it in: caption = paste0("Data: Békés & Kézdi (2021). ", nrow(hotels), " hotels in ", nrow(cities), " cities.")

A3 · Colour that means something

Take the weekend chart from session 5 — four cities, one line each — and replace ggplot’s default colours with the colour-blind-safe Okabe–Ito palette:

okabe_ito <- unname(palette.colors(palette = "Okabe-Ito"))
# then: + scale_colour_manual(values = okabe_ito[2:5])
weekends <- prices |>
  filter(weekend == 1) |>
  inner_join(features, by = "hotel_id") |>
  filter(city == city_actual, accommodation_type == "Hotel",
         city %in% c("London", "Paris", "Rome", "Vienna")) |>
  group_by(city, year, month) |>
  summarise(n = n(), median_price = median(price), .groups = "drop") |>
  mutate(date = make_date(year, month))

ggplot(data = weekends, mapping = aes(x = date, y = median_price, colour = city)) +
  geom_line() +
  geom_point() +
  scale_x_date(date_breaks = "1 month", date_labels = "%b") +
  scale_y_continuous(labels = label_number(prefix = "€")) +
  labs(x = NULL, y = NULL, colour = NULL) +
  theme_board()

Is colour doing a categorical, sequential or diverging job here?

Part B — Deception

B1 · Spot the trick

A consultant sent the board these four charts. Every number on them is correct. For each one, write down:

  • (a) what the chart wants you to believe,
  • (b) the trick — name it with the checklist from the lecture,
  • (c) what an honest version would show.

Chart 1

Chart 2

Chart 3

Chart 4

Claim · Baseline · Window · Axes · n · Summary · Encoding · Source

B2 · Fix two of them

Pick two of the four and build the honest version in your document. Give each the title it can actually support.

  • Chart 1: the median prices of the four cities; bars from zero.
  • Chart 2: Amsterdam’s median weekend price for every month in the data (session 5, Q6).
  • Chart 3: the median, and n — for example in the city label: paste0(city, " (n = ", n, ")").
  • Chart 4: the number of hotels per city, as bars or dots.

Part C — The checklist on a fellow student’s chart

Swap laptops with a fellow student. Run the eight checklist questions over their A2 chart. Write down the one thing you would change, and tell them.

The checklist

Before you hand in any chart — in Report 2 and after:

If you finish early

a) Diverging instead of highlighting

Redraw A2 as the difference from the European median, in percent, with scale_fill_gradient2(): blue below, red above, grey in the middle. Which version answers the board’s question better?

b) Make your own trick

Build one misleading chart from this data — honest numbers, one bad choice. Show it to a fellow student and let them find the trick.

c) Save it

Store your A2 chart as an object, a2 <- ggplot(…) + …, and save it twice:

  • as a PDF, 8 × 4.5 inches — the default for a report;
  • as a PNG for a PowerPoint deck: same size, dpi = 300, bg = "white".

Open both files and zoom in. Which one stays sharp? Does the text size still work at 8 × 4.5 inches — and if not, what do you change?

d) Two series, one axis

Lecture B3 put Vienna’s median weekend price and the number of hotels in the data on two y axes — and showed that the second axis can tell any story. Build the honest version: set both series to 100 in November 2017 and show the relative change from there, on one axis.

vienna_supply <- prices |>
  filter(weekend == 1) |>
  inner_join(features, by = "hotel_id") |>
  filter(city == "Vienna", city_actual == "Vienna", accommodation_type == "Hotel") |>
  group_by(year, month) |>
  summarise(n = n(), median_price = median(price), .groups = "drop") |>
  mutate(date = make_date(year, month))

What does the chart show in January — and what does its title not allow you to say?