This is the file you submit

Please do not start a blank .Rmd — work directly in this template, since every question is already laid out for you here.

  1. Put every group member in the YAML author line and in the names table below.
  2. Place your R in the named chunk directly under each part. Where a question has parts (a) and (b), use q1a then q1b — the same pattern continues later (q6a, q10a, q16a, and so on). These chunk names matter for marking, so please do not delete or rename them.
  3. Write your answers under Answer. Bring numbers in with inline R — a backtick, then r, then the object name (for example n_rows) — rather than typing the digits by hand.
  4. Keep this file next to the data/ folder so data/spotify_tracks_etf2121.csv loads.
  5. Knit often (Cmd/Ctrl + Shift + K), and submit both the knitted .html and this filled-in .Rmd.
  6. Submit the 5-minute group recording and complete the Group member evaluation (FeedbackFruits) by the same deadline. Every student must complete this.

Your knitted file needs to show the R code for every task, not just the numbers — hidden code (echo = FALSE) scores zero for that task.

ETF2121: Q1–Q15 (skip Part B) and Part C. ETF5912: Q1–Q18 and Part C.

This assignment, the recording, and the FeedbackFruits group member evaluation are due Friday 23 October 2026, 11.55pm.

Name Student ID Unit (ETF2121 or ETF5912)

Imagine you are briefing a Spotify manager using a course extract of tracks. The two tasks in front of you look related at first glance, but they are asking genuinely different questions.

Problem 1: How popular is a song, on a 0–100 scale? You will answer this with linear regression.

Problem 2: Is a song in the top 10% of this file — yes or no? You will answer this with logistic regression.

Clean the data before you model it — a model built on a messy table will mislead you long before it reaches a manager. Every number in your write-up must come from your own R output, and your recommendation should be something a manager could genuinely act on.

A real top song - BTS

Dynamite by BTS is a reminder that a “top song” is a business outcome worth defining carefully — now test what this dataset can actually tell you. 🎵😉

What is expected

You will work through the template alongside spotify_tracks_etf2121.csv. Complete both problems, then record a 5-minute briefing summarising what you found.

Who Written questions Recording
ETF2121 Q1–Q15 (skip Part B) Part C
ETF5912 Q1–Q18 Part C
Part What we mark Questions ETF2121 ETF5912
A Inspect, clean, plot, split, fit both models, read coefficients with intervals, and brief a manager Q1–Q15 90 90
B ETF5912 only: a regression tree for Problem 1 and a classification tree for Problem 2; 19 marks plus an optional 1-mark extension bonus, capped at 20 Q16–Q18 — 20
C One 5-minute group recording: a briefing of the two problems, not a tour of the code — 10 10
Total 100 120

ETF5912 groups, and mixed groups, are marked out of 120 because Part B is extra work. ETF2121 groups skip Part B and are marked out of 100.


The data 📂

The data lives in data/spotify_tracks_etf2121.csv, in the data/ folder next to the template. It is a sample of Spotify tracks curated for this unit, not the full catalogue, and each row represents one song.

Gildas Le Drogoff collected the original data in February 2026, and we added a simple genre column drawn from Maharshi Pandya’s list.

Three limitations of this data will matter — keep them in mind as you work, and return to them in Q15:

Column Meaning
track_id Spotify track id
track_name Song title
artist_name Main artist
album_name Album title
release_year Album release year (2015–2025 in this file)
popularity Track popularity, 0–100
artist_popularity Artist popularity, 0–100
explicit Explicit lyrics (TRUE / FALSE)
duration_min Length in minutes
danceability, energy, valence, acousticness, speechiness, instrumentalness, liveness Audio features, usually 0–1
loudness Loudness in dB (usually negative)
tempo Tempo in BPM
key Pitch class 0–11 (-1 if unknown)
mode 1 = major, 0 = minor
main_genre Genre group: pop, hip-hop, dance, rock, indie, latin, r-n-b, country, metal, other

The models use popularity (or is_top_song) as the outcome and these predictors: danceability, energy, valence, acousticness, speechiness, instrumentalness, loudness, tempo, explicit, main_genre, release_year. Leave liveness, artist_popularity, duration_min, key, and mode out of the models.

Use the same training set and test set for every model, including Part B.


Part A — Guided analysis 🧪

90 marks (Q1–Q15). Write your R in the named chunk under each part, then fill in the blanks using your own output.

Section 1: Inspect, then clean 🧹

Open the file, confirm what one row is, then clean it. A model built on duplicate songs or blank titles is not something you will want to brief to a manager.

Q1 — Open the file (4 marks: 2 + 2)

Import data/spotify_tracks_etf2121.csv and store it as raw.

Check: you have the right file

raw should have 20,128 rows and 21 columns. If not, you have the wrong file — fix Q1 before going on.

(a) (2 marks) How many rows and columns are there? What type is each of track_id, popularity, explicit, and main_genre? Is release_year stored as a number or a date?

Write your code in the q1a chunk, then record your answer below.

library(readr)
library(dplyr)
library(ggplot2)

# Import the file, then inspect its size and column types.
raw <- read_csv("spotify_tracks_etf2121.csv", show_col_types = FALSE)

n_rows <- nrow(raw)
n_cols <- ncol(raw)
class(raw$track_id)
## [1] "character"
class(raw$popularity)
## [1] "numeric"
class(raw$explicit)
## [1] "logical"
class(raw$main_genre)
## [1] "character"
class(raw$release_year)
## [1] "numeric"

Answer (a): 20128 rows, 21 columns. track_idcharacter. popularity numeric. explicit logical. main_genre character. release_year is numeric.

(b) (2 marks) Inspect the first eight rows. What is the first song (title and artist)? In one sentence, state what one row represents.

Write your code in the q1b chunk, then record your answer below.

