Spotify Audio Features Analysis 1921–2020

Введение

Проект исследует эволюцию аудио‑характеристик треков Spotify за столетие (1920‑е—2020‑е). Используют данные из Kaggle (Kaggle >> Spotify Tracks Dataset.).

Задачи:

  1. Загрузить и очистить данные tracks.csv из Kaggle.

  2. Сформировать новые признаки (год, десятилетие, отношения фич, бины темпа).

  3. Провести разведочный анализ и агрегации.

  4. Построить информативные визуализации трендов.

  5. Оценить корреляции аудио‑фич с популярностью.

  6. Построить модель оценки популярности “трека”.

Загрузка и объединение данных

library(tidyverse)
library(lubridate)
library(stringr)
library(janitor)
library(skimr)
library(gt)
library(tidymodels)

# Пути к файлам
tracks_path  <- "data/tracks.csv"

tracks <- read_csv(tracks_path) |>
  clean_names() |>
  # Извлечение отдельного ID артиста из строкового поля и парсинг года релиза
  mutate(
    artist_id    = str_extract(id_artists, "[A-Za-z0-9]{22}"),  # Берём первые 22 символа
    release_year = as.integer(str_sub(release_date, 1, 4))      # Год — первые 4 знака
  )

Предобработка и признаки

spotify <- tracks |>
  # Приведение ключевых столбцов к числовому типу
  mutate(across(c(danceability, energy, loudness, tempo, valence), as.numeric)) |>
  # Фильтрация: оставляем только записи с допустимой громкостью и известным годом
  filter(loudness <= 0, !is.na(release_year)) |>
  # Убираем дубликаты
  distinct() |>
  # Добавляем только признак десятилетия
  mutate(
    decade = floor(release_year / 10) * 10   # Десятилетие
  )

