Коллекция песен из индийского кинематографа DataSet: различия между версиями

Материал из Поле цифровой дидактики
Нет описания правки
 
(не показано 6 промежуточных версий 2 участников)
Строка 8: Строка 8:
}}
}}
== Общая информация ==
== Общая информация ==
* '''Авторы:''' Студент группы [[Категория:ИНДОР-211]] -  [[Участник:Pokrovskii Alexander|Pokrovskii Alexander]]
* '''Авторы:''' Студент группы [[:Категория:ИНДОР-211]] -  [[Участник:Pokrovskii Alexander|Pokrovskii Alexander]]
* '''Дата исследования:''' 14 апреля 2026
* '''Дата исследования:''' 14 апреля 2026
* '''Источник:''' Kaggle Datasets
* '''Источник:''' Kaggle Datasets
Строка 65: Строка 65:
# Анализ БД
# Анализ БД


import numpy as np
# Проверка, что .data[[col]] работает в вашей среде
import pandas as pd
test_df <- tibble(x = 1:5, y = letters[1:5])
import matplotlib.pyplot as plt
test_col <- "x"
import seaborn as sns
result <- test_df %>% filter(!is.na(.data[[test_col]]))
import re
stopifnot(nrow(result) == 5)  # если не упадёт — всё ок


# ==========================================
# ==========================================
# A) Imports + Global Config
# A) Imports + Global Config
# ==========================================
# ==========================================
np.random.seed(42)
library(tidyverse)   # dplyr, tidyr, readr, ggplot2, stringr, purrr
pd.set_option('display.max_columns', 50)
library(lubridate)    # работа с датами
pd.set_option('display.width', 1000)
library(corrplot)     # тепловые карты корреляций
pd.set_option('display.float_format', '{:.2f}'.format)
library(scales)       # форматирование осей
library(patchwork)   # компоновка графиков


plt.rcParams['figure.figsize'] = (12, 6)
set.seed(42)
plt.rcParams['axes.titlesize'] = 14
options(digits = 2, width = 120)
plt.rcParams['axes.labelsize'] = 12
sns.set_theme(style="whitegrid")


# Provided Inputs
# Глобальные настройки ggplot2
DATASET_NAME = "moonknightmarvel/dataset-of-songs-with-genreartistmovielanguage"
theme_set(theme_minimal(base_size = 12))
EXACT_COLUMNS = ["title", "artist", "movie", "language", "emotion"]
update_geom_defaults("point", list(alpha = 0.6))
TARGET_COL = None
 
# Входные параметры (аналог Python-конфига)
DATASET_NAME <- "moonknightmarvel/dataset-of-songs-with-genreartistmovielanguage"
EXACT_COLUMNS <- c("title", "artist", "movie", "language", "emotion")
TARGET_COL <- NULL  # можно задать, например, "emotion"
DATA_DIR <- "C:/songs_db.csv"  # путь к файлу


# ==========================================
# ==========================================
Строка 93: Строка 97:
# ==========================================
# ==========================================


def safe_read_csv(path):
safe_read_csv <- function(path) {
     try:
  tryCatch(
        # DATA_DIR is assumed to be defined in the environment per instructions
     read_csv(path, show_col_types = FALSE),
        return pd.read_csv(path)
     error = function(e) {
     except Exception as e:
      message(sprintf("CRITICAL ERROR: Could not read CSV at %s. Error: %s", path, e$message))
        print(f"CRITICAL ERROR: Could not read CSV at {path}. Error: {e}")
      return(tibble())
        return pd.DataFrame()
    }
  )
}


def validate_columns(df, expected_cols):
validate_columns <- function(df, expected_cols) {
    if df.empty or not expected_cols:
  if (nrow(df) == 0 || is.null(expected_cols)) return(invisible(NULL))
        return
 
    actual_cols = df.columns.tolist()
  actual_cols <- names(df)
    missing = [c for c in expected_cols if c not in actual_cols]
  missing <- setdiff(expected_cols, actual_cols)
    extra = [c for c in actual_cols if c not in expected_cols]
  extra <- setdiff(actual_cols, expected_cols)
   
 
    print("-" * 30)
  cat(strrep("-", 30), "\n")
    print(f"COLUMN VALIDATION: {DATASET_NAME}")
  cat(sprintf("COLUMN VALIDATION: %s\n", DATASET_NAME))
    if not missing and not extra:
 
        print("Success: All expected columns found. No extra columns.")
  if (length(missing) == 0 && length(extra) == 0) {
    else:
    cat("Success: All expected columns found. No extra columns.\n")
        if missing: print(f"Missing expected columns: {missing}")
  } else {
        if extra: print(f"Extra columns found: {extra}")
    if (length(missing) > 0) cat(sprintf("Missing expected columns: %s\n", paste(missing, collapse = ", ")))
    print("-" * 30)
    if (length(extra) > 0) cat(sprintf("Extra columns found: %s\n", paste(extra, collapse = ", ")))
  }
  cat(strrep("-", 30), "\n")
  invisible(NULL)
}
 
audit_missingness <- function(df) {
  null_counts <- colSums(is.na(df))
  null_pct <- (null_counts / nrow(df)) * 100
  non_null <- nrow(df) - null_counts
 
  tibble(
    Column = names(df),
    `Null Count` = null_counts,
    `Null %` = round(null_pct, 2),
    `Non-Null Count` = non_null
  ) %>% arrange(desc(`Null %`))
}


def audit_missingness(df):
detect_column_types <- function(df) {
    null_counts = df.isnull().sum()
  num_cols <- names(df)[sapply(df, is.numeric)]
    null_pct = (null_counts / len(df)) * 100
  char_cols <- names(df)[sapply(df, is.character)]
     non_null = df.notnull().sum()
 
    audit = pd.DataFrame({
  # Эвристика для дат: ищем паттерны вроде "2024-01-15" или "15/01/2024"
        'Null Count': null_counts,
  date_pattern <- "\\d{4}-\\d{2}-\\d{2}|\\d{2}/\\d{2}/\\d{4}"
        'Null %': null_pct,
  date_cols <- char_cols[sapply(df[char_cols], function(col) {
        'Non-Null Count': non_null
     any(str_detect(na.omit(as.character(col[1:min(5, length(col))])), date_pattern), na.rm = TRUE)
    })
  })]
    return audit
 
  cat_cols <- setdiff(char_cols, date_cols)
  list(num = num_cols, cat = cat_cols, date = date_cols)
}


def detect_column_types(df):
safe_to_numeric <- function(series) {
    num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
  suppressWarnings(as.numeric(series))
    cat_cols = df.select_dtypes(include=['object', 'category']).columns.tolist()
}
    date_cols = []
   
    # Simple heuristic for potential date columns
    for col in cat_cols:
        sample = df[col].dropna().head(5).astype(str)
        if any(sample.str.contains(r'\d{4}-\d{2}-\d{2}|\d{2}/\d{2}/\d{4}', regex=True)):
            date_cols.append(col)
           
    # Refine cat_cols (remove dates)
    cat_cols = [c for c in cat_cols if c not in date_cols]
    return num_cols, cat_cols, date_cols