head(raw, 8)
track_id track_name artist_name album_name release_year popularity artist_popularity explicit duration_min danceability energy key loudness mode speechiness acousticness instrumentalness liveness valence tempo main_genre
538gYspJYGYP5x7yWGkkb5 Truckin’ - Live Grateful Dead Arrows Of Neon (Live Autzen August 21,1993) 2023 1 67 FALSE 7.054700 0.489 0.7040 9 -10.143 1 0.0446 0.752000 0.009980 0.6000 0.735 128.016 rock
3AfKN7WWrw96DZH1ob9rMb Nightmare Unresolved Escape the Nightmare 2017 10 47 FALSE 3.573600 0.518 0.9930 1 -4.469 1 0.2430 0.129000 0.755000 0.0782 0.163 150.089 dance
6lNNnVhr3p29wo3z2QHwyR WALKING ON WATER Amir Obe after. 2023 1 46 FALSE 2.273683 0.269 0.6390 1 -8.271 1 0.0501 0.000722 0.003870 0.3250 0.125 190.230 r-n-b
5lHsjSxllbUbW91v12cTnt Swim Beneath My Skin Counterparts You’re Not You Anymore 2017 13 51 FALSE 2.911467 0.250 0.9940 0 -1.595 0 0.2030 0.001040 0.000000 0.5160 0.188 178.098 metal
3agZIM9npZeWJWEoBsEvaz Allegretto in C Minor, WoO 53 - Excerpt Ludwig van Beethoven “A Classical Winter”: The Piano 2025 11 73 FALSE 1.607550 0.557 0.0178 0 -27.481 0 0.0517 0.990000 0.911000 0.1220 0.160 105.154 other
3Ltjz9QblnRnzYekexV1By Toss A Coin To Your Witcher (From “The Witcher series”) [Lofi Beat] Celestial Alignment Chillhop Covers, Vol. 1 2020 19 43 FALSE 2.874700 0.797 0.0619 10 -24.808 0 0.2080 0.622000 0.886000 0.1300 0.336 100.105 other
6IleBCr2a59f47541Opgyg Banna Bannada Loka (From “Ekangi”) Shankar Mahadevan Sad Feeling Songs 2017 8 77 FALSE 5.146417 0.513 0.5810 4 -5.108 0 0.0301 0.695000 0.000155 0.1880 0.349 94.717 pop
5a5By7s1KIPqDYZyV8UrN9 Tchaikovsky: Violin Concerto in D Major, Op. 35: I. Allegro moderato, Pt. 3 Pyotr Ilyich Tchaikovsky Violin Concertos: Tchaikovsky, Bruch, Mozart, Prokofiev, Stravinsky, Lalo 2024 10 70 FALSE 1.999767 0.373 0.2050 9 -18.626 1 0.0448 0.928000 0.608000 0.5710 0.290 126.578 other

Answer (b): The first song is “Truckin’ - Live” by Grateful Dead. One row is a song record..

Q2 — Years, genres, and missing values (4 marks: 2 + 2)

(a) (2 marks) Report the smallest and largest release_year values and the number of distinct years. How many main_genre labels are there, and which label has the most rows?

Write your code in the q2a chunk, then record your answer below.

library(dplyr)
year_summary <- raw |>
  summarise(
    first_year = min(release_year),
    last_year = max(release_year),
    n_years = n_distinct(release_year)
  )

year_summary
first_year last_year n_years
2015 2025 11
genre_counts_raw <- raw |>
  count(main_genre) |>
  arrange(desc(n))

genre_counts_raw
main_genre n
other 9089
dance 4054
latin 1948
pop 1705
rock 1374
r-n-b 612
metal 390
indie 346
hip-hop 315
country 295
year_min <- year_summary$first_year
year_max <- year_summary$last_year
n_years <- year_summary$n_years

n_genres <- nrow(genre_counts_raw)
largest_genre <- genre_counts_raw$main_genre[1]
largest_genre_n <- genre_counts_raw$n[1]

Answer (a): Years 2015 to 2025 (11 years). 10 genre labels. Largest: other, 9089 rows.

(b) (2 marks) Which columns contain missing values, and how many missing values does each contain? Do any of the modelling columns listed above contain missing values? Yes / no.

Write your code in the q2b chunk, then record your answer below.

library(ggplot2)
model_columns <- c(
  "popularity",
  "danceability",
  "energy",
  "valence",
  "acousticness",
  "speechiness",
  "instrumentalness",
  "loudness",
  "tempo",
  "explicit",
  "main_genre",
  "release_year"
)

missing_counts <- colSums(is.na(raw))

missing_nonzero <- missing_counts[missing_counts > 0]
missing_nonzero
##  track_name artist_name 
##          25          25
model_missing <- sum(missing_counts[model_columns])
model_missing
## [1] 0

Answer (b): Missing columns and counts: ______. Missing values in modelling columns: yes / no.

Q3 — Clean the table (4 marks: 2 + 2)

The same track_id twice is the same song counted twice. A row with no title or artist cannot be named later.

Drop rows with a missing track_name or artist_name, keep one row per track_id, and save the result as tracks. From this point on, work only with tracks.

Check: tracks is ready

You should get 19,998 unique track_id values, 130 extra duplicate rows, 50 rows with a missing title or artist, and 19,998 songs remaining in tracks. If you do not, stop and fix Q3 before Q4.

(a) (2 marks) Using raw, report the number of unique track_id values, extra duplicate rows, rows with a missing title or artist, and songs remaining after cleaning.

Write your code in the q3a chunk, then record your answer below.

unique_ids <- n_distinct(raw$track_id)

extra_copies <- nrow(raw) - unique_ids

missing_name <- sum(
  is.na(raw$track_name) |
  is.na(raw$artist_name)
)

tracks <- raw |>
  filter(
    !is.na(track_name),
    !is.na(artist_name)
  ) |>
  distinct(
    track_id,
    .keep_all = TRUE
  )

n_clean <- nrow(tracks)

unique_ids
## [1] 19998
extra_copies
## [1] 130
missing_name
## [1] 50
n_clean
## [1] 19998

Answer (a): Unique ids ______. Extra copies ______. Missing name ______. After cleaning ______.

(b) (2 marks) Using raw, show one duplicated track_id and three rows with a missing title or artist. Does the duplicated ID refer to the same song? Yes / no.

Write your code in the q3b chunk, then record your answer below.

duplicate_id <- raw$track_id[
  which(duplicated(raw$track_id))[1]
]

dup_example <- raw |>
  filter(track_id == duplicate_id) |>
  select(
    track_id,
    track_name,
    artist_name
  )

dup_example
track_id track_name artist_name
72AxRjmhnkZ461Bm4wysYa Serenade for String Orchestra, Op. 48, TH 48: II. Walzer: Moderato - Tempo di Valse Pyotr Ilyich Tchaikovsky
72AxRjmhnkZ461Bm4wysYa Serenade for String Orchestra, Op. 48, TH 48: II. Walzer: Moderato - Tempo di Valse Pyotr Ilyich Tchaikovsky
missing_example <- raw |>
  filter(
    is.na(track_name) |
    is.na(artist_name)
  ) |>
  select(
    track_id,
    track_name,
    artist_name
  ) |>
  head(3)

missing_example
track_id track_name artist_name
4GmRnc9J8GBBkVHUtihE3Z Vuurwerk NA
4ZQMLMnq96EBgdJCiwChsP Música Mágica de Olas para Dormir pt. 4 NA
440HhZPx47InubN85le1JO NA Kaleem Taylor

