1 Задание 1 — Свой датасет с NA значениями

Задание: Сформируйте собственный датасет с помощью функции c(), содержащий числовые данные и NA значения.

# c() — "concatenate", склеивает элементы в вектор
# NA — специальное значение "данных нет" (Not Available)

my_vector <- c(10, 25, NA, 47, NA, 63, 8, NA, 99, 12)

print(my_vector)
##  [1] 10 25 NA 47 NA 63  8 NA 99 12
cat("Количество элементов:", length(my_vector), "\n")
## Количество элементов: 10
# summary() даёт краткую сводку: мин, макс, среднее, и сколько NA
summary(my_vector)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.     NAs 
##    8.00   11.00   25.00   37.71   55.00   99.00       3

2 Задание 2 — Очистка данных через is.na()

Задание: Проведите очистку данных с использованием функции is.na() и выведите “чистый” датасет.

# is.na() возвращает TRUE там где NA, и FALSE там где данные есть
is.na(my_vector)
##  [1] FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE
# ! означает "НЕ". Так берём только те элементы, где НЕ NA
clean_vector <- my_vector[!is.na(my_vector)]

cat("=== Исходный вектор (с пропусками) ===\n")
## === Исходный вектор (с пропусками) ===
print(my_vector)
##  [1] 10 25 NA 47 NA 63  8 NA 99 12
cat("\n=== Чистый вектор (без NA) ===\n")
## 
## === Чистый вектор (без NA) ===
print(clean_vector)
## [1] 10 25 47 63  8 99 12
cat("\nБыло элементов:", length(my_vector))
## 
## Было элементов: 10
cat("\nСтало элементов:", length(clean_vector))
## 
## Стало элементов: 7
cat("\nУдалено NA:", sum(is.na(my_vector)), "\n")
## 
## Удалено NA: 3

3 Задание 3 — Таблица данных и очистка через complete.cases()

Задание: Сгенерируйте таблицу данных с числовыми и текстовыми столбцами. Очистите данные с помощью complete.cases().

# data.frame() создаёт таблицу — основной формат данных в R
df_raw <- data.frame(
  name   = c("Alex", "Maria", NA, "Ivan", "Olga", NA, "Petr"),
  age    = c(25, NA, 30, 45, NA, 28, 33),
  salary = c(50000, 60000, NA, 80000, 55000, NA, 70000)
)

cat("=== Исходная таблица ===\n")
## === Исходная таблица ===
print(df_raw)
##    name age salary
## 1  Alex  25  50000
## 2 Maria  NA  60000
## 3  <NA>  30     NA
## 4  Ivan  45  80000
## 5  Olga  NA  55000
## 6  <NA>  28     NA
## 7  Petr  33  70000
# complete.cases() возвращает TRUE для строк, где ВСЕ столбцы заполнены (нет ни одного NA)
complete_mask <- complete.cases(df_raw)

cat("\n=== Маска полных строк (TRUE = строка без NA) ===\n")
## 
## === Маска полных строк (TRUE = строка без NA) ===
print(complete_mask)
## [1]  TRUE FALSE FALSE  TRUE FALSE FALSE  TRUE
df_clean <- df_raw[complete_mask, ]

cat("\n=== Чистая таблица ===\n")
## 
## === Чистая таблица ===
print(df_clean)
##   name age salary
## 1 Alex  25  50000
## 4 Ivan  45  80000
## 7 Petr  33  70000
cat("\nИсходно строк:", nrow(df_raw))
## 
## Исходно строк: 7
cat("\nПосле очистки:", nrow(df_clean), "\n")
## 
## После очистки: 3

4 Задание 4 — Заполнение пропусков через preProcess (пакет caret)

Задание: Проанализируйте датасет airquality с пропусками. Заполните пропуски предсказанными значениями (среднее, медиана) с помощью preProcess из пакета caret.

# install.packages("caret")  # раскомментируй если пакет не установлен
library(caret)