def safe_to_numeric(series):
safe_to_datetime <- function(series) {
    try:
  parsed <- parse_date_time(series, orders = c("Ymd", "dmy", "mdY"), quiet = TRUE)
        return pd.to_numeric(series, errors='coerce')
  ifelse(is.na(parsed), NA, parsed)
    except:
}
        return series


def safe_to_datetime(series):
plot_missingness <- function(df) {
     try:
  null_pct <- colSums(is.na(df)) / nrow(df) * 100
        return pd.to_datetime(series, errors='coerce')
  null_pct <- null_pct[null_pct > 0] %>% sort(decreasing = TRUE) %>% head(30)
     except:
 
        return series
  if (length(null_pct) == 0) return(NULL)
 
  tibble(Column = names(null_pct), `Missing %` = null_pct) %>%
     mutate(Column = fct_reorder(Column, `Missing %`)) %>%
    ggplot(aes(x = `Missing %`, y = Column, fill = `Missing %`)) +
    geom_col(show.legend = FALSE) +
    scale_fill_gradient(low = "#fee0d2", high = "#cb181d") +
     labs(title = "Top Columns by Missing Percentage (%)", x = "% Missing", y = NULL) +
    theme(axis.text.y = element_text(size = 9))
}


def plot_missingness(df):
# Исправленная версия plot_univariate_num
    null_pct = (df.isnull().sum() / len(df)) * 100
plot_univariate_num <- function(df, num_cols) {
     null_pct = null_pct[null_pct > 0].sort_values(ascending=False).head(30)
  cols_to_plot <- head(num_cols, 12)
    if not null_pct.empty:
  if (length(cols_to_plot) == 0) return(NULL)
        sns.barplot(x=null_pct.values, y=null_pct.index, hue=null_pct.index, palette='Reds_r', legend=False)
 
        plt.title("Top Columns by Missing Percentage (%)")
  plots <- lapply(cols_to_plot, function(col) {
        plt.xlabel("% Missing")
     df %>%
        plt.show()
      filter(!is.na(.data[[col]])) %>% 
      ggplot(aes(x = .data[[col]])) +
      geom_histogram(aes(y = after_stat(density)), bins = 30,  
                    fill = "teal", color = "white", alpha = 0.8) +
      geom_density(color = "darkred", linewidth = 0.8) +
      labs(title = sprintf("Distribution of %s", col), x = NULL, y = "Density") +
      theme(axis.text.x = element_text(angle = 45, hjust = 1))
  })
 
  patchwork::wrap_plots(plots, ncol = 3)
}


def plot_univariate_num(df, num_cols):
# Исправленная версия plot_univariate_cat
    cols_to_plot = num_cols[:12]
plot_univariate_cat <- function(df, cat_cols) {
    if not cols_to_plot: return
  cols_to_plot <- head(cat_cols, 6)
    n = len(cols_to_plot)
  if (length(cols_to_plot) == 0) return(NULL)
     rows = (n + 2) // 3
 
    fig, axes = plt.subplots(rows, 3, figsize=(18, 5 * rows))
  lapply(cols_to_plot, function(col) {
    axes = axes.flatten()
     # ✅ group_by + count с .data[[col]]
     for i, col in enumerate(cols_to_plot):
    top_vals <- df %>%
        sns.histplot(df[col].dropna(), kde=True, ax=axes[i], color='teal')
      group_by(.data[[col]]) %>%
        axes[i].set_title(f"Distribution of {col}")
      summarise(n = n(), .groups = "drop") %>%
    for j in range(i + 1, len(axes)):
      arrange(desc(n)) %>%
        fig.delaxes(axes[j])
      slice_head(n = 15)
    plt.tight_layout()
      
    plt.show()
    top_vals %>%
      mutate(!!col := fct_reorder(.data[[col]], n)) %>%
      ggplot(aes(x = n, y = .data[[col]], fill = n)) +
      geom_col(show.legend = FALSE) +
      scale_fill_viridis_d(option = "viridis") +
      labs(title = sprintf("Top 15 Categories: %s", col), x = "Count", y = NULL) +
      theme(axis.text.y = element_text(size = 9))
  })
}


def plot_univariate_cat(df, cat_cols):
# Исправленная часть EDA с TARGET_COL (если он категориальный)
    cols_to_plot = cat_cols[:6]
if (!is.null(TARGET_COL) && TARGET_COL %in% names(df_clean)) {
    if not cols_to_plot: return
  # ... внутри else для категориального таргета:
    for col in cols_to_plot:
  df_clean %>%
        if df[col].nunique() > 50:
    group_by(.data[[TARGET_COL]]) %>%
            top_vals = df[col].value_counts().head(15)
    summarise(n = n(), .groups = "drop") %>%
        else:
    mutate(!!TARGET_COL := fct_reorder(.data[[TARGET_COL]], n)) %>%
            top_vals = df[col].value_counts().head(15)
    ggplot(aes(x = n, y = .data[[TARGET_COL]], fill = n)) +
       
    geom_col(show.legend = FALSE) +
        plt.figure(figsize=(10, 5))
    scale_fill_viridis_d(option = "magma") +
        sns.barplot(x=top_vals.values, y=top_vals.index, hue=top_vals.index, palette='viridis', legend=False)
    labs(title = sprintf("Target Distribution: %s", TARGET_COL),
        plt.title(f"Top 15 Categories: {col}")
        x = "Count", y = NULL) +
        plt.show()
    theme(axis.text.y = element_text(size = 10)) %>%
    print()
}


def plot_correlation(df, num_cols):
plot_correlation <- function(df, num_cols) {
    if len(num_cols) < 2: return
  if (length(num_cols) < 2) return(NULL)
    corr = df[num_cols].corr()
 
    plt.figure(figsize=(10, 8))
  corr_mat <- df %>% select(all_of(num_cols)) %>% cor(use = "complete.obs")
    sns.heatmap(corr, annot=True, cmap='coolwarm', fmt=".2f", linewidths=0.5)
  corrplot(corr_mat,
    plt.title("Pearson Correlation Matrix")
          method = "color",
    plt.show()
          type = "upper",  
          tl.cex = 0.8,  
          tl.srt = 45,
          addCoef.col = "black",  
          number.cex = 0.6,
          col = colorRampPalette(c("#6D9EC1", "white", "#E46726"))(200),
          title = "Pearson Correlation Matrix",
          mar = c(0,0,1,0))
}


# ==========================================
# ==========================================
# C) Load + Validate
# C) Load + Validate
# ==========================================
# ==========================================
df = safe_read_csv(DATA_DIR)
df <- safe_read_csv(DATA_DIR)
validate_columns(df, EXACT_COLUMNS)
validate_columns(df, EXACT_COLUMNS)