Answer (b): Duplicated id ______. Same song: yes / no, because ______. The three missing-name rows shown are ______.

Q4 — Popularity, and why 70 is the wrong cutoff (6 marks: 4 + 2)

Spotify scores popularity from 0 to 100. Inexperienced data analysts might mistakenly treat 70 as “a hit”. That cutoff would be defensible only if a meaningful share of this file actually reached 70.

Use the cleaned data in tracks.

(a) (4 marks) Report the mean and median popularity. Create a histogram of song popularity and a bar chart showing the number of songs in each main_genre. Does the histogram show most songs clustering around 70, or mostly lower popularity scores? Which genre appears most often?

Write your code in the q4a chunk, then record your answer below.

pop_summary <- tracks |>
  summarise(
    mean_popularity = mean(popularity),
    median_popularity = median(popularity)
  )

pop_summary
mean_popularity median_popularity
13.9883 8
pop_mean <- pop_summary$mean_popularity
pop_median <- pop_summary$median_popularity

ggplot(
  tracks,
  aes(x = popularity)
) +
  geom_histogram(
    binwidth = 5,
    boundary = -0.5
  ) +
  labs(
    title = "Distribution of song popularity",
    x = "Popularity score",
    y = "Number of songs"
  ) +
  theme_minimal()

genre_counts <- tracks |>
  count(main_genre) |>
  arrange(desc(n))

genre_counts
main_genre n
other 9034
dance 4025
latin 1938
pop 1691
rock 1361
r-n-b 610
metal 387
indie 344
hip-hop 314
country 294
ggplot(
  genre_counts,
  aes(
    x = reorder(main_genre, n),
    y = n
  )
) +
  geom_col() +
  coord_flip() +
  labs(
    title = "Number of songs by genre",
    x = "Genre",
    y = "Number of songs"
  ) +
  theme_minimal()

largest_genre_clean <- genre_counts$main_genre[1]

Answer (a): Mean ______. Median ______. The mass of the histogram is ______. Largest genre: ______.

(b) (2 marks) What percentage of songs have popularity >= 70? In a few sentences, explain why 70 is an inappropriate yes/no cutoff for this dataset.

Write your code in the q4b chunk, then record your answer below.

share70 <- mean(
  tracks$popularity >= 70
)

share70_percent <- 100 * share70

share70_percent
## [1] 1.785179

Answer (b): Songs with popularity >= 70: ______%. A cutoff of 70 is inappropriate because ______.

Q5 — Do the features move together? (4 marks: 2 + 2)

In a linear model, each coefficient represents the impact of one variable while keeping all others constant, but this interpretation becomes difficult if two predictors are highly correlated and tend to change together.

(a) (2 marks) Calculate and display the correlation matrix for the numeric modelling predictors only (danceability, energy, valence, acousticness, speechiness, instrumentalness, loudness, tempo, release_year), excluding popularity. Round all correlations to two decimal places to assess potential multicollinearity between predictors.

Perfect copies versus noisy overlap

One of the assumptions of the linear model is that the predictors are not perfectly correlated with each other. Perfect multicollinearity is when one predictor is an exact duplicate of another. When predictors are highly correlated but not identical, the model can still fit, but the coefficient estimates become less precise: standard errors get larger and interpreting “holding the other fixed” becomes tricky.

Write your code in the q5a chunk, then record your answer below.

num_predictors <- c(
  "danceability",
  "energy",
  "valence",
  "acousticness",
  "speechiness",
  "instrumentalness",
  "loudness",
  "tempo",
  "release_year"
)

cor_matrix <- tracks |>
  select(all_of(num_predictors)) |>
  cor()

round(cor_matrix, 2)
##                  danceability energy valence acousticness speechiness
## danceability             1.00   0.35    0.55        -0.39        0.21
## energy                   0.35   1.00    0.35        -0.77        0.11
## valence                  0.55   0.35    1.00        -0.22        0.12
## acousticness            -0.39  -0.77   -0.22         1.00       -0.09
## speechiness              0.21   0.11    0.12        -0.09        1.00
## instrumentalness        -0.36  -0.36   -0.39         0.32       -0.19
## loudness                 0.50   0.79    0.40        -0.66        0.09
## tempo                    0.10   0.28    0.14        -0.25        0.03
## release_year             0.04  -0.03    0.01         0.03        0.01
##                  instrumentalness loudness tempo release_year
## danceability                -0.36     0.50  0.10         0.04
## energy                      -0.36     0.79  0.28        -0.03
## valence                     -0.39     0.40  0.14         0.01
## acousticness                 0.32    -0.66 -0.25         0.03
## speechiness                 -0.19     0.09  0.03         0.01
## instrumentalness             1.00    -0.58 -0.13         0.02
## loudness                    -0.58     1.00  0.27        -0.02
## tempo                       -0.13     0.27  1.00         0.00
## release_year                 0.02    -0.02  0.00         1.00
largest_abs_cor <- max(
  abs(cor_matrix[upper.tri(cor_matrix)])
)

largest_abs_cor
## [1] 0.7864584

Answer (a): The displayed matrix is the nine predictors (no popularity). The largest |r| among them is ______. That is high / not perfect, so the slopes would be ______.

(b) (2 marks) Which pair of predictors has the largest absolute correlation?

Write your code in the q5b chunk, then record your answer below.

pairs <- which(
  upper.tri(cor_matrix),
  arr.ind = TRUE
)

best_pair <- pairs[
  which.max(
    abs(cor_matrix[pairs])
  ),
]

pair_names <- colnames(cor_matrix)[best_pair]

pair_r <- cor_matrix[
  best_pair[1],
  best_pair[2]
]

pair_names
## [1] "energy"   "loudness"
pair_r
## [1] 0.7864584

Answer (b): Among predictors, ______ and ______ have the largest absolute correlation (r = ______).

Q6 — Define “top song”, then split (6 marks: 2 + 4)

Call a song a top song if it sits in the top 10% of tracks — at or above the 90th percentile of popularity.

Check before you model

top_cut should be 34. After set.seed(2121) and a stratified 70/30 split, train should have 13,997 rows and test 6,001 rows, with about 10.5% top songs in each. If you do not get these, stop and fix Q6 before Q7.

(a) (2 marks) Calculate and store the 90th percentile of popularity as top_cut, then define is_top_song as popularity >= top_cut. What is the value of top_cut?

Write your code in the q6a chunk, then record your answer below.

top_cut <- quantile(
  tracks$popularity,
  probs = 0.90
) |>
  unname()