# Быстрый обзор распределений ключевых аудио-характеристик
skimr::skim(select(spotify, danceability:valence))
Data summary
Name select(spotify, danceabil…
Number of rows 586453
Number of columns 10
_______________________
Column type frequency:
numeric 10
________________________
Group variables None

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
danceability 0 1 0.56 0.17 0 0.45 0.58 0.69 0.99 ▁▃▇▇▂
energy 0 1 0.54 0.25 0 0.34 0.55 0.75 1.00 ▃▆▇▇▆
key 0 1 5.22 3.52 0 2.00 5.00 8.00 11.00 ▇▃▃▅▆
loudness 0 1 -10.21 5.09 -60 -12.89 -9.24 -6.48 0.00 ▁▁▁▃▇
mode 0 1 0.66 0.47 0 0.00 1.00 1.00 1.00 ▅▁▁▁▇
speechiness 0 1 0.10 0.18 0 0.03 0.04 0.08 0.97 ▇▁▁▁▁
acousticness 0 1 0.45 0.35 0 0.10 0.42 0.78 1.00 ▇▃▃▃▆
instrumentalness 0 1 0.11 0.27 0 0.00 0.00 0.01 1.00 ▇▁▁▁▁
liveness 0 1 0.21 0.18 0 0.10 0.14 0.28 1.00 ▇▃▁▁▁
valence 0 1 0.55 0.26 0 0.35 0.56 0.77 1.00 ▃▆▇▇▇

После фильтрации получилось порядка 580 000 уникальных треков с полными данными. Небольшая доля пропусков по фичам (около 3 %) была исключена, что не должно существенно исказить общую выборку.

Сводная таблица по десятилетиям

summary_by_decade <- spotify |>
  group_by(decade) |>
  summarise(
    avg_tempo        = mean(tempo, na.rm = TRUE),              # Средний темп
    avg_danceability = mean(danceability, na.rm = TRUE),       # Средний danceability
    avg_energy       = mean(energy, na.rm = TRUE),             # Средний energy
    avg_valence      = mean(valence, na.rm = TRUE),            # Средний valence
    n_tracks         = n(),                                    # Количество треков
    .groups = "drop"
  )

# Визуализация таблицы десятилетий
gt(summary_by_decade)
decade avg_tempo avg_danceability avg_energy avg_valence n_tracks
1900 141.9990 0.6590000 0.7910000 0.9560000 1
1920 113.0974 0.6043189 0.2812913 0.5996449 7610
1930 112.7302 0.5507997 0.3061573 0.5706884 13033
1940 107.0191 0.4755958 0.2678298 0.5010070 18039
1950 110.6982 0.4825790 0.2979488 0.4945469 35322
1960 114.0942 0.4969944 0.4043408 0.5613316 47236
1970 117.3786 0.5237674 0.5018790 0.5803162 61825
1980 118.8548 0.5632659 0.5500151 0.5800964 82318
1990 119.6791 0.5720926 0.5743593 0.5694884 108868
2000 121.4159 0.5902608 0.6490758 0.5640935 86798
2010 122.0603 0.6085678 0.6583276 0.5141886 105193
2020 121.8608 0.6615462 0.6328292 0.5030643 20210

Как мы можем видеть, музыка становится заметно более «энергичной» и «танцевальной» на протяжении последних 50 лет.

Основные визуализации трендов

# Средняя danceability по годам
year_summary <- spotify |>
  group_by(release_year) |>
  summarise(
    avg_dance = mean(danceability, na.rm = TRUE),             # Средний danceability в год
    .groups = "drop"
  )

ggplot(year_summary, aes(x = release_year, y = avg_dance)) +
  geom_line() +
  geom_point(size = 1) +
  labs(
    title = "Средняя danceability по годам",
    x = "Год выпуска", y = "Средняя danceability"
  )

Из графика видно что тренд устойчив, а последние годы могут указывать на начало новой фазы стилистических перемен. До 1950 видна сильная вариация, вероятно это связано с маленькой выборкой музыки тех лет.

# Облачко точек (sample 20k) + LOESS
set.seed(42)  # Для воспроизводимости выборки
spotify_sample <- spotify |> sample_n(20000)

ggplot(spotify_sample, aes(x = release_year, y = danceability)) +
  geom_point(alpha = 0.1) +                                  # Полупрозрачные точки
  geom_smooth(method = "loess", se = TRUE) +                 # LOESS сглаживание
  labs(
    title = "Danceability (выборка 20k точек)",
    x = "Год выпуска", y = "Danceability"
  )
`geom_smooth()` using formula = 'y ~ x'

Также видно что с течением лет музыка становиться всё более танцивальной

Дополнительные визуализации

# Распределение популярности треков
ggplot(spotify, aes(popularity)) +
  geom_histogram(bins = 30) +                                # Гистограмма popularity
  labs(title = "Распределение популярности треков",
       x = "Popularity", y = "Count")

Гистограмма показывает, что большинство “треков”, кроме тех что имеют прям очень мало прослушиваний, имеет умеренные значения метрики popularity (около 40–60), а совсем популярных (> 80) менее 5 % выборки.

# Корреляционная матрица фич
corr_data <- spotify |>
  select(danceability, energy, loudness, tempo, valence, popularity) |>
  drop_na()

corr_mat <- cor(corr_data)                  # Матрица корреляций
melted_corr <- as.data.frame(as.table(corr_mat))

ggplot(melted_corr, aes(Var1, Var2, fill = Freq)) +
  geom_tile(color = "white") +              # рамки по клеткам
  geom_text(aes(label = sprintf("%.2f", Freq)), size = 3) +  # подписи
  scale_fill_gradient2(                      # цветовая шкала от –1 до +1
    low = "blue", high = "red", mid = "white",
    midpoint = 0, limit = c(-1,1), space = "Lab"
  ) +
  labs(
    title = "Корреляции между фичами и популярностью",
    x = NULL, y = NULL
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    panel.grid = element_blank()
  )

Видна значимая корреляция между loudness и energy, что не слишком уж и удивительный результат. Популярность больше всего же коррелирует с loudness однако корреляция не является сильной (~0.33).

# Популярность vs аудио-фичи
features <- c("danceability", "energy", "loudness", "tempo", "valence")

for (feat in features) {
  p <- ggplot(spotify_sample, aes_string(x = feat, y = "popularity")) +
    geom_point(alpha = 0.2) +
    geom_smooth(method = "lm", se = FALSE) +
    labs(
      title = paste("Popularity vs", feat),
      x = feat, y = "Popularity"
    )
  print(p)   # обязательно печатаем каждый график
}
`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

Моделирование популярности: линейная vs RF

library(tidymodels)
library(dials)
library(tune)
library(vip)

Присоединяю пакет: 'vip'
Следующий объект скрыт от 'package:utils':

    vi
set.seed(42)  # Для воспроизводимости

# Формируем выборку для моделирования
model_data <- spotify |>
  sample_n(50000) |>
  select(popularity, danceability, energy, loudness, tempo, valence) |>
  drop_na()

# Сплит на тренировочную и тестовую выборки
split <- initial_split(model_data, prop = 0.8)
train <- training(split)
test  <- testing(split)

# Рецепт: нормализация всех предикторов
rec <- recipe(popularity ~ ., data = train) |>
  step_normalize(all_predictors())

# Линейная модель
lin_mod <- linear_reg() |> set_engine("lm")
lin_wf  <- workflow() |> add_recipe(rec) |> add_model(lin_mod)
lin_fit <- fit(lin_wf, data = train)

# RF hyper-parameter tuning 
# Спецификация модели с tune()
rf_spec <- rand_forest(
  mtry  = tune(),
  trees = tune(),
  min_n = tune()
) |>
  set_engine("ranger") |>
  set_mode("regression")

# Workflow для тюнинга
rf_wf_tune <- workflow() |>
  add_recipe(rec) |>
  add_model(rf_spec)

# Сетка гиперпараметров (как было)
rf_grid <- grid_random(
  mtry(range  = c(2, 5)),
  trees(range = c(200, 800)),
  min_n(range = c(3, 15)),
  size = 10         # 10 случайных
)

# 5-кратная кросс-валидация (как было)
folds <- vfold_cv(train, v = 5)

# Тюнинг по RMSE и R² через workflow
rf_tune <- tune_grid(
  rf_wf_tune,
  resamples = folds,
  grid      = rf_grid,
  metrics   = metric_set(rmse, rsq),
    control   = control_grid(
                verbose = TRUE,       # подробные логи
                allow_par = FALSE,    # отключить параллельность, если она даёт сбои
                save_pred = TRUE      # сохранять предсказания для диагностики
              )
)
i Fold1: preprocessor 1/1
✓ Fold1: preprocessor 1/1
i Fold1: preprocessor 1/1, model 1/10
✓ Fold1: preprocessor 1/1, model 1/10
i Fold1: preprocessor 1/1, model 1/10 (extracts)
i Fold1: preprocessor 1/1, model 1/10 (predictions)
i Fold1: preprocessor 1/1, model 2/10
✓ Fold1: preprocessor 1/1, model 2/10
i Fold1: preprocessor 1/1, model 2/10 (extracts)
i Fold1: preprocessor 1/1, model 2/10 (predictions)
i Fold1: preprocessor 1/1, model 3/10
✓ Fold1: preprocessor 1/1, model 3/10
i Fold1: preprocessor 1/1, model 3/10 (extracts)
i Fold1: preprocessor 1/1, model 3/10 (predictions)
i Fold1: preprocessor 1/1, model 4/10
✓ Fold1: preprocessor 1/1, model 4/10
i Fold1: preprocessor 1/1, model 4/10 (extracts)
i Fold1: preprocessor 1/1, model 4/10 (predictions)
i Fold1: preprocessor 1/1, model 5/10
✓ Fold1: preprocessor 1/1, model 5/10
i Fold1: preprocessor 1/1, model 5/10 (extracts)
i Fold1: preprocessor 1/1, model 5/10 (predictions)
i Fold1: preprocessor 1/1, model 6/10
✓ Fold1: preprocessor 1/1, model 6/10
i Fold1: preprocessor 1/1, model 6/10 (extracts)
i Fold1: preprocessor 1/1, model 6/10 (predictions)
i Fold1: preprocessor 1/1, model 7/10
✓ Fold1: preprocessor 1/1, model 7/10
i Fold1: preprocessor 1/1, model 7/10 (extracts)
i Fold1: preprocessor 1/1, model 7/10 (predictions)
i Fold1: preprocessor 1/1, model 8/10
✓ Fold1: preprocessor 1/1, model 8/10
i Fold1: preprocessor 1/1, model 8/10 (extracts)
i Fold1: preprocessor 1/1, model 8/10 (predictions)
i Fold1: preprocessor 1/1, model 9/10
✓ Fold1: preprocessor 1/1, model 9/10
i Fold1: preprocessor 1/1, model 9/10 (extracts)
i Fold1: preprocessor 1/1, model 9/10 (predictions)
i Fold1: preprocessor 1/1, model 10/10
✓ Fold1: preprocessor 1/1, model 10/10
i Fold1: preprocessor 1/1, model 10/10 (extracts)
i Fold1: preprocessor 1/1, model 10/10 (predictions)
i Fold2: preprocessor 1/1
✓ Fold2: preprocessor 1/1
i Fold2: preprocessor 1/1, model 1/10
✓ Fold2: preprocessor 1/1, model 1/10
i Fold2: preprocessor 1/1, model 1/10 (extracts)
i Fold2: preprocessor 1/1, model 1/10 (predictions)
i Fold2: preprocessor 1/1, model 2/10
✓ Fold2: preprocessor 1/1, model 2/10
i Fold2: preprocessor 1/1, model 2/10 (extracts)
i Fold2: preprocessor 1/1, model 2/10 (predictions)
i Fold2: preprocessor 1/1, model 3/10
✓ Fold2: preprocessor 1/1, model 3/10
i Fold2: preprocessor 1/1, model 3/10 (extracts)
i Fold2: preprocessor 1/1, model 3/10 (predictions)
i Fold2: preprocessor 1/1, model 4/10
✓ Fold2: preprocessor 1/1, model 4/10
i Fold2: preprocessor 1/1, model 4/10 (extracts)
i Fold2: preprocessor 1/1, model 4/10 (predictions)
i Fold2: preprocessor 1/1, model 5/10
✓ Fold2: preprocessor 1/1, model 5/10
i Fold2: preprocessor 1/1, model 5/10 (extracts)
i Fold2: preprocessor 1/1, model 5/10 (predictions)
i Fold2: preprocessor 1/1, model 6/10
✓ Fold2: preprocessor 1/1, model 6/10
i Fold2: preprocessor 1/1, model 6/10 (extracts)
i Fold2: preprocessor 1/1, model 6/10 (predictions)
i Fold2: preprocessor 1/1, model 7/10
✓ Fold2: preprocessor 1/1, model 7/10
i Fold2: preprocessor 1/1, model 7/10 (extracts)
i Fold2: preprocessor 1/1, model 7/10 (predictions)
i Fold2: preprocessor 1/1, model 8/10
✓ Fold2: preprocessor 1/1, model 8/10
i Fold2: preprocessor 1/1, model 8/10 (extracts)
i Fold2: preprocessor 1/1, model 8/10 (predictions)
i Fold2: preprocessor 1/1, model 9/10
✓ Fold2: preprocessor 1/1, model 9/10
i Fold2: preprocessor 1/1, model 9/10 (extracts)
i Fold2: preprocessor 1/1, model 9/10 (predictions)
i Fold2: preprocessor 1/1, model 10/10
✓ Fold2: preprocessor 1/1, model 10/10
i Fold2: preprocessor 1/1, model 10/10 (extracts)
i Fold2: preprocessor 1/1, model 10/10 (predictions)
i Fold3: preprocessor 1/1
✓ Fold3: preprocessor 1/1
i Fold3: preprocessor 1/1, model 1/10
✓ Fold3: preprocessor 1/1, model 1/10
i Fold3: preprocessor 1/1, model 1/10 (extracts)
i Fold3: preprocessor 1/1, model 1/10 (predictions)
i Fold3: preprocessor 1/1, model 2/10
✓ Fold3: preprocessor 1/1, model 2/10
i Fold3: preprocessor 1/1, model 2/10 (extracts)
i Fold3: preprocessor 1/1, model 2/10 (predictions)
i Fold3: preprocessor 1/1, model 3/10
✓ Fold3: preprocessor 1/1, model 3/10
i Fold3: preprocessor 1/1, model 3/10 (extracts)
i Fold3: preprocessor 1/1, model 3/10 (predictions)
i Fold3: preprocessor 1/1, model 4/10
✓ Fold3: preprocessor 1/1, model 4/10
i Fold3: preprocessor 1/1, model 4/10 (extracts)
i Fold3: preprocessor 1/1, model 4/10 (predictions)
i Fold3: preprocessor 1/1, model 5/10
✓ Fold3: preprocessor 1/1, model 5/10
i Fold3: preprocessor 1/1, model 5/10 (extracts)
i Fold3: preprocessor 1/1, model 5/10 (predictions)
i Fold3: preprocessor 1/1, model 6/10
✓ Fold3: preprocessor 1/1, model 6/10
i Fold3: preprocessor 1/1, model 6/10 (extracts)
i Fold3: preprocessor 1/1, model 6/10 (predictions)
i Fold3: preprocessor 1/1, model 7/10
✓ Fold3: preprocessor 1/1, model 7/10
i Fold3: preprocessor 1/1, model 7/10 (extracts)
i Fold3: preprocessor 1/1, model 7/10 (predictions)
i Fold3: preprocessor 1/1, model 8/10
✓ Fold3: preprocessor 1/1, model 8/10
i Fold3: preprocessor 1/1, model 8/10 (extracts)
i Fold3: preprocessor 1/1, model 8/10 (predictions)
i Fold3: preprocessor 1/1, model 9/10
✓ Fold3: preprocessor 1/1, model 9/10
i Fold3: preprocessor 1/1, model 9/10 (extracts)
i Fold3: preprocessor 1/1, model 9/10 (predictions)
i Fold3: preprocessor 1/1, model 10/10
✓ Fold3: preprocessor 1/1, model 10/10
i Fold3: preprocessor 1/1, model 10/10 (extracts)
i Fold3: preprocessor 1/1, model 10/10 (predictions)
i Fold4: preprocessor 1/1
✓ Fold4: preprocessor 1/1
i Fold4: preprocessor 1/1, model 1/10
✓ Fold4: preprocessor 1/1, model 1/10
i Fold4: preprocessor 1/1, model 1/10 (extracts)
i Fold4: preprocessor 1/1, model 1/10 (predictions)
i Fold4: preprocessor 1/1, model 2/10
✓ Fold4: preprocessor 1/1, model 2/10
i Fold4: preprocessor 1/1, model 2/10 (extracts)
i Fold4: preprocessor 1/1, model 2/10 (predictions)
i Fold4: preprocessor 1/1, model 3/10
✓ Fold4: preprocessor 1/1, model 3/10
i Fold4: preprocessor 1/1, model 3/10 (extracts)
i Fold4: preprocessor 1/1, model 3/10 (predictions)
i Fold4: preprocessor 1/1, model 4/10
✓ Fold4: preprocessor 1/1, model 4/10
i Fold4: preprocessor 1/1, model 4/10 (extracts)
i Fold4: preprocessor 1/1, model 4/10 (predictions)
i Fold4: preprocessor 1/1, model 5/10
✓ Fold4: preprocessor 1/1, model 5/10
i Fold4: preprocessor 1/1, model 5/10 (extracts)
i Fold4: preprocessor 1/1, model 5/10 (predictions)
i Fold4: preprocessor 1/1, model 6/10
✓ Fold4: preprocessor 1/1, model 6/10
i Fold4: preprocessor 1/1, model 6/10 (extracts)
i Fold4: preprocessor 1/1, model 6/10 (predictions)
i Fold4: preprocessor 1/1, model 7/10
✓ Fold4: preprocessor 1/1, model 7/10
i Fold4: preprocessor 1/1, model 7/10 (extracts)
i Fold4: preprocessor 1/1, model 7/10 (predictions)
i Fold4: preprocessor 1/1, model 8/10
✓ Fold4: preprocessor 1/1, model 8/10
i Fold4: preprocessor 1/1, model 8/10 (extracts)
i Fold4: preprocessor 1/1, model 8/10 (predictions)
i Fold4: preprocessor 1/1, model 9/10
✓ Fold4: preprocessor 1/1, model 9/10
i Fold4: preprocessor 1/1, model 9/10 (extracts)
i Fold4: preprocessor 1/1, model 9/10 (predictions)
i Fold4: preprocessor 1/1, model 10/10
✓ Fold4: preprocessor 1/1, model 10/10
i Fold4: preprocessor 1/1, model 10/10 (extracts)
i Fold4: preprocessor 1/1, model 10/10 (predictions)
i Fold5: preprocessor 1/1
✓ Fold5: preprocessor 1/1
i Fold5: preprocessor 1/1, model 1/10
✓ Fold5: preprocessor 1/1, model 1/10
i Fold5: preprocessor 1/1, model 1/10 (extracts)
i Fold5: preprocessor 1/1, model 1/10 (predictions)
i Fold5: preprocessor 1/1, model 2/10
✓ Fold5: preprocessor 1/1, model 2/10
i Fold5: preprocessor 1/1, model 2/10 (extracts)
i Fold5: preprocessor 1/1, model 2/10 (predictions)
i Fold5: preprocessor 1/1, model 3/10
✓ Fold5: preprocessor 1/1, model 3/10
i Fold5: preprocessor 1/1, model 3/10 (extracts)
i Fold5: preprocessor 1/1, model 3/10 (predictions)
i Fold5: preprocessor 1/1, model 4/10
✓ Fold5: preprocessor 1/1, model 4/10
i Fold5: preprocessor 1/1, model 4/10 (extracts)
i Fold5: preprocessor 1/1, model 4/10 (predictions)
i Fold5: preprocessor 1/1, model 5/10
✓ Fold5: preprocessor 1/1, model 5/10
i Fold5: preprocessor 1/1, model 5/10 (extracts)
i Fold5: preprocessor 1/1, model 5/10 (predictions)
i Fold5: preprocessor 1/1, model 6/10
✓ Fold5: preprocessor 1/1, model 6/10
i Fold5: preprocessor 1/1, model 6/10 (extracts)
i Fold5: preprocessor 1/1, model 6/10 (predictions)
i Fold5: preprocessor 1/1, model 7/10
✓ Fold5: preprocessor 1/1, model 7/10
i Fold5: preprocessor 1/1, model 7/10 (extracts)
i Fold5: preprocessor 1/1, model 7/10 (predictions)
i Fold5: preprocessor 1/1, model 8/10
✓ Fold5: preprocessor 1/1, model 8/10
i Fold5: preprocessor 1/1, model 8/10 (extracts)
i Fold5: preprocessor 1/1, model 8/10 (predictions)
i Fold5: preprocessor 1/1, model 9/10
✓ Fold5: preprocessor 1/1, model 9/10
i Fold5: preprocessor 1/1, model 9/10 (extracts)
i Fold5: preprocessor 1/1, model 9/10 (predictions)
i Fold5: preprocessor 1/1, model 10/10
✓ Fold5: preprocessor 1/1, model 10/10
i Fold5: preprocessor 1/1, model 10/10 (extracts)
i Fold5: preprocessor 1/1, model 10/10 (predictions)
# Подбираем лучшие параметры и финализируем
rf_best <- select_best(rf_tune, metric = "rmse")
rf_final_spec <- finalize_model(rf_spec, rf_best)

# Финальный workflow и обучение
rf_wf_final <- workflow() |>
  add_recipe(rec) |>
  add_model(rf_final_spec)

rf_fit <- fit(rf_wf_final, data = train)

# Опционально: визуализируем важность признаков

# --- Предсказание и сравнение ---------------------------------
# Предсказания на тестовой выборке
lin_preds <- predict(lin_fit, test) |> bind_cols(test)
rf_preds  <- predict(rf_fit,  test) |> bind_cols(test)

# Сбор метрик для сравнения моделей
metrics_compare <- bind_rows(
  metrics(lin_preds, truth = popularity, estimate = .pred) |> mutate(model = "Linear"),
  metrics(rf_preds,  truth = popularity, estimate = .pred) |> mutate(model = "Random Forest (tuned)")
)

metrics_compare  # Вывод таблицы метрик
# A tibble: 6 × 4
  .metric .estimator .estimate model                
  <chr>   <chr>          <dbl> <chr>                
1 rmse    standard      17.0   Linear               
2 rsq     standard       0.165 Linear               
3 mae     standard      13.8   Linear               
4 rmse    standard      16.4   Random Forest (tuned)
5 rsq     standard       0.224 Random Forest (tuned)
6 mae     standard      13.3   Random Forest (tuned)
metrics_compare |>
  filter(.metric %in% c("rsq", "rmse")) |>
  ggplot(aes(x = model, y = .estimate, fill = .metric)) +
  geom_col(position = "dodge") +
  labs(
    title = "Сравнение моделей: R² и RMSE",
    x = "Модель", y = "Значение метрики"
  )

# Actual vs Predicted для Random Forest
ggplot(rf_preds, aes(popularity, .pred)) +
  geom_point(alpha = 0.3) +
  geom_abline(linetype = "dashed") +
  labs(
    title = "Фактическое vs Предсказанное (Random Forest)",
    x = "Actual Popularity", y = "Predicted Popularity"
  )

# Фактическое vs Предсказанное для обеих моделей
library(dplyr)
lin_plot <- lin_preds %>% mutate(model = "Linear")
rf_plot  <- rf_preds  %>% mutate(model = "Random Forest (tuned)")
all_preds <- bind_rows(lin_plot, rf_plot)

ggplot(all_preds, aes(x = popularity, y = .pred)) +
  geom_point(alpha = 0.3) +
  geom_abline(linetype = "dashed") +
  facet_wrap(~model) +
  labs(
    title = "Actual vs Predicted Popularity",
    x = "Actual Popularity", y = "Predicted Popularity"
  )

Ни одна из моделей не показала себя удовлетворительно, однако у модели Tuned Random Forest результаты немного лучше, и следует предпочесть её.

Выводы

  • Тенденции во времени: музыка стала быстрее, «танцевальнее» с каждым десятилетием.

  • Корреляции: Громкость трека — самая сильная аудио-фича для предсказания популярности, за ней следуют enargy и danceability.

  • Моделирование:

Линейная регрессия даёт не лучший уровень объяснения (16 %).

Tuned Random Forest значимо улучшает качество (до 22 %), что указывает на важность учёта сложных, нелинейных взаимодействий признаков, однако данный уровень является недостаточным для полноценного предсказания популярности отедельных “треков”.

  • Рекомендации для дальнейшего анализа:

    • Добавить категориальные и текстовые признаки (жанр, артист, год, лейбл).

    • Возможно рассмотреть другие модели для решения задачи предсказания.