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 Audio Features Analysis 1921–2020
Введение
Проект исследует эволюцию аудио‑характеристик треков Spotify за столетие (1920‑е—2020‑е). Используют данные из Kaggle (Kaggle >> Spotify Tracks Dataset.).
Задачи:
Загрузить и очистить данные tracks.csv из Kaggle.
Сформировать новые признаки (год, десятилетие, отношения фич, бины темпа).
Провести разведочный анализ и агрегации.
Построить информативные визуализации трендов.
Оценить корреляции аудио‑фич с популярностью.
Построить модель оценки популярности “трека”.
Загрузка и объединение данных
Предобработка и признаки
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))| 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 %), что указывает на важность учёта сложных, нелинейных взаимодействий признаков, однако данный уровень является недостаточным для полноценного предсказания популярности отедельных “треков”.
Рекомендации для дальнейшего анализа:
Добавить категориальные и текстовые признаки (жанр, артист, год, лейбл).
Возможно рассмотреть другие модели для решения задачи предсказания.