tracks <- tracks |>
  mutate(
    is_top_song = popularity >= top_cut,
    main_genre = factor(main_genre),
    explicit = factor(
      explicit,
      levels = c(FALSE, TRUE)
    )
  )

top_cut
## [1] 34
tracks |>
  count(is_top_song)
is_top_song n
FALSE 17891
TRUE 2107

Answer (a): top_cut = ______.

(b) (4 marks) Create a reproducible 70% training set and 30% test set, stratified by is_top_song. Run set.seed(2121) immediately before sampling, and store the sets as train and test. Report the number of rows and the top-song proportion in each set. Reuse this exact split for every subsequent model, including Part B; do not split the data again.

Write your code in the q6b chunk, then record your answer below.

non_top_rows <- which(
  !tracks$is_top_song
)

top_rows <- which(
  tracks$is_top_song
)

set.seed(2121)

non_top_train <- sample(
  non_top_rows,
  floor(
    0.70 * length(non_top_rows)
  )
)

top_train <- sample(
  top_rows,
  floor(
    0.70 * length(top_rows)
  )
)

train_idx <- c(
  non_top_train,
  top_train
)

train <- tracks[
  train_idx,
]

test <- tracks[
  -train_idx,
]

train_share <- mean(
  train$is_top_song
)

test_share <- mean(
  test$is_top_song
)

train |>
  summarise(
    n_songs = n(),
    top_share = mean(is_top_song)
  )
n_songs top_share
13997 0.1053083
test |>
  summarise(
    n_songs = n(),
    top_share = mean(is_top_song)
  )
n_songs top_share
6001 0.1054824

Answer (b): Training set: ______ rows, ______ top-song share. Test set: ______ rows, ______ top-song share. I will reuse these exact objects because ______.

Now you have a training set and a test set. Fit models on the training set and evaluate them on the test set.

Block 1 — Linear regression (Problem 1) 📈

Predict a 0–100 popularity score. First ask whether the line is better than no model at all.

Unless a later question tells you to drop a variable, use:

popularity ~ danceability + energy + valence + acousticness + speechiness + instrumentalness + loudness + tempo + explicit + main_genre + release_year

Fit the model on the training set. Check the model on the test set.

Q7 — How good is the model? Is it better than no model? (8 marks: 2 + 4 + 2)

A manager will want to know if including the audio features actually improves predictions. Here, no model means always predicting the training-set mean popularity for every song.

(a) (2 marks) Establish a no-model baseline: report the mean training-set popularity, use that value as the prediction for every test-set song, and calculate the resulting test-set RMSE. Comment on the size of this error on a 0–100 scale.

Write your code in the q7a chunk, then record your answer below.

baseline_mean <- mean(train$popularity)

baseline_pred <- rep(baseline_mean, nrow(test))

baseline_rmse <- sqrt(
  mean((test$popularity - baseline_pred)^2)
)

baseline_mean
## [1] 13.95642
baseline_rmse
## [1] 15.32129

Answer (a): Mean 13.96. No-model test set RMSE 15.32. How large: This is a fairly large error on a 0 to 100 popularity scale.

(b) (4 marks) Fit the linear model on the training set and print its summary. Report the test-set RMSE and test-set \(R^2\). From the last line of summary(), report the F-statistic and its p-value. In one sentence, state whether the audio-feature model as a whole beats predicting the mean.

Write your code in the q7b chunk, then record your answer below.

linear_full <- lm(
  popularity ~ danceability + energy + valence + acousticness +
    speechiness + instrumentalness + loudness + tempo +
    explicit + main_genre + release_year,
  data = train
)

summary(linear_full)
## 
## Call:
## lm(formula = popularity ~ danceability + energy + valence + acousticness + 
##     speechiness + instrumentalness + loudness + tempo + explicit + 
##     main_genre + release_year, data = train)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -30.672  -9.707  -3.865   5.833  80.915 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)       -8.622e+02  7.822e+01 -11.024  < 2e-16 ***
## danceability       5.646e+00  8.951e-01   6.308 2.92e-10 ***
## energy            -4.066e+00  8.844e-01  -4.597 4.32e-06 ***
## valence           -5.256e+00  6.010e-01  -8.745  < 2e-16 ***
## acousticness      -1.736e+00  5.690e-01  -3.051  0.00229 ** 
## speechiness       -5.498e+00  1.144e+00  -4.807 1.55e-06 ***
## instrumentalness  -2.971e+00  4.408e-01  -6.739 1.66e-11 ***
## loudness           4.042e-01  3.386e-02  11.939  < 2e-16 ***
## tempo             -2.412e-03  4.297e-03  -0.561  0.57456    
## explicitTRUE       4.775e+00  4.241e-01  11.257  < 2e-16 ***
## main_genredance   -5.831e+00  1.092e+00  -5.339 9.48e-08 ***
## main_genrehip-hop  2.834e+00  1.438e+00   1.970  0.04881 *  
## main_genreindie   -2.061e-01  1.411e+00  -0.146  0.88390    
## main_genrelatin   -1.234e+00  1.119e+00  -1.103  0.27002    
## main_genremetal   -2.866e+00  1.424e+00  -2.012  0.04423 *  
## main_genreother   -5.139e+00  1.071e+00  -4.800 1.61e-06 ***
## main_genrepop     -1.173e+00  1.129e+00  -1.039  0.29876    
## main_genrer-n-b   -3.165e+00  1.267e+00  -2.498  0.01249 *  
## main_genrerock    -3.726e+00  1.146e+00  -3.252  0.00115 ** 
## release_year       4.394e-01  3.873e-02  11.345  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 14.43 on 13977 degrees of freedom
## Multiple R-squared:  0.1076, Adjusted R-squared:  0.1064 
## F-statistic: 88.72 on 19 and 13977 DF,  p-value: < 2.2e-16
pred_linear_full <- predict(linear_full, newdata = test)

linear_full_rmse <- sqrt(
  mean((test$popularity - pred_linear_full)^2)
)

linear_full_r2 <- 1 -
  sum((test$popularity - pred_linear_full)^2) /
  sum((test$popularity - mean(test$popularity))^2)

f_stats <- summary(linear_full)$fstatistic
f_value <- unname(f_stats["value"])
f_p <- pf(
  f_stats["value"],
  f_stats["numdf"],
  f_stats["dendf"],
  lower.tail = FALSE
)

rmse_improvement <- baseline_rmse - linear_full_rmse

linear_full_rmse
## [1] 14.55398
linear_full_r2
## [1] 0.09761048
f_value
## [1] 88.71749
f_p
## value 
##     0