# airquality — встроенный датасет R, данные о качестве воздуха в Нью-Йорке
# Содержит пропуски в столбцах Ozone и Solar.R

cat("=== Первые 10 строк airquality ===\n")
## === Первые 10 строк airquality ===
head(airquality, 10)
##    Ozone Solar.R Wind Temp Month Day
## 1     41     190  7.4   67     5   1
## 2     36     118  8.0   72     5   2
## 3     12     149 12.6   74     5   3
## 4     18     313 11.5   62     5   4
## 5     NA      NA 14.3   56     5   5
## 6     28      NA 14.9   66     5   6
## 7     23     299  8.6   65     5   7
## 8     19      99 13.8   59     5   8
## 9      8      19 20.1   61     5   9
## 10    NA     194  8.6   69     5  10
cat("\n=== Количество NA по каждому столбцу ===\n")
## 
## === Количество NA по каждому столбцу ===
colSums(is.na(airquality))
##   Ozone Solar.R    Wind    Temp   Month     Day 
##      37       7       0       0       0       0
# --- Заполнение МЕДИАНОЙ ---
# preProcess() создаёт объект обработки, который запоминает параметры трансформации
# method = "medianImpute" — заполнить пропуски медианой (устойчивее к выбросам, чем среднее)
prep_median <- preProcess(airquality, method = "medianImpute")

# predict() применяет объект к данным и возвращает заполненный датафрейм
data_median <- predict(prep_median, airquality)

cat("\n=== После заполнения МЕДИАНОЙ — NA остались? ===\n")
## 
## === После заполнения МЕДИАНОЙ — NA остались? ===
colSums(is.na(data_median))
##   Ozone Solar.R    Wind    Temp   Month     Day 
##       0       0       0       0       0       0
# --- Заполнение СРЕДНИМ (вручную) ---
# caret не имеет отдельного "meanImpute", поэтому делаем сами
data_mean <- airquality

for (col in names(data_mean)) {
  if (is.numeric(data_mean[[col]])) {
    col_mean <- mean(data_mean[[col]], na.rm = TRUE)  # na.rm=TRUE — игнорируем NA при расчёте
    data_mean[[col]][is.na(data_mean[[col]])] <- col_mean
  }
}

cat("\n=== После заполнения СРЕДНИМ — NA остались? ===\n")
## 
## === После заполнения СРЕДНИМ — NA остались? ===
colSums(is.na(data_mean))
##   Ozone Solar.R    Wind    Temp   Month     Day 
##       0       0       0       0       0       0
# Сравниваем результаты на первых 10 строках столбца Ozone
cat("\n=== Сравнение значений Ozone (первые 10 строк) ===\n")
## 
## === Сравнение значений Ozone (первые 10 строк) ===
comparison <- data.frame(
  Original = airquality$Ozone[1:10],
  Median   = data_median$Ozone[1:10],
  Mean     = round(data_mean$Ozone[1:10], 1)
)
print(comparison)
##    Original Median Mean
## 1        41   41.0 41.0
## 2        36   36.0 36.0
## 3        12   12.0 12.0
## 4        18   18.0 18.0
## 5        NA   31.5 42.1
## 6        28   28.0 28.0
## 7        23   23.0 23.0
## 8        19   19.0 19.0
## 9         8    8.0  8.0
## 10       NA   31.5 42.1

5 Задание 5 — Обнаружение и удаление выбросов через boxplot

Задание: Сгенерируйте два числовых набора данных с выбросами. Обнаружьте и удалите выбросы с помощью boxplot.

# set.seed() фиксирует случайность — чтобы каждый раз получались одинаковые числа
set.seed(42)

# rnorm() генерирует числа из нормального распределения
# Специально добавляем несколько очень больших и маленьких значений — это выбросы
set1 <- c(rnorm(50, mean = 100, sd = 10), 200, 250, -50)
set2 <- c(rnorm(50, mean = 50,  sd = 5),  150, -20, 160)