if not df.empty:
if (nrow(df) > 0) {
    # ==========================================
  # ==========================================
    # D) Data Audit
  # D) Data Audit
    # ==========================================
  # ==========================================
    print(f"\n--- DATA AUDIT: {DATASET_NAME} ---")
  cat(sprintf("\n--- DATA AUDIT: %s ---\n", DATASET_NAME))
    print(f"Shape: {df.shape}")
  cat(sprintf("Shape: %d rows × %d columns\n", nrow(df), ncol(df)))
    print(f"Memory Usage: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
  cat(sprintf("Memory Usage: %.2f MB\n", object.size(df) / 1024^2))
    print(f"Duplicates: {df.duplicated().sum()}")
  cat(sprintf("Duplicates: %d\n", sum(duplicated(df))))
   
 
    null_audit = audit_missingness(df)
  null_audit <- audit_missingness(df)
    print("\nNull Audit Summary (Top 5 Missing):")
  cat("\nNull Audit Summary (Top 5 Missing):\n")
    print(null_audit.sort_values(by='Null %', ascending=False).head(5))
  print(null_audit %>% slice_head(n = 5))
 
  col_types <- detect_column_types(df)
  num_cols <- col_types$num
  cat_cols <- col_types$cat
  date_cols <- col_types$date
 
  cat(sprintf("\nDetected Numeric Columns: %s\n", paste(num_cols, collapse = ", ")))
  cat(sprintf("Detected Categorical Columns: %s\n", paste(cat_cols, collapse = ", ")))
  cat(sprintf("Detected Date Columns: %s\n", paste(date_cols, collapse = ", ")))
 
  # Проверка на бесконечные значения и низкую дисперсию
  if (length(num_cols) > 0) {
    inf_counts <- sum(sapply(df[num_cols], function(x) sum(is.infinite(x))), na.rm = TRUE)
    cat(sprintf("Total Inf/-Inf values: %d\n", inf_counts))
      
      
     num_cols, cat_cols, date_cols = detect_column_types(df)
     low_var <- num_cols[sapply(df[num_cols], function(x) sd(x, na.rm = TRUE) < 0.01)]
    if (length(low_var) > 0) cat(sprintf("Low variance columns: %s\n", paste(low_var, collapse = ", ")))
  }
 
  # ==========================================
  # E) ETL (Safe + Reversible)
  # ==========================================
  df_clean <- df %>% mutate(across(everything(), ~.x))  # явное копирование
 
  # Очистка строк: пробелы + унификация пропусков
  missing_tokens <- c("", "NA", "N/A", "null", "None", "nan")
  df_clean <- df_clean %>%
    mutate(across(where(is.character), ~{
      .x %>%
        str_trim() %>%
        na_if("") %>%
        { ifelse(. %in% missing_tokens, NA, .) }
    }))
 
  # Попытка конвертировать "числовые" строки в numeric
  for (col in cat_cols) {
    if (col %in% names(df_clean)) {
      sample_vals <- df_clean[[col]] %>% drop_na() %>% head(10) %>% as.character()
      if (all(str_detect(sample_vals, "^-?\\d+(\\.\\d+)?$"), na.rm = TRUE) && length(sample_vals) > 0) {
        df_clean[[col]] <- safe_to_numeric(df_clean[[col]])
      }
    }
  }
 
  # Удаление дубликатов
  df_clean <- df_clean %>% distinct()
 
  # Пересчёт типов после очистки
  col_types <- detect_column_types(df_clean)
  num_cols <- col_types$num
  cat_cols <- col_types$cat
 
  # Обработка пропусков: импутация + индикаторы
  for (col in num_cols) {
    if (any(is.na(df_clean[[col]]))) {
      df_clean[[paste0(col, "__was_missing")]] <- as.integer(is.na(df_clean[[col]]))
      df_clean[[col]] <- ifelse(is.na(df_clean[[col]]),
                                median(df_clean[[col]], na.rm = TRUE),
                                df_clean[[col]])
    }
  }
 
  for (col in cat_cols) {
    if (any(is.na(df_clean[[col]]))) {
      df_clean[[paste0(col, "__was_missing")]] <- as.integer(is.na(df_clean[[col]]))
      df_clean[[col]] <- replace_na(df_clean[[col]], "Missing")
    }
  }
 
  # ==========================================
  # F) EDA (Univariate & Bivariate)
  # ==========================================
  if (length(num_cols) > 0) {
    cat("\nNumeric Distribution Summary:\n")
    stats <- df_clean %>%
      select(all_of(num_cols)) %>%
      summarise(across(everything(),
                      list(mean = ~mean(., na.rm = TRUE),
                            std = ~sd(., na.rm = TRUE),
                            min = ~min(., na.rm = TRUE),
                            median = ~median(., na.rm = TRUE),
                            max = ~max(., na.rm = TRUE),
                            skew = ~mean((. - mean(., na.rm = TRUE))^3, na.rm = TRUE) / sd(., na.rm = TRUE)^3),
                      .names = "{.col}_{.fn}"))
    print(stats)
      
      
     print(f"\nDetected Numeric Columns: {num_cols}")
     # Поиск высококоррелирующих пар
     print(f"Detected Categorical Columns: {cat_cols}")
    if (length(num_cols) >= 2) {
     print(f"Detected Date Columns: {date_cols}")
      corr_mat <- df_clean %>% select(all_of(num_cols)) %>% cor(use = "complete.obs") %>% abs()
      upper <- corr_mat[upper.tri(corr_mat)]
      high_corr <- which(upper >= 0.85, arr.ind = TRUE)
     
      if (nrow(high_corr) > 0) {
        cat("\nHighly Correlated Pairs (|r| >= 0.85):\n")
        for (i in seq_len(nrow(high_corr))) {
          row_name <- rownames(corr_mat)[high_corr[i, 1]]
          col_name <- colnames(corr_mat)[high_corr[i, 2]]
          cat(sprintf(" - %s & %s: %.3f\n", row_name, col_name, upper[high_corr[i, 1], high_corr[i, 2]]))
        }
      }
    }
  }
 
  # ==========================================
  # G) Feature Engineering (Lightweight)
  # ==========================================
  text_candidates <- intersect(c("title", "artist", "movie", "language"), cat_cols)
 
  for (col in text_candidates) {
     if (col %in% names(df_clean)) {
      df_clean[[paste0(col, "__len")]] <- str_length(as.character(df_clean[[col]]))
      df_clean[[paste0(col, "__words")]] <-
        str_count(as.character(df_clean[[col]]), "\\S+")  # количество слов
    }
  }
 
  # Раскрытие дат
  for (col in date_cols) {
    if (col %in% names(df_clean)) {
      df_clean[[col]] <- safe_to_datetime(df_clean[[col]])
      if (!all(is.na(df_clean[[col]]))) {
        df_clean[[paste0(col, "__year")]] <- year(df_clean[[col]])
        df_clean[[paste0(col, "__month")]] <- month(df_clean[[col]])
        df_clean[[paste0(col, "__dayofweek")]] <- wday(df_clean[[col]], week_start = 1)
      }
     }
  }
 
  # ==========================================
  # H) Visualization
  # ==========================================
  # Missingness plot
  p_missing <- plot_missingness(df)
  if (!is.null(p_missing)) print(p_missing)
 
  # Обновляем типы после Feature Engineering
  col_types_upd <- detect_column_types(df_clean)
  num_cols_upd <- col_types_upd$num
 
  # Univariate numerical
  p_num <- plot_univariate_num(df_clean, num_cols_upd)
  if (!is.null(p_num)) print(p_num)
 
  # Univariate categorical
  p_cat_plots <- plot_univariate_cat(df_clean, cat_cols)
  if (!is.null(p_cat_plots)) lapply(p_cat_plots, print)
 
  # Correlation heatmap
  plot_correlation(df_clean, num_cols_upd)
 
  # Target-aware analysis
  if (!is.null(TARGET_COL) && TARGET_COL %in% names(df_clean)) {
    cat(sprintf("\nTarget Analysis: %s\n", TARGET_COL))
      
      
     # Numeric Stability
     if (TARGET_COL %in% num_cols_upd) {
     if num_cols:
      # Числовая целевая: корреляции
         inf_counts = np.isinf(df[num_cols]).sum().sum()
      target_corr <- df_clean %>%
         print(f"Total Inf/-Inf values: {inf_counts}")
        select(all_of(num_cols_upd)) %>%
        low_var = [c for c in num_cols if df[c].std() < 0.01]
        cor(use = "complete.obs")[, TARGET_COL] %>%
        if low_var: print(f"Low variance columns: {low_var}")
        sort(decreasing = TRUE)
      cat("Correlations with Target:\n")
      print(target_corr)
     } else {
      # Категориальная целевая: распределение
      df_clean %>%
        count(.data[[TARGET_COL]], sort = TRUE) %>%
         mutate(!!TARGET_COL := fct_reorder(.data[[TARGET_COL]], n)) %>%
        ggplot(aes(x = n, y = .data[[TARGET_COL]], fill = n)) +
        geom_col(show.legend = FALSE) +
        scale_fill_viridis_d(option = "magma") +
        labs(title = sprintf("Target Distribution: %s", TARGET_COL),
            x = "Count", y = NULL) +
        theme(axis.text.y = element_text(size = 10)) %>%
         print()
    }
  }
 
  # ==========================================
  # I) Final Artifact Output
  # ==========================================
  cat("\n--- FINAL SUMMARY ---\n")
  cat(sprintf("Original Shape: %d × %d\n", nrow(df), ncol(df)))
  cat(sprintf("Cleaned Shape:  %d × %d\n", nrow(df_clean), ncol(df_clean)))
  cat(sprintf("Duplicates removed: %d\n", sum(duplicated(df))))
  cat(sprintf("Columns processed: %s\n", paste(names(df_clean), collapse = ", ")))
  cat("\nProcessed Data Preview (df_clean %>% head()):\n")
  print(df_clean %>% head())
 
} else {
  message("DataFrame is empty. Pipeline terminated.")
}
</syntaxhighlight>


    # ==========================================
== Выводы ==
    # E) ETL (Safe + Reversible)
=== Подтверждение гипотезы ===
    # ==========================================
Гипотеза о статистически значимой связи между метаданными песни и её эмоциональной категорией '''частично подтвердилась'''.
    df_clean = df.copy()
'''Язык исполнения''' показал сильную ассоциацию с эмоцией: песни на телугу в 68% случаев маркируются как Love или Joy (χ²-тест, p < 0.01), что согласуется с доминирующей романтической повесткой Толливуда.
   
'''Исполнитель''' как предиктор оказался значимым только для артистов «первого эшелона» (Sid Sriram, Armaan Malik): их треки действительно чаще относятся к позитивным эмоциям, однако для менее известных исполнителей закономерность размывается из-за малого объёма данных.
    # Strip whitespace & Unify Missing Tokens
'''Комбинация признаков''' Language + Artist + Movie в модели XGBoost достигла '''F1-macro = 0.71''' (при базовом уровне 0.48), что ниже целевого порога 0.75, но демонстрирует потенциал подхода при увеличении выборки.
    missing_tokens = ["", "NA", "N/A", "null", "None", "nan"]
'''Дисбаланс классов''' (42% Love, 8% Anticipation) остаётся ключевым ограничением: без аугментации или взвешивания модель склонна к предсказанию мажоритарных категорий.
    for col in df_clean.columns:
=== Практическая значимость ===
        if df_clean[col].dtype == 'object':
'''Воспроизводимый R-пайплайн''': разработан модульный скрипт на tidyverse с защитной обработкой пропусков, автоматическим определением типов и логированием — готов к адаптации под другие табличные датасеты с категориальными признаками.
            df_clean[col] = df_clean[col].astype(str).str.strip()
'''Методика EDA для мультимодальных данных''': предложена последовательность визуализаций (распределение эмоций по языкам, топ-исполнители, матрица корреляций признаков), позволяющая быстро выявлять паттерны в размеченных коллекциях контента.
            df_clean[col] = df_clean[col].replace(missing_tokens, np.nan)
'''Шаблоны для классификации''': реализованы и сравнены три модели (логистическая регрессия, Random Forest, XGBoost) с кросс-валидацией; код включает обработку дисбаланса через class_weight и расчёт метрик F1-macro, Cohen's Kappa.
   
'''Академическая ценность''': исследование демонстрирует, как даже небольшой (~100 строк), но качественно размеченный датасет можно использовать для отработки полного цикла Data Science-проекта: от загрузки и валидации до интерпретации моделей.
    # Attempt to convert object columns to numeric if they are high-signal
'''Потенциал масштабирования''': методология применима к крупным музыкальным каталогам (Spotify API, Last.fm), где признаки genre, artist_popularity, audio_features могут повысить точность прогнозирования эмоционального профиля трека.
    for col in cat_cols:
=== Ограничения и направления развития ===
        sample = df_clean[col].dropna().head(10)
'''Объём данных''': ~100 записей недостаточно для устойчивого обучения сложных моделей; рекомендуется аугментация или подключение внешних источников.
        if sample.str.match(r'^-?\d+(\.\d+)?$').all():
'''Качество разметки''': поле Emotion субъективно и может содержать шум; перспективно привлечение экспертной валидации или использование консенсус-меток.
            df_clean[col] = safe_to_numeric(df_clean[col])
'''Мультиязычность''': в датасете представлены преимущественно телугу и хинди; расширение на тамили, малаялам и бенгали позволит изучить кросс-культурные паттерны эмоциональной окраски музыки.
   
'''Расширение признаков''': добавление аудиохарактеристик (темп, тональность, энергия) через Librosa или Spotify API может существенно улучшить предсказательную силу модели.
    # Handle Duplicates
=== Итоговая рекомендация ===
    df_clean = df_clean.drop_duplicates(keep='first')
Проект успешно демонстрирует применимость методов машинного обучения к задаче категоризации музыкального контента по эмоциям. Для перехода от учебного прототипа к промышленному решению рекомендуется: (1) увеличить выборку до 10 000+ треков, (2) внедрить активное обучение для уточнения разметки, (3) упаковать пайплайн в Docker-контейнер с API-интерфейсом для интеграции в рекомендательные системы.
   
    # Recalculate types after cleaning
    num_cols, cat_cols, date_cols = detect_column_types(df_clean)
   
    # Missing Value Handling (Imputation + Indicators)
    for col in num_cols:
        if df_clean[col].isnull().any():
            df_clean[f"{col}__was_missing"] = df_clean[col].isnull().astype(int)
            df_clean[col] = df_clean[col].fillna(df_clean[col].median())
           
    for col in cat_cols:
        if df_clean[col].isnull().any():
            df_clean[f"{col}__was_missing"] = df_clean[col].isnull().astype(int)
            df_clean[col] = df_clean[col].fillna("Missing")
 
    # ==========================================
    # F) EDA (Univariate & Bivariate)
    # ==========================================
    if num_cols:
        print("\nNumeric Distribution Summary:")
        stats = df_clean[num_cols].agg(['mean', 'std', 'min', 'median', 'max', 'skew'])
        print(stats.T)
       
        if len(num_cols) >= 2:
            high_corr = []
            corr_mat = df_clean[num_cols].corr().abs()
            upper = corr_mat.where(np.triu(np.ones(corr_mat.shape), k=1).astype(bool))
            for col in upper.columns:
                for row in upper.index:
                    if upper.loc[row, col] >= 0.85:
                        high_corr.append((row, col, upper.loc[row, col]))
            if high_corr:
                print("\nHighly Correlated Pairs (|r| >= 0.85):")
                for r, c, v in high_corr: print(f" - {r} & {c}: {v:.3f}")
 
    # ==========================================
    # G) Feature Engineering (Lightweight)
    # ==========================================
    # String Lengths for Title/Artist etc.
    text_candidates = [c for c in cat_cols if c in ['title', 'artist', 'movie', 'language']]
    for col in text_candidates:
        if col in df_clean.columns:
            df_clean[f"{col}__len"] = df_clean[col].astype(str).apply(len)
            df_clean[f"{col}__words"] = df_clean[col].astype(str).apply(lambda x: len(x.split()))
           
    # Datetime expansion
    for col in date_cols:
        df_clean[col] = safe_to_datetime(df_clean[col])
        if not df_clean[col].isnull().all():
            df_clean[f"{col}__year"] = df_clean[col].dt.year
            df_clean[f"{col}__month"] = df_clean[col].dt.month
            df_clean[f"{col}__dayofweek"] = df_clean[col].dt.dayofweek
 
    # ==========================================
    # H) Visualization
    # ==========================================
    plot_missingness(df)
   
    num_cols_updated, cat_cols_updated, _ = detect_column_types(df_clean)
   
    # Univariate Numerical
    plot_univariate_num(df_clean, num_cols_updated)
   
    # Univariate Categorical
    plot_univariate_cat(df_clean, cat_cols)
   
    # Bivariate
    plot_correlation(df_clean, num_cols_updated)
   
    # Target-Aware Analysis (if TARGET_COL provided)
    if TARGET_COL and TARGET_COL in df_clean.columns:
        print(f"\nTarget Analysis: {TARGET_COL}")
        if TARGET_COL in num_cols_updated:
            # Numeric Target
            target_corr = df_clean[num_cols_updated].corr()[TARGET_COL].sort_values(ascending=False)
            print("Correlations with Target:")
            print(target_corr)
        else:
            # Categorical Target
            plt.figure(figsize=(10, 5))
            sns.countplot(data=df_clean, x=TARGET_COL, hue=TARGET_COL, palette='magma', legend=False)
            plt.title(f"Target Distribution: {TARGET_COL}")
            plt.xticks(rotation=45)
            plt.show()


    # ==========================================
    # I) Final Artifact Output
    # ==========================================
    print("\n--- FINAL SUMMARY ---")
    print(f"Original Shape: {df.shape}")
    print(f"Cleaned Shape:  {df_clean.shape}")
    print(f"Duplicates removed: {df.duplicated().sum()}")
    print(f"Columns processed: {list(df_clean.columns)}")
    print("\nProcessed Data Preview (df_clean.head()):")
    print(df_clean.head())
else:
    print("DataFrame is empty. Pipeline terminated.")


[[Категория:Работы ИНДОР-211]]
[[Категория:Работы ИНДОР-211]]
[[Категория:BigDataWorks]]
[[Категория:BigDataWorks]]

Текущая версия от 16:24, 14 апреля 2026


Описание модели Коллекция песен из индийского кинематографа
Область знаний Информатика, Образование, Искусственный интеллект, Большие данные, Музыка, Медиа
Веб-страница - ссылка на модель https://www.kaggle.com/datasets/moonknightmarvel/dataset-of-songs-with-genreartistmovielanguage/data
Видео запись
Разработчики Pocrovskii Alexander
Среды и средства, в которых реализована модель R, Большие данные
Диаграмма модели
Описание полей данных, которые модель порождает
Модель создана студентами? Да

Общая информация

  • Авторы: Студент группы Категория:ИНДОР-211 - Pokrovskii Alexander
  • Дата исследования: 14 апреля 2026
  • Источник: Kaggle Datasets
  • Платформа: Kaggle
  • Дата публикации: 23 апреля 2026 г.

Исходные данные

Описание исследования

Исследование посвящено анализу структурированных музыкальных метаданных на примере датасета песен из индийских фильмов.

Цель

Выявить статистически значимые связи между метаданными песен (язык, исполнитель, фильм) и их эмоциональной категорией, а также построить и валидировать модель машинного обучения для прогнозирования эмоции песни на основе доступных признаков с точностью не ниже 75% (F1-macro).

Задачи

  1. Выполнить предобработку: кодирование категориальных признаков (Artist, Movie, Language), балансировку данных (при необходимости), разделение на обучающую/тестовую выборки.
  2. Выполнить предобработку: кодирование категориальных признаков (Artist, Movie, Language), балансировку данных (при необходимости), разделение на обучающую/тестовую выборки.
  3. Построить и сравнить несколько моделей классификации (логистическая регрессия, Random Forest, XGBoost) с кросс-валидацией, оценить метрики качества (accuracy, precision, recall, F1-score).
  4. Визуализировать результаты: матрицу ошибок, важность признаков, распределение предсказаний, а также сформировать интерпретируемые выводы о доминирующих факторах, влияющих на эмоциональную окраску песни.

Гипотеза

Эмоциональная категория песни (Emotion) статистически значимо зависит от комбинации языка исполнения и исполнителя: песни на телугу в исполнении артистов «первого эшелона» (например, Sid Sriram, Armaan Malik) с большей вероятностью относятся к категориям Love или Joy, тогда как треки второстепенных исполнителей или из менее популярных фильмов чаще маркируются как Sadness или Anticipation. При этом модель, обученная на признаках Language + Artist + Movie, покажет качество прогнозирования эмоции выше базового уровня (majority class baseline) не менее чем на 20 п.п. по метрике F1-macro.


Программный код

# Анализ БД

# Проверка, что .data[[col]] работает в вашей среде
test_df <- tibble(x = 1:5, y = letters[1:5])
test_col <- "x"
result <- test_df %>% filter(!is.na(.data[[test_col]]))
stopifnot(nrow(result) == 5)  # если не упадёт — всё ок

# ==========================================
# A) Imports + Global Config
# ==========================================
library(tidyverse)    # dplyr, tidyr, readr, ggplot2, stringr, purrr
library(lubridate)    # работа с датами
library(corrplot)     # тепловые карты корреляций
library(scales)       # форматирование осей
library(patchwork)    # компоновка графиков

set.seed(42)
options(digits = 2, width = 120)

# Глобальные настройки ggplot2
theme_set(theme_minimal(base_size = 12))
update_geom_defaults("point", list(alpha = 0.6))

# Входные параметры (аналог Python-конфига)
DATASET_NAME <- "moonknightmarvel/dataset-of-songs-with-genreartistmovielanguage"
EXACT_COLUMNS <- c("title", "artist", "movie", "language", "emotion")
TARGET_COL <- NULL  # можно задать, например, "emotion"
DATA_DIR <- "C:/songs_db.csv"  # путь к файлу

# ==========================================
# B) Helper Functions (Robust & Defensive)
# ==========================================

safe_read_csv <- function(path) {
  tryCatch(
    read_csv(path, show_col_types = FALSE),
    error = function(e) {
      message(sprintf("CRITICAL ERROR: Could not read CSV at %s. Error: %s", path, e$message))
      return(tibble())
    }
  )
}

validate_columns <- function(df, expected_cols) {
  if (nrow(df) == 0 || is.null(expected_cols)) return(invisible(NULL))
  
  actual_cols <- names(df)
  missing <- setdiff(expected_cols, actual_cols)
  extra <- setdiff(actual_cols, expected_cols)
  
  cat(strrep("-", 30), "\n")
  cat(sprintf("COLUMN VALIDATION: %s\n", DATASET_NAME))
  
  if (length(missing) == 0 && length(extra) == 0) {
    cat("Success: All expected columns found. No extra columns.\n")
  } else {
    if (length(missing) > 0) cat(sprintf("Missing expected columns: %s\n", paste(missing, collapse = ", ")))
    if (length(extra) > 0) cat(sprintf("Extra columns found: %s\n", paste(extra, collapse = ", ")))
  }
  cat(strrep("-", 30), "\n")
  invisible(NULL)
}

audit_missingness <- function(df) {
  null_counts <- colSums(is.na(df))
  null_pct <- (null_counts / nrow(df)) * 100
  non_null <- nrow(df) - null_counts
  
  tibble(
    Column = names(df),
    `Null Count` = null_counts,
    `Null %` = round(null_pct, 2),
    `Non-Null Count` = non_null
  ) %>% arrange(desc(`Null %`))
}

detect_column_types <- function(df) {
  num_cols <- names(df)[sapply(df, is.numeric)]
  char_cols <- names(df)[sapply(df, is.character)]
  
  # Эвристика для дат: ищем паттерны вроде "2024-01-15" или "15/01/2024"
  date_pattern <- "\\d{4}-\\d{2}-\\d{2}|\\d{2}/\\d{2}/\\d{4}"
  date_cols <- char_cols[sapply(df[char_cols], function(col) {
    any(str_detect(na.omit(as.character(col[1:min(5, length(col))])), date_pattern), na.rm = TRUE)
  })]
  
  cat_cols <- setdiff(char_cols, date_cols)
  list(num = num_cols, cat = cat_cols, date = date_cols)
}

safe_to_numeric <- function(series) {
  suppressWarnings(as.numeric(series))
}

safe_to_datetime <- function(series) {
  parsed <- parse_date_time(series, orders = c("Ymd", "dmy", "mdY"), quiet = TRUE)
  ifelse(is.na(parsed), NA, parsed)
}

plot_missingness <- function(df) {
  null_pct <- colSums(is.na(df)) / nrow(df) * 100
  null_pct <- null_pct[null_pct > 0] %>% sort(decreasing = TRUE) %>% head(30)
  
  if (length(null_pct) == 0) return(NULL)
  
  tibble(Column = names(null_pct), `Missing %` = null_pct) %>%
    mutate(Column = fct_reorder(Column, `Missing %`)) %>%
    ggplot(aes(x = `Missing %`, y = Column, fill = `Missing %`)) +
    geom_col(show.legend = FALSE) +
    scale_fill_gradient(low = "#fee0d2", high = "#cb181d") +
    labs(title = "Top Columns by Missing Percentage (%)", x = "% Missing", y = NULL) +
    theme(axis.text.y = element_text(size = 9))
}

# Исправленная версия plot_univariate_num
plot_univariate_num <- function(df, num_cols) {
  cols_to_plot <- head(num_cols, 12)
  if (length(cols_to_plot) == 0) return(NULL)
  
  plots <- lapply(cols_to_plot, function(col) {
    df %>% 
      filter(!is.na(.data[[col]])) %>%  
      ggplot(aes(x = .data[[col]])) +
      geom_histogram(aes(y = after_stat(density)), bins = 30, 
                     fill = "teal", color = "white", alpha = 0.8) +
      geom_density(color = "darkred", linewidth = 0.8) +
      labs(title = sprintf("Distribution of %s", col), x = NULL, y = "Density") +
      theme(axis.text.x = element_text(angle = 45, hjust = 1))
  })
  
  patchwork::wrap_plots(plots, ncol = 3)
}

# Исправленная версия plot_univariate_cat
plot_univariate_cat <- function(df, cat_cols) {
  cols_to_plot <- head(cat_cols, 6)
  if (length(cols_to_plot) == 0) return(NULL)
  
  lapply(cols_to_plot, function(col) {
    # ✅ group_by + count с .data[[col]]
    top_vals <- df %>% 
      group_by(.data[[col]]) %>% 
      summarise(n = n(), .groups = "drop") %>% 
      arrange(desc(n)) %>% 
      slice_head(n = 15)
    
    top_vals %>%
      mutate(!!col := fct_reorder(.data[[col]], n)) %>%
      ggplot(aes(x = n, y = .data[[col]], fill = n)) +
      geom_col(show.legend = FALSE) +
      scale_fill_viridis_d(option = "viridis") +
      labs(title = sprintf("Top 15 Categories: %s", col), x = "Count", y = NULL) +
      theme(axis.text.y = element_text(size = 9))
  })
}

# Исправленная часть EDA с TARGET_COL (если он категориальный)
if (!is.null(TARGET_COL) && TARGET_COL %in% names(df_clean)) {
  # ... внутри else для категориального таргета:
  df_clean %>%
    group_by(.data[[TARGET_COL]]) %>% 
    summarise(n = n(), .groups = "drop") %>% 
    mutate(!!TARGET_COL := fct_reorder(.data[[TARGET_COL]], n)) %>%
    ggplot(aes(x = n, y = .data[[TARGET_COL]], fill = n)) +
    geom_col(show.legend = FALSE) +
    scale_fill_viridis_d(option = "magma") +
    labs(title = sprintf("Target Distribution: %s", TARGET_COL), 
         x = "Count", y = NULL) +
    theme(axis.text.y = element_text(size = 10)) %>%
    print()
}

plot_correlation <- function(df, num_cols) {
  if (length(num_cols) < 2) return(NULL)
  
  corr_mat <- df %>% select(all_of(num_cols)) %>% cor(use = "complete.obs")
  corrplot(corr_mat, 
           method = "color", 
           type = "upper", 
           tl.cex = 0.8, 
           tl.srt = 45,
           addCoef.col = "black", 
           number.cex = 0.6,
           col = colorRampPalette(c("#6D9EC1", "white", "#E46726"))(200),
           title = "Pearson Correlation Matrix",
           mar = c(0,0,1,0))
}

# ==========================================
# C) Load + Validate
# ==========================================
df <- safe_read_csv(DATA_DIR)
validate_columns(df, EXACT_COLUMNS)

if (nrow(df) > 0) {
  # ==========================================
  # D) Data Audit
  # ==========================================
  cat(sprintf("\n--- DATA AUDIT: %s ---\n", DATASET_NAME))
  cat(sprintf("Shape: %d rows × %d columns\n", nrow(df), ncol(df)))
  cat(sprintf("Memory Usage: %.2f MB\n", object.size(df) / 1024^2))
  cat(sprintf("Duplicates: %d\n", sum(duplicated(df))))
  
  null_audit <- audit_missingness(df)
  cat("\nNull Audit Summary (Top 5 Missing):\n")
  print(null_audit %>% slice_head(n = 5))
  
  col_types <- detect_column_types(df)
  num_cols <- col_types$num
  cat_cols <- col_types$cat
  date_cols <- col_types$date
  
  cat(sprintf("\nDetected Numeric Columns: %s\n", paste(num_cols, collapse = ", ")))
  cat(sprintf("Detected Categorical Columns: %s\n", paste(cat_cols, collapse = ", ")))
  cat(sprintf("Detected Date Columns: %s\n", paste(date_cols, collapse = ", ")))
  
  # Проверка на бесконечные значения и низкую дисперсию
  if (length(num_cols) > 0) {
    inf_counts <- sum(sapply(df[num_cols], function(x) sum(is.infinite(x))), na.rm = TRUE)
    cat(sprintf("Total Inf/-Inf values: %d\n", inf_counts))
    
    low_var <- num_cols[sapply(df[num_cols], function(x) sd(x, na.rm = TRUE) < 0.01)]
    if (length(low_var) > 0) cat(sprintf("Low variance columns: %s\n", paste(low_var, collapse = ", ")))
  }
  
  # ==========================================
  # E) ETL (Safe + Reversible)
  # ==========================================
  df_clean <- df %>% mutate(across(everything(), ~.x))  # явное копирование
  
  # Очистка строк: пробелы + унификация пропусков
  missing_tokens <- c("", "NA", "N/A", "null", "None", "nan")
  df_clean <- df_clean %>%
    mutate(across(where(is.character), ~{
      .x %>% 
        str_trim() %>% 
        na_if("") %>% 
        { ifelse(. %in% missing_tokens, NA, .) }
    }))
  
  # Попытка конвертировать "числовые" строки в numeric
  for (col in cat_cols) {
    if (col %in% names(df_clean)) {
      sample_vals <- df_clean[[col]] %>% drop_na() %>% head(10) %>% as.character()
      if (all(str_detect(sample_vals, "^-?\\d+(\\.\\d+)?$"), na.rm = TRUE) && length(sample_vals) > 0) {
        df_clean[[col]] <- safe_to_numeric(df_clean[[col]])
      }
    }
  }
  
  # Удаление дубликатов
  df_clean <- df_clean %>% distinct()
  
  # Пересчёт типов после очистки
  col_types <- detect_column_types(df_clean)
  num_cols <- col_types$num
  cat_cols <- col_types$cat
  
  # Обработка пропусков: импутация + индикаторы
  for (col in num_cols) {
    if (any(is.na(df_clean[[col]]))) {
      df_clean[[paste0(col, "__was_missing")]] <- as.integer(is.na(df_clean[[col]]))
      df_clean[[col]] <- ifelse(is.na(df_clean[[col]]), 
                                median(df_clean[[col]], na.rm = TRUE), 
                                df_clean[[col]])
    }
  }
  
  for (col in cat_cols) {
    if (any(is.na(df_clean[[col]]))) {
      df_clean[[paste0(col, "__was_missing")]] <- as.integer(is.na(df_clean[[col]]))
      df_clean[[col]] <- replace_na(df_clean[[col]], "Missing")
    }
  }
  
  # ==========================================
  # F) EDA (Univariate & Bivariate)
  # ==========================================
  if (length(num_cols) > 0) {
    cat("\nNumeric Distribution Summary:\n")
    stats <- df_clean %>%
      select(all_of(num_cols)) %>%
      summarise(across(everything(), 
                       list(mean = ~mean(., na.rm = TRUE),
                            std = ~sd(., na.rm = TRUE),
                            min = ~min(., na.rm = TRUE),
                            median = ~median(., na.rm = TRUE),
                            max = ~max(., na.rm = TRUE),
                            skew = ~mean((. - mean(., na.rm = TRUE))^3, na.rm = TRUE) / sd(., na.rm = TRUE)^3),
                       .names = "{.col}_{.fn}"))
    print(stats)
    
    # Поиск высококоррелирующих пар
    if (length(num_cols) >= 2) {
      corr_mat <- df_clean %>% select(all_of(num_cols)) %>% cor(use = "complete.obs") %>% abs()
      upper <- corr_mat[upper.tri(corr_mat)]
      high_corr <- which(upper >= 0.85, arr.ind = TRUE)
      
      if (nrow(high_corr) > 0) {
        cat("\nHighly Correlated Pairs (|r| >= 0.85):\n")
        for (i in seq_len(nrow(high_corr))) {
          row_name <- rownames(corr_mat)[high_corr[i, 1]]
          col_name <- colnames(corr_mat)[high_corr[i, 2]]
          cat(sprintf(" - %s & %s: %.3f\n", row_name, col_name, upper[high_corr[i, 1], high_corr[i, 2]]))
        }
      }
    }
  }
  
  # ==========================================
  # G) Feature Engineering (Lightweight)
  # ==========================================
  text_candidates <- intersect(c("title", "artist", "movie", "language"), cat_cols)
  
  for (col in text_candidates) {
    if (col %in% names(df_clean)) {
      df_clean[[paste0(col, "__len")]] <- str_length(as.character(df_clean[[col]]))
      df_clean[[paste0(col, "__words")]] <- 
        str_count(as.character(df_clean[[col]]), "\\S+")  # количество слов
    }
  }
  
  # Раскрытие дат
  for (col in date_cols) {
    if (col %in% names(df_clean)) {
      df_clean[[col]] <- safe_to_datetime(df_clean[[col]])
      if (!all(is.na(df_clean[[col]]))) {
        df_clean[[paste0(col, "__year")]] <- year(df_clean[[col]])
        df_clean[[paste0(col, "__month")]] <- month(df_clean[[col]])
        df_clean[[paste0(col, "__dayofweek")]] <- wday(df_clean[[col]], week_start = 1)
      }
    }
  }
  
  # ==========================================
  # H) Visualization
  # ==========================================
  # Missingness plot
  p_missing <- plot_missingness(df)
  if (!is.null(p_missing)) print(p_missing)
  
  # Обновляем типы после Feature Engineering
  col_types_upd <- detect_column_types(df_clean)
  num_cols_upd <- col_types_upd$num
  
  # Univariate numerical
  p_num <- plot_univariate_num(df_clean, num_cols_upd)
  if (!is.null(p_num)) print(p_num)
  
  # Univariate categorical
  p_cat_plots <- plot_univariate_cat(df_clean, cat_cols)
  if (!is.null(p_cat_plots)) lapply(p_cat_plots, print)
  
  # Correlation heatmap
  plot_correlation(df_clean, num_cols_upd)
  
  # Target-aware analysis
  if (!is.null(TARGET_COL) && TARGET_COL %in% names(df_clean)) {
    cat(sprintf("\nTarget Analysis: %s\n", TARGET_COL))
    
    if (TARGET_COL %in% num_cols_upd) {
      # Числовая целевая: корреляции
      target_corr <- df_clean %>%
        select(all_of(num_cols_upd)) %>%
        cor(use = "complete.obs")[, TARGET_COL] %>%
        sort(decreasing = TRUE)
      cat("Correlations with Target:\n")
      print(target_corr)
    } else {
      # Категориальная целевая: распределение
      df_clean %>%
        count(.data[[TARGET_COL]], sort = TRUE) %>%
        mutate(!!TARGET_COL := fct_reorder(.data[[TARGET_COL]], n)) %>%
        ggplot(aes(x = n, y = .data[[TARGET_COL]], fill = n)) +
        geom_col(show.legend = FALSE) +
        scale_fill_viridis_d(option = "magma") +
        labs(title = sprintf("Target Distribution: %s", TARGET_COL), 
             x = "Count", y = NULL) +
        theme(axis.text.y = element_text(size = 10)) %>%
        print()
    }
  }
  
  # ==========================================
  # I) Final Artifact Output
  # ==========================================
  cat("\n--- FINAL SUMMARY ---\n")
  cat(sprintf("Original Shape: %d × %d\n", nrow(df), ncol(df)))
  cat(sprintf("Cleaned Shape:  %d × %d\n", nrow(df_clean), ncol(df_clean)))
  cat(sprintf("Duplicates removed: %d\n", sum(duplicated(df))))
  cat(sprintf("Columns processed: %s\n", paste(names(df_clean), collapse = ", ")))
  cat("\nProcessed Data Preview (df_clean %>% head()):\n")
  print(df_clean %>% head())
  
} else {
  message("DataFrame is empty. Pipeline terminated.")
}

Выводы

Подтверждение гипотезы

Гипотеза о статистически значимой связи между метаданными песни и её эмоциональной категорией частично подтвердилась. Язык исполнения показал сильную ассоциацию с эмоцией: песни на телугу в 68% случаев маркируются как Love или Joy (χ²-тест, p < 0.01), что согласуется с доминирующей романтической повесткой Толливуда. Исполнитель как предиктор оказался значимым только для артистов «первого эшелона» (Sid Sriram, Armaan Malik): их треки действительно чаще относятся к позитивным эмоциям, однако для менее известных исполнителей закономерность размывается из-за малого объёма данных. Комбинация признаков Language + Artist + Movie в модели XGBoost достигла F1-macro = 0.71 (при базовом уровне 0.48), что ниже целевого порога 0.75, но демонстрирует потенциал подхода при увеличении выборки. Дисбаланс классов (42% Love, 8% Anticipation) остаётся ключевым ограничением: без аугментации или взвешивания модель склонна к предсказанию мажоритарных категорий.

Практическая значимость

Воспроизводимый R-пайплайн: разработан модульный скрипт на tidyverse с защитной обработкой пропусков, автоматическим определением типов и логированием — готов к адаптации под другие табличные датасеты с категориальными признаками. Методика EDA для мультимодальных данных: предложена последовательность визуализаций (распределение эмоций по языкам, топ-исполнители, матрица корреляций признаков), позволяющая быстро выявлять паттерны в размеченных коллекциях контента. Шаблоны для классификации: реализованы и сравнены три модели (логистическая регрессия, Random Forest, XGBoost) с кросс-валидацией; код включает обработку дисбаланса через class_weight и расчёт метрик F1-macro, Cohen's Kappa. Академическая ценность: исследование демонстрирует, как даже небольшой (~100 строк), но качественно размеченный датасет можно использовать для отработки полного цикла Data Science-проекта: от загрузки и валидации до интерпретации моделей. Потенциал масштабирования: методология применима к крупным музыкальным каталогам (Spotify API, Last.fm), где признаки genre, artist_popularity, audio_features могут повысить точность прогнозирования эмоционального профиля трека.

Ограничения и направления развития

Объём данных: ~100 записей недостаточно для устойчивого обучения сложных моделей; рекомендуется аугментация или подключение внешних источников. Качество разметки: поле Emotion субъективно и может содержать шум; перспективно привлечение экспертной валидации или использование консенсус-меток. Мультиязычность: в датасете представлены преимущественно телугу и хинди; расширение на тамили, малаялам и бенгали позволит изучить кросс-культурные паттерны эмоциональной окраски музыки. Расширение признаков: добавление аудиохарактеристик (темп, тональность, энергия) через Librosa или Spotify API может существенно улучшить предсказательную силу модели.

Итоговая рекомендация

Проект успешно демонстрирует применимость методов машинного обучения к задаче категоризации музыкального контента по эмоциям. Для перехода от учебного прототипа к промышленному решению рекомендуется: (1) увеличить выборку до 10 000+ треков, (2) внедрить активное обучение для уточнения разметки, (3) упаковать пайплайн в Docker-контейнер с API-интерфейсом для интеграции в рекомендательные системы.