Answer (b): Test set RMSE 14.55. Test set \(R^2\) 0.098. F = 88.72. p <2e-16. The bundle does beat the mean. How good: the model has limited predictive power because it explains only about 9.8% of the variation in popularity on unseen songs.

(c) (2 marks) Compare the linear model’s test-set RMSE with the no-model baseline’s test-set RMSE. In two sentences, state whether the model is better and whether the improvement is large enough to matter on a 0–100 scale.

Answer (c): The linear model does beat the baseline: RMSE changes from 15.32 to 14.55, an improvement of 0.77 popularity points. This is not large enough to matter because the reduction in prediction error is less than one popularity point on a 0 to 100 scale.

Q8 — Energy and loudness overlap (4 marks: 2 + 2)

You already saw that overlap in Q5. Keeping both makes each coefficient “holding the other fixed,” which is hard to brief.

(a) (2 marks) Using the model from Q7, report the coefficients for energy and loudness. Do they have the same sign? Yes / no.

Write your code in the q8a chunk, then record your answer below.

energy_full <- coef(linear_full)["energy"]
loudness_full <- coef(linear_full)["loudness"]

same_sign <- sign(energy_full) == sign(loudness_full)

energy_full
##    energy 
## -4.065976
loudness_full
##  loudness 
## 0.4042363
same_sign
## energy 
##  FALSE

Answer (a): Energy -4.066. Loudness 0.404. Same sign: no.

(b) (2 marks) Remove loudness and refit the model. Report the energy coefficient and the new training-set adjusted \(R^2\). In one sentence, identify the model you will use from this point (the Q7 model or the model without loudness) and justify your choice. A nearly unchanged adjusted \(R^2\) is a valid reason. Use that chosen model in Q9–Q11.

Write your code in the q8b chunk, then record your answer below.

linear_reduced <- lm(
  popularity ~ danceability + energy + valence + acousticness +
    speechiness + instrumentalness + tempo +
    explicit + main_genre + release_year,
  data = train
)

summary(linear_reduced)
## 
## Call:
## lm(formula = popularity ~ danceability + energy + valence + acousticness + 
##     speechiness + instrumentalness + tempo + explicit + main_genre + 
##     release_year, data = train)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -29.957  -9.625  -4.046   5.829  81.950 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)       -8.602e+02  7.861e+01 -10.942  < 2e-16 ***
## danceability       8.536e+00  8.661e-01   9.856  < 2e-16 ***
## energy             1.905e+00  7.331e-01   2.599 0.009367 ** 
## valence           -5.582e+00  6.034e-01  -9.251  < 2e-16 ***
## acousticness      -1.818e+00  5.719e-01  -3.180 0.001478 ** 
## speechiness       -7.242e+00  1.140e+00  -6.352 2.19e-10 ***
## instrumentalness  -5.247e+00  3.994e-01 -13.137  < 2e-16 ***
## tempo              1.766e-03  4.305e-03   0.410 0.681573    
## explicitTRUE       4.872e+00  4.262e-01  11.432  < 2e-16 ***
## main_genredance   -5.917e+00  1.098e+00  -5.391 7.11e-08 ***
## main_genrehip-hop  2.688e+00  1.445e+00   1.860 0.062918 .  
## main_genreindie   -8.409e-02  1.418e+00  -0.059 0.952723    
## main_genrelatin   -1.302e+00  1.124e+00  -1.158 0.247006    
## main_genremetal   -2.609e+00  1.431e+00  -1.823 0.068337 .  
## main_genreother   -5.563e+00  1.075e+00  -5.173 2.33e-07 ***
## main_genrepop     -1.147e+00  1.134e+00  -1.011 0.311933    
## main_genrer-n-b   -2.938e+00  1.273e+00  -2.308 0.021020 *  
## main_genrerock    -3.820e+00  1.152e+00  -3.317 0.000912 ***
## release_year       4.341e-01  3.892e-02  11.154  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 14.5 on 13978 degrees of freedom
## Multiple R-squared:  0.09852,    Adjusted R-squared:  0.09736 
## F-statistic: 84.87 on 18 and 13978 DF,  p-value: < 2.2e-16
energy_reduced <- coef(linear_reduced)["energy"]
adj_r2_full <- summary(linear_full)$adj.r.squared
adj_r2_reduced <- summary(linear_reduced)$adj.r.squared
adj_r2_change <- adj_r2_reduced - adj_r2_full

pred_linear_reduced <- predict(linear_reduced, newdata = test)

linear_reduced_rmse <- sqrt(
  mean((test$popularity - pred_linear_reduced)^2)
)

energy_reduced
##   energy 
## 1.905216
adj_r2_reduced
## [1] 0.09735903
adj_r2_change
## [1] -0.00904916

Answer (b): I dropped loudness. Energy coefficient 1.905. Training set adjusted \(R^2\) 0.097. I will use the model without loudness because removing loudness changes adjusted R squared by only 0.009, while making the energy coefficient easier to interpret.

Q9 — Where the model is weak (4 marks: 3 + 1)

Use the model you chose in Q8.

(a) (3 marks) Create a residuals-versus-fitted plot using the training set. Then combine the training-set fitted values with the test-set predictions into one set of predicted popularity scores. Report the minimum and maximum of that combined set, and count how many values fall below 0 or exceed 100. Explain the residual pattern in a few sentences, including whether the spread stays even or fans out.

Write your code in the q9a chunk, then record your answer below.

residual_data <- data.frame(
  fitted = fitted(linear_reduced),
  residual = resid(linear_reduced)
)

ggplot(residual_data, aes(x = fitted, y = residual)) +
  geom_point(alpha = 0.25) +
  geom_hline(yintercept = 0, linetype = "dashed") +
  labs(
    title = "Residuals versus fitted values",
    x = "Fitted popularity",
    y = "Residual"
  ) +
  theme_minimal()

combined_predictions <- c(
  fitted(linear_reduced),
  pred_linear_reduced
)

prediction_min <- min(combined_predictions)
prediction_max <- max(combined_predictions)
below_zero <- sum(combined_predictions < 0)
above_100 <- sum(combined_predictions > 100)

prediction_min
## [1] -0.1800283
prediction_max
## [1] 32.92345
below_zero
## [1] 1
above_100
## [1] 0

Answer (a): Combined prediction range -0.18 to 32.92. Below 0: 1. Above 100: 0. Spread: fans out. The residual spread becomes wider as fitted popularity increases, suggesting that prediction errors are less consistent for songs with higher predicted popularity. The model also underpredicts some highly popular songs, as shown by the large positive residuals.