# Рисуем боксплоты ДО очистки
par(mfrow = c(1, 2))  # два графика рядом

boxplot(set1, main = "Набор 1 (до очистки)", ylab = "Значение",
        col = "lightblue", border = "navy")

boxplot(set2, main = "Набор 2 (до очистки)", ylab = "Значение",
        col = "lightyellow", border = "darkorange")

# boxplot.stats()$out возвращает список выбросов
# Выброс = значение за пределами 1.5 * IQR от краёв ящика
outliers1 <- boxplot.stats(set1)$out
outliers2 <- boxplot.stats(set2)$out

cat("=== Выбросы в наборе 1 ===\n")
## === Выбросы в наборе 1 ===
print(outliers1)
## [1] 200 250 -50
cat("\n=== Выбросы в наборе 2 ===\n")
## 
## === Выбросы в наборе 2 ===
print(outliers2)
## [1]  35.03455 150.00000 -20.00000 160.00000
# %in% проверяет вхождение в список; ! инвертирует — берём те, кого НЕТ в списке выбросов
clean1 <- set1[!set1 %in% outliers1]
clean2 <- set2[!set2 %in% outliers2]

cat("\nНабор 1: было", length(set1), "-> стало", length(clean1), "элементов\n")
## 
## Набор 1: было 53 -> стало 50 элементов
cat("Набор 2: было", length(set2), "-> стало", length(clean2), "элементов\n")
## Набор 2: было 53 -> стало 49 элементов
# Боксплоты ПОСЛЕ очистки
boxplot(clean1, main = "Набор 1 (после очистки)", ylab = "Значение",
        col = "lightgreen", border = "darkgreen")

boxplot(clean2, main = "Набор 2 (после очистки)", ylab = "Значение",
        col = "lightpink", border = "red")

par(mfrow = c(1, 1))

6 Задание 6 — Удаление дублирующихся строк

Задание: Сгенерируйте таблицу с дублирующимися строками. Удалите их с помощью unique() и duplicated(). Сравните результаты.

df_dupes <- data.frame(
  product  = c("Apple", "Banana", "Apple", "Pear", "Banana", "Orange", "Apple"),
  price    = c(30, 50, 30, 40, 50, 60, 30),
  quantity = c(5, 3, 5, 7, 3, 2, 5)
)

cat("=== Исходная таблица (с дублями) ===\n")
## === Исходная таблица (с дублями) ===
print(df_dupes)
##   product price quantity
## 1   Apple    30        5
## 2  Banana    50        3
## 3   Apple    30        5
## 4    Pear    40        7
## 5  Banana    50        3
## 6  Orange    60        2
## 7   Apple    30        5
# --- Метод 1: unique() ---
# Оставляет только уникальные строки. Просто и коротко.
df_unique <- unique(df_dupes)

cat("\n=== После unique() ===\n")
## 
## === После unique() ===
print(df_unique)
##   product price quantity
## 1   Apple    30        5
## 2  Banana    50        3
## 4    Pear    40        7
## 6  Orange    60        2
# --- Метод 2: duplicated() ---
# Возвращает TRUE для строк, которые уже встречались раньше.
# Первое вхождение — FALSE, все повторы — TRUE.
dup_mask <- duplicated(df_dupes)

cat("\n=== Маска дублей (TRUE = строка-дубль) ===\n")
## 
## === Маска дублей (TRUE = строка-дубль) ===
print(dup_mask)
## [1] FALSE FALSE  TRUE FALSE  TRUE FALSE  TRUE
df_no_dup <- df_dupes[!dup_mask, ]

cat("\n=== После duplicated() ===\n")
## 
## === После duplicated() ===
print(df_no_dup)
##   product price quantity
## 1   Apple    30        5
## 2  Banana    50        3
## 4    Pear    40        7
## 6  Orange    60        2
cat("\n=== Сравнение методов ===\n")
## 
## === Сравнение методов ===
cat("Исходно строк:      ", nrow(df_dupes), "\n")
## Исходно строк:       7
cat("После unique():     ", nrow(df_unique), "\n")
## После unique():      4
cat("После duplicated(): ", nrow(df_no_dup), "\n")
## После duplicated():  4
# Вывод: результат одинаковый.
# unique()      — короче, проще
# duplicated()  — гибче: сначала смотришь где дубли, потом решаешь что делать