(b) (1 mark) In two sentences, state whether the line is equally trustworthy for ordinary songs and songs that already sound like hits, and what you would not promise a manager.

Answer (b): The model is more trustworthy for ordinary songs because the residuals are relatively smaller and more concentrated in that range. I would not promise a manager that the model can reliably predict hit songs or extreme popularity values.

Q10 — Inference: coefficients, intervals, and p-values (10 marks: 3 + 4 + 3)

Use the model you chose in Q8 (without loudness). confint() gives 95% intervals.

(a) (3 marks) Use confint() to report the 95% confidence intervals for energy and danceability. Does each interval exclude 0? Yes / no.

Write your code in the q10a chunk, then record your answer below.

ci_reduced <- confint(
  linear_reduced,
  c("energy", "danceability"),
  level = 0.95
)

energy_ci <- ci_reduced["energy", ]
danceability_ci <- ci_reduced["danceability", ]

energy_excludes_zero <-
  energy_ci[1] > 0 | energy_ci[2] < 0

dance_excludes_zero <-
  danceability_ci[1] > 0 | danceability_ci[2] < 0

energy_ci
##     2.5 %    97.5 % 
## 0.4681774 3.3422537
danceability_ci
##     2.5 %    97.5 % 
##  6.838207 10.233391

Answer (a): Energy interval 0.468 to 3.342. Excludes 0: yes. Danceability interval 6.838 to 10.233. Excludes 0: yes.

(b) (4 marks) In a few sentences, interpret the energy coefficient in popularity points. State what is held constant, include a practically meaningful change in energy (for example, a 0.1 increase), and include the 95% confidence interval from (a).

Write your code in the q10b chunk, then record your answer below.

energy_effect_01 <- 0.1 * energy_reduced

energy_p <- summary(linear_reduced)$coefficients[
  "energy",
  "Pr(>|t|)"
]

energy_effect_01
##    energy 
## 0.1905216
energy_p
## [1] 0.009366698

Answer (b): Holding the other predictors fixed, a one-unit increase in energy is associated with 1.905 popularity points on average (95% interval 0.468 to 3.342). A 0.1 increase corresponds to about 0.191 popularity points. This is an association. There is no evidence that increasing energy causes an increase in popularity.

(c) (3 marks) In two or three sentences, choose one feature with a small p-value. Report its coefficient and p-value. Explain whether its effect is large enough to act on, or merely statistically detectable because the dataset is large.

Answer (c): For energy, coefficient = 1.905 and p = 0.009. The effect is not large enough to act on because a realistic 0.1 increase in energy is associated with only about 0.191 popularity points on a 0 to 100 scale.

Q11 — Brief Problem 1 (8 marks; about 150 words)

Write a business brief for your manager.

  • Follow this structure:
    1. State the business problem: predicting a 0–100 popularity score, and what a “no model” (guessing the mean) achieves.
    2. Compare model performance to the baseline: test set RMSE for the average, test set RMSE for the linear model, and the size of the improvement.
    3. Briefly explain what a typical coefficient (e.g., energy) means in practical terms, including the confidence interval.
    4. Say whether adjusting one audio feature would make a big difference to popularity, or just a minor change.
    5. Point out the model’s limitations (can predict values outside 0–100 and is weakest for extreme tracks).
    6. Summarise with a recommendation: Is the model reliable for everyday forecasting? Should a manager use it for predicting hits?
  • About 150 words is enough. Focus on actionable conclusions and what the results mean for decision making, not on statistical significance or model fitting details.

Write here (8 marks; about 150 words): Our business problem is to predict song popularity on a 0 to 100 scale. The no model baseline predicts the training mean of 13.96 and has a test set RMSE of 15.32. The reduced linear model has a test set RMSE of 14.61, an improvement of 0.71 popularity points. Holding the other predictors fixed, a one unit increase in energy is associated with 1.905 popularity points, with a 95% interval from 0.468 to 3.342. A realistic 0.1 increase in energy corresponds to only 0.191 popularity points, so changing one audio feature is likely to make only a minor practical difference. The model can predict values outside the 0 to 100 range and is less reliable for extreme songs. I recommend using it for rough everyday forecasting, but not for predicting hits.

Block 2 — Logistic regression (Problem 2) ✅

Use the same training set and test set. The outcome is now is_top_song. For the logistic model, use the original Q7 predictor list (including loudness), not the reduced Q8 linear model. The model gives P(top song). You choose the cutoff that turns that probability into yes or no.

Q12 — Predicting top songs (6 marks: 2 + 4)

About 10% of songs are top songs, so “always say no” is already about 90% accurate. However, it finds nothing.

(a) (2 marks) Report the proportion of top songs in the training and test sets and the test-set accuracy obtained by always predicting “not a top song”. Briefly explain why this accuracy appears impressive but is unhelpful.

Write your code in the q12a chunk, then record your answer below.

train_top_share <- mean(train$is_top_song)
test_top_share <- mean(test$is_top_song)

always_no_accuracy <- mean(!test$is_top_song)

train_top_share
## [1] 0.1053083
test_top_share
## [1] 0.1054824
always_no_accuracy
## [1] 0.8945176

Answer (a): Training top song share 10.53%; test top song share 10.55%. Always predicting “not top” gives test accuracy 89.45%, which looks impressive because most songs are not top songs but is unhelpful because it identifies no actual top songs.

(b) (4 marks) Fit a logistic regression to the training set (family = binomial) using is_top_song as the outcome and the same predictors as Q7 (including loudness). Print a brief summary. On the test set, classify a song as top when its predicted probability is at least 0.5. Report the four confusion-matrix counts (TN, FP, FN, TP), accuracy, sensitivity, and specificity. Here, sensitivity is the proportion of actual top songs correctly identified, and specificity is the proportion of actual non-top songs correctly identified. Identify the false-negative cell (a missed top song) and the false-positive cell (a false yes).

Write your code in the q12b chunk, then record your answer below.

logit_full <- glm(
  is_top_song ~ danceability + energy + valence + acousticness +
    speechiness + instrumentalness + loudness + tempo +
    explicit + main_genre + release_year,
  data = train,
  family = binomial
)

summary(logit_full)
## 
## Call:
## glm(formula = is_top_song ~ danceability + energy + valence + 
##     acousticness + speechiness + instrumentalness + loudness + 
##     tempo + explicit + main_genre + release_year, family = binomial, 
##     data = train)
## 
## Coefficients:
##                     Estimate Std. Error z value Pr(>|z|)    
## (Intercept)       -1.591e+02  1.876e+01  -8.483  < 2e-16 ***
## danceability       7.934e-01  2.157e-01   3.679 0.000234 ***
## energy            -1.246e+00  2.422e-01  -5.145 2.68e-07 ***
## valence           -7.821e-01  1.429e-01  -5.472 4.45e-08 ***
## acousticness      -1.067e-01  1.354e-01  -0.788 0.430847    
## speechiness       -6.932e-01  2.917e-01  -2.377 0.017470 *  
## instrumentalness  -5.999e-01  1.297e-01  -4.625 3.75e-06 ***
## loudness           1.251e-01  1.218e-02  10.270  < 2e-16 ***
## tempo             -2.586e-04  1.025e-03  -0.252 0.800930    
## explicitTRUE       5.809e-01  8.160e-02   7.118 1.09e-12 ***
## main_genredance   -6.555e-01  2.060e-01  -3.182 0.001460 ** 
## main_genrehip-hop  2.250e-01  2.493e-01   0.903 0.366601    
## main_genreindie    7.034e-02  2.597e-01   0.271 0.786474    
## main_genrelatin   -1.670e-01  2.081e-01  -0.803 0.422213    
## main_genremetal   -5.941e-01  2.980e-01  -1.994 0.046202 *  
## main_genreother   -7.270e-01  2.010e-01  -3.618 0.000297 ***
## main_genrepop     -8.957e-02  2.090e-01  -0.428 0.668302    
## main_genrer-n-b   -4.934e-01  2.382e-01  -2.071 0.038317 *  
## main_genrerock    -3.174e-01  2.166e-01  -1.466 0.142762    
## release_year       7.887e-02  9.285e-03   8.494  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 9422.6  on 13996  degrees of freedom
## Residual deviance: 8599.6  on 13977  degrees of freedom
## AIC: 8639.6
## 
## Number of Fisher Scoring iterations: 6
logit_prob_05 <- predict(
  logit_full,
  newdata = test,
  type = "response"
)

logit_pred_05 <- logit_prob_05 >= 0.5

actual_top <- test$is_top_song

TN_05 <- sum(!actual_top & !logit_pred_05)
FP_05 <- sum(!actual_top & logit_pred_05)
FN_05 <- sum(actual_top & !logit_pred_05)
TP_05 <- sum(actual_top & logit_pred_05)

accuracy_05 <- (TN_05 + TP_05) / nrow(test)
sensitivity_05 <- TP_05 / (TP_05 + FN_05)
specificity_05 <- TN_05 / (TN_05 + FP_05)

table(
  Actual = actual_top,
  Predicted = logit_pred_05
)
##        Predicted
## Actual  FALSE TRUE
##   FALSE  5368    0
##   TRUE    632    1
TN_05
## [1] 5368
FP_05
## [1] 0
FN_05
## [1] 632
TP_05
## [1] 1
accuracy_05
## [1] 0.8946842
sensitivity_05
## [1] 0.001579779
specificity_05
## [1] 1

Answer (b): TN 5368. FP 0. FN 632. TP 1. Accuracy 89.47%. Sensitivity 0.16%. Specificity 100%. FN is a missed top song. FP is a false yes.

Q13 — A better cutoff, and one named song (6 marks: 4 + 2)

Accuracy can sit next to the Q12 baseline while sensitivity stays near zero. That is not a coding error. Decide which mistake is worse: missing a top song, or saying yes too often. Do not hunt through many cutoffs on the test set.

(a) (4 marks) Choose a probability cutoff other than 0.5, based on business reasoning and, if useful, training-set results. Do not select or tune the cutoff using test-set performance. Justify the cutoff in one sentence, then apply it once to the test set. Report the four confusion-matrix counts (TN, FP, FN, TP), accuracy, sensitivity, and specificity.

Write your code in the q13a chunk, then record your answer below.

chosen_cutoff <- 0.30

logit_pred_new <- logit_prob_05 >= chosen_cutoff

TN_new <- sum(!actual_top & !logit_pred_new)
FP_new <- sum(!actual_top & logit_pred_new)
FN_new <- sum(actual_top & !logit_pred_new)
TP_new <- sum(actual_top & logit_pred_new)

accuracy_new <- (TN_new + TP_new) / nrow(test)
sensitivity_new <- TP_new / (TP_new + FN_new)
specificity_new <- TN_new / (TN_new + FP_new)

TN_new
## [1] 5297
FP_new
## [1] 71
FN_new
## [1] 593
TP_new
## [1] 40
accuracy_new
## [1] 0.8893518
sensitivity_new
## [1] 0.06319115
specificity_new
## [1] 0.9867735

Answer (a): Cutoff 0.3. Reason: I chose a lower cutoff because missing a genuine top song is more costly than generating some false positive recommendations, so the decision prioritises sensitivity. TN 5297. FP 71. FN 593. TP 40. Accuracy 88.94%. Sensitivity 6.32%. Specificity 98.68%.

(b) (2 marks) From the test set, select the track with track_name “Que Malo” and artist_name “Bad Bunny”. Report its modelled probability of being a top song, compare that probability with your Q13(a) cutoff, and state the resulting yes/no decision. In two sentences, explain why the decision follows from your chosen cutoff; do not judge it against 0.5 unless 0.5 is your chosen cutoff.

Write your code in the q13b chunk, then record your answer below.

que_malo <- test |>
  filter(
    track_name == "Que Malo",
    artist_name == "Bad Bunny"
  )

que_malo_prob <- predict(
  logit_full,
  newdata = que_malo,
  type = "response"
)

que_malo_decision <- que_malo_prob >= chosen_cutoff

que_malo_prob
##         1 
## 0.2796899
que_malo_decision
##     1 
## FALSE

Answer (b): Song: Que Malo by Bad Bunny. P = 0.28. Cutoff 0.3. Decision: no.

Q14 — Inference for the energy coefficient (8 marks: 2 + 6)

(a) (2 marks) From the Q12 logistic model, report the energy coefficient and its 95% confidence interval from confint(). Does the interval exclude 0? Yes / no.

Write your code in the q14a chunk, then record your answer below.

# Write your R here.

Answer (a): Energy coefficient ______. 95% interval ______ to ______. Excludes 0: yes / no.

(b) (6 marks) Interpret the sign and magnitude of the energy coefficient, holding the other variables fixed. Explain that a logistic coefficient changes the log-odds, or convert it to an odds ratio. Illustrate with a 0.1 increase in energy. Do not describe the coefficient as a constant change in probability.

Write your code in the q14b chunk, then record your answer below.

# Write your R here.