7 Задание 7а — Обработка пропусков через пакет mice

Задание: Обработайте пропуски в данных с использованием пакета mice.

# install.packages("mice")
library(mice)

# nhanes — встроенный датасет из пакета mice с пропусками, удобен для примера
cat("=== Датасет nhanes ===\n")
## === Датасет nhanes ===
head(nhanes)
##   age  bmi hyp chl
## 1   1   NA  NA  NA
## 2   2 22.7   1 187
## 3   1   NA   1 187
## 4   3   NA  NA  NA
## 5   1 20.4   1 113
## 6   3   NA  NA 184
cat("\n=== NA по столбцам ===\n")
## 
## === NA по столбцам ===
colSums(is.na(nhanes))
## age bmi hyp chl 
##   0   9   8  10
# md.pattern() показывает паттерны пропусков: какие комбинации NA встречаются
cat("\n=== Паттерны пропусков ===\n")
## 
## === Паттерны пропусков ===
md.pattern(nhanes)

##    age hyp bmi chl   
## 13   1   1   1   1  0
## 3    1   1   1   0  1
## 1    1   1   0   1  1
## 1    1   0   0   1  2
## 7    1   0   0   0  3
##      0   8   9  10 27
# --- Множественное вменение (Multiple Imputation) ---
# mice() создаёт несколько (m=5) версий датасета, в каждой пропуски заполнены
# по-разному на основе статистических моделей. Умнее, чем просто среднее!
#
# method = "pmm" — predictive mean matching, хорошо для числовых данных
# seed = 123     — фиксируем случайность
# printFlag = FALSE — не захламляем консоль логом

imp <- mice(nhanes, m = 5, method = "pmm", seed = 123, printFlag = FALSE)

summary(imp)
## Class: mids
## Number of multiple imputations:  5 
## Imputation methods:
##   age   bmi   hyp   chl 
##    "" "pmm" "pmm" "pmm" 
## PredictorMatrix:
##     age bmi hyp chl
## age   0   1   1   1
## bmi   1   0   1   1
## hyp   1   1   0   1
## chl   1   1   1   0
# complete() извлекает один из 5 заполненных датасетов
filled_data <- complete(imp, action = 1)

cat("\n=== Заполненные данные ===\n")
## 
## === Заполненные данные ===
print(filled_data)
##    age  bmi hyp chl
## 1    1 22.0   1 238
## 2    2 22.7   1 187
## 3    1 30.1   1 187
## 4    3 26.3   2 218
## 5    1 20.4   1 113
## 6    3 27.5   1 184
## 7    1 22.5   1 118
## 8    1 30.1   1 187
## 9    2 22.0   1 238
## 10   2 27.4   1 186
## 11   1 33.2   1 187
## 12   2 27.4   2 229
## 13   3 21.7   1 206
## 14   2 28.7   2 204
## 15   1 29.6   1 187
## 16   1 35.3   1 187
## 17   3 27.2   2 284
## 18   2 26.3   2 199
## 19   1 35.3   1 218
## 20   3 25.5   2 218
## 21   1 33.2   1 187
## 22   1 33.2   1 229
## 23   1 27.5   1 131
## 24   3 24.9   1 284
## 25   2 27.4   1 186
cat("\n=== Остались ли NA? ===\n")
## 
## === Остались ли NA? ===
colSums(is.na(filled_data))
## age bmi hyp chl 
##   0   0   0   0
# Синие точки = реальные данные, красные = вмененные алгоритмом
stripplot(imp, pch = 20, cex = 1.5,
          main = "Реальные (синие) vs вмененные (красные) значения")