Answer (b): Holding other predictors fixed, the energy coefficient is ______, so a one-unit increase reduces the log-odds by ______ on average (odds ratio ______). A 0.1 increase in energy ______. This is not a constant probability change because ______.

Q15 — Which model, and what this file cannot tell you (8 marks: 4 + 4)

(a) (4 marks) In two short paragraphs, explain which model you would use for “How popular is this song?” (0–100) and which you would use for “Is it a top song?” (yes/no). Explain why these questions require different answers.

Write (a) here: For the 0–100 question, I would use ______ because ______. For the yes/no top-song question, I would use ______ with cutoff ______ because ______. These answers differ because ______.

(b) (4 marks) In four or five sentences, explain the limitations of the file and identify one question about song popularity that this extract cannot answer. Include the sources below in a reference list.

Le Drogoff, G. (2026). Spotify Huge Track Analysis Dataset. Hugging Face. https://huggingface.co/datasets/GildasLeDrogoff/spotify-huge-track-analysis-dataset

Pandya, M. (2022). Spotify Tracks Dataset. https://www.kaggle.com/datasets/maharshipandya/-spotify-tracks-dataset

Spotify for Developers. (2024, 27 November). Introducing some changes to our Web API. https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api

Write 200–250 words in total across parts (a) and (b). The reference list does not count towards the word guide.

Write (b) here: The file is limited by ______. One question this extract cannot answer is ______.

References: Include all three sources listed above in your submitted answer.


Part B — ETF5912 only: an extra taught method 🌳

Part B is capped at 20 marks. Q16–Q18 contain 19 assessed marks, plus an optional 1-mark extension bonus. Without the optional extension, the maximum available for Part B is 19/20. If every group member is in ETF2121, skip this part and go straight to Part C.

Fit trees on the same predictors and the same training set and test set. This is not a third business problem — only whether a more flexible method is worth abandoning the simpler model.

Q16 — Can a tree predict popularity better? (8 marks: 2 + 6)

A regression tree can bend where a line cannot. It does not give you a Q10-style coefficient.

(a) (2 marks) Fit a regression tree for popularity on the training set using the same predictors as Q7 and rpart(..., method = "anova"). Use the default rpart settings; do not tune the tree on the test set. Print the tree or printcp().

Write your code in the q16a chunk, then record your answer below.

# Write your R here.

Answer (a): Fitted with method = "anova": yes / no. First split or tree structure: ______.

(b) (6 marks) Calculate the test-set RMSE and compare it with the Q7 no-model baseline and the Q8 linear model. In two or three sentences, assess whether any improvement on the 0–100 scale is worth losing the interpretability of the coefficients.

Write your code in the q16b chunk, then record your answer below.

# Write your R here.

Answer (b): Tree test-set RMSE ______. Linear-model RMSE ______. Baseline RMSE ______. I would / would not keep the tree because ______.

Q17 — Can a tree find top songs better? (8 marks: 2 + 6)

(a) (2 marks) Fit a classification tree for is_top_song on the training set using the same predictors as Q12 and rpart(..., method = "class"). Keep the default rpart settings; do not set cp or minsplit. Print the tree, printcp(), and the class predictions. Does the default tree ever predict “yes”?

Write your code in the q17a chunk, then record your answer below.

# Write your R here.

Answer (a): The default tree does / does not predict any top songs. Evidence: ______.

(b) (6 marks) On the test set, report the four confusion-matrix counts (TN, FP, FN, TP), accuracy, sensitivity, and specificity. If the tree gives probabilities, apply the same cutoff as Q13(a). Sensitivity is the share of actual top songs found; specificity is the share of actual non-top songs correctly rejected. Compare the tree with Q12/Q13 and, in two sentences, state which error you would accept — missed top songs or false yeses — and why.

Write your code in the q17b chunk, then record your answer below.

# Write your R here.

Answer (b): TN ______. FP ______. FN ______. TP ______. Accuracy ______. Sensitivity ______. Specificity ______. Compared with Q13, ______. I would accept ______ because ______.

Q18 — Two problems, two choices (3 marks: 2 + 1)

(a) (2 marks) For Problem 1 (0–100), which method is better for a manager to use? Justify your choice with test-set RMSE and interpretability.

Answer (a): I choose ______. Its test-set RMSE is ______ versus ______ for ______, and its managerial advantage is ______.

(b) (1 mark) For Problem 2 (yes/no), which method is better for a manager to use? Justify your choice with the error you are willing to accept.

Answer (b): I choose ______ because ______.

One method this unit did not teach (1 bonus mark)

Name one method not taught in this unit (for example, random forest or lasso). In about 80–120 words, explain what it could add to Problem 1 or Problem 2 and whether you would use it.

# Optional.

Answer: Method ______. It could add ______ to Problem 1 / Problem 2. I would / would not use it because ______.


Part C — 5-minute recording (10 marks) 🎥

10 marks for both ETF2121 and ETF5912.

Submit one recording per group, no longer than 5 minutes, due at the same time as the knit (23 October 2026, 11.55pm). Think of this as a briefing, not a live tutorial presentation.

Speak as if a Spotify manager has given you five minutes. Your recording will be assessed using these three criteria:

  1. Evidence and findings (4 marks). State the two tasks: predicting how popular a song is and deciding whether it is a top song. For Problem 1, compare the linear model with the Q7 “always guess the average” baseline; give the Q10 coefficient and its interval; include the Q9 warning that predictions can fall outside 0–100; and state your Q11 verdict. For Problem 2, give the top-song cutoff (not simply 70), your probability cutoff, the error you are willing to accept, and one named song. Include one limitation from Q15 and one question about song popularity that this extract cannot answer.
  2. Manager briefing (3 marks). Present a clear, accurate, and professionally structured briefing for a Spotify manager. Explain what the evidence means for a decision. Do not explain R code or simply read your written answers aloud. ETF5912 groups may briefly state whether the extra method changed their recommendation.
  3. Recording requirements and member participation (3 marks). Submit one recording of no more than 5 minutes in which every group member listed on the cover speaks. Use clear audio and a playable .mp4 or .mov, or provide a working link through Monash OneDrive, Google Drive, or unlisted YouTube. The marker must be able to open a shared link without requesting access.

A missing, locked, or unplayable recording scores 0 for Part C.

Required recording link or uploaded file name: ________________________________________________


Policy 📌

The assignment files, recording, and FeedbackFruits group member evaluation are due Friday 23 October 2026, 11.55pm. Deadlines leave time for marking and feedback, and there is no extension on this group assignment under Monash policy. Everyone listed on the assignment should contribute; FeedbackFruits results will be used to assess each member’s contribution and may change that student’s individual mark.