8 Задание 7б — Мультиколлинеарность

Задание: Разберите пример с мультиколлинеарностью.

# Мультиколлинеарность — когда два или больше предиктора сильно коррелируют между собой.
# Проблема: модель не понимает, кто из них "виновен" в результате.

# install.packages("car")
library(car)

# mtcars — встроенный датасет с характеристиками автомобилей
# Предсказываем расход топлива (mpg) через несколько переменных
model_full <- lm(mpg ~ wt + hp + disp + cyl, data = mtcars)
summary(model_full)
## 
## Call:
## lm(formula = mpg ~ wt + hp + disp + cyl, data = mtcars)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -4.0562 -1.4636 -0.4281  1.2854  5.8269 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept) 40.82854    2.75747  14.807 1.76e-14 ***
## wt          -3.85390    1.01547  -3.795 0.000759 ***
## hp          -0.02054    0.01215  -1.691 0.102379    
## disp         0.01160    0.01173   0.989 0.331386    
## cyl         -1.29332    0.65588  -1.972 0.058947 .  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 2.513 on 27 degrees of freedom
## Multiple R-squared:  0.8486, Adjusted R-squared:  0.8262 
## F-statistic: 37.84 on 4 and 27 DF,  p-value: 1.061e-10
# VIF = Variance Inflation Factor (коэффициент инфляции дисперсии)
# Показывает, насколько дисперсия коэффициента раздута из-за корреляции с другими
#
# VIF < 5   — всё ок
# VIF 5-10  — есть проблема
# VIF > 10  — серьёзная мультиколлинеарность

cat("=== VIF исходной модели ===\n")
## === VIF исходной модели ===
vif_full <- vif(model_full)
print(round(vif_full, 2))
##    wt    hp  disp   cyl 
##  4.85  3.41 10.37  6.74
cat("\nПредикторы с VIF > 5:\n")
## 
## Предикторы с VIF > 5:
print(vif_full[vif_full > 5])
##      disp       cyl 
## 10.373286  6.737707
# Матрица корреляций — смотрим кто с кем коррелирует
cat("\n=== Матрица корреляций предикторов ===\n")
## 
## === Матрица корреляций предикторов ===
print(round(cor(mtcars[, c("wt", "hp", "disp", "cyl")]), 2))
##        wt   hp disp  cyl
## wt   1.00 0.66 0.89 0.78
## hp   0.66 1.00 0.79 0.83
## disp 0.89 0.79 1.00 0.90
## cyl  0.78 0.83 0.90 1.00
# Решение: убираем сильно коррелирующие переменные (disp и cyl коррелируют с wt)
model_clean <- lm(mpg ~ wt + hp, data = mtcars)
summary(model_clean)
## 
## Call:
## lm(formula = mpg ~ wt + hp, data = mtcars)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -3.941 -1.600 -0.182  1.050  5.854 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept) 37.22727    1.59879  23.285  < 2e-16 ***
## wt          -3.87783    0.63273  -6.129 1.12e-06 ***
## hp          -0.03177    0.00903  -3.519  0.00145 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 2.593 on 29 degrees of freedom
## Multiple R-squared:  0.8268, Adjusted R-squared:  0.8148 
## F-statistic: 69.21 on 2 and 29 DF,  p-value: 9.109e-12
cat("\n=== VIF после удаления коллинеарных переменных ===\n")
## 
## === VIF после удаления коллинеарных переменных ===
print(round(vif(model_clean), 2))
##   wt   hp 
## 1.77 1.77
cat("Теперь все VIF < 5 — мультиколлинеарность устранена!\n")
## Теперь все VIF < 5 — мультиколлинеарность устранена!
# Матрица графиков — визуально видна корреляция между disp, cyl, wt
pairs(mtcars[, c("mpg", "wt", "hp", "disp", "cyl")],
      main = "Матрица корреляций (видна мультиколлинеарность)",
      col = "steelblue", pch = 19)