Освоить основные приёмы предварительной обработки данных в среде R: формирование наборов данных с пропусками, удаление и заполнение пропущенных значений, обнаружение и удаление выбросов и дубликатов, а также диагностику мультиколлинеарности.
Используются пакеты caret (функция
preProcess), mice (множественное вменение
пропусков) и car (функция vif), а также
встроенные наборы данных airquality, nhanes и
mtcars.
Если пакеты не установлены, выполните один раз:
Задание: Сформируйте свой собственный датасет с
помощью функции 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
## Количество элементов: 10
## Min. 1st Qu. Median Mean 3rd Qu. Max. NAs
## 8.00 11.00 25.00 37.71 55.00 99.00 3
Вывод: Создан числовой вектор из 10 элементов,
содержащий 3 пропущенных значения (NA). Функция
summary() считает статистики только по имеющимся
наблюдениям и отдельно показывает количество пропусков
(NAs = 3): среднее значение 37,71 вычислено по 7
непропущенным элементам.
Задание: Проведите очистку данных с использованием
функции is.na() [1] и выведите «чистый» датасет.
## [1] FALSE FALSE TRUE FALSE TRUE FALSE FALSE TRUE FALSE FALSE
# ! означает "НЕ". Так берём только те элементы, где НЕ NA
clean_vector <- my_vector[!is.na(my_vector)]
cat("=== Исходный вектор (с пропусками) ===\n")## === Исходный вектор (с пропусками) ===
## [1] 10 25 NA 47 NA 63 8 NA 99 12
##
## === Чистый вектор (без NA) ===
## [1] 10 25 47 63 8 99 12
##
## Было элементов: 10
##
## Стало элементов: 7
##
## Удалено NA: 3
Вывод: Функция is.na() возвращает
логическую маску пропусков, а оператор ! инвертирует её,
что позволяет оставить только заполненные элементы. Из 10 элементов
удалено 3 пропуска, в чистом векторе осталось 7 значений.
Задание: Сгенерируйте таблицу данных с числовыми и
текстовыми столбцами (аналогично с помощью функции c).
Очистите данные с помощью функции complete.cases() [1].
# 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")## === Исходная таблица ===
## 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) ===
## [1] TRUE FALSE FALSE TRUE FALSE FALSE TRUE
##
## === Чистая таблица ===
## name age salary
## 1 Alex 25 50000
## 4 Ivan 45 80000
## 7 Petr 33 70000
##
## Исходно строк: 7
##
## После очистки: 3
Вывод: Функция complete.cases()
отмечает строки, в которых нет ни одного NA сразу во всех
столбцах — как числовых, так и текстовых. Из 7 строк осталось 3 полных;
остальные 4 содержали хотя бы один пропуск. Недостаток метода — потеря
данных: в данном примере удалено более половины строк.
Задание: Проанализируйте датасет
airquality с пропусками. С использованием функции
preProcess из пакета caret заполните пропуски
предсказанными значениями (среднее, медиана) [2].
# install.packages("caret") # раскомментируй если пакет не установлен
library(caret)
# airquality — встроенный датасет R, данные о качестве воздуха в Нью-Йорке
# Содержит пропуски в столбцах Ozone и Solar.R
cat("=== Первые 10 строк airquality ===\n")## === Первые 10 строк airquality ===
## 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
##
## === Количество NA по каждому столбцу ===
## 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 остались? ===
## 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 остались? ===
## 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
Вывод: Датасет airquality (встроенный в
R) содержит 37 пропусков в столбце Ozone и 7 в столбце
Solar.R. Функция preProcess() с методом
medianImpute заполнила пропуски медианами столбцов (для
Ozone — 31,5). В caret нет отдельного метода
для заполнения средним, поэтому среднее рассчитано вручную (для
Ozone — 42,1). Медиана менее чувствительна к выбросам,
поэтому даёт более консервативную оценку.
Задание: Сгенерируйте два числовых набора данных и
добавьте в них выбросы. С использованием функции boxplot
обнаружьте выбросы и удалите их [3, 4].
# 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 ===
## [1] 200 250 -50
##
## === Выбросы в наборе 2 ===
## [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 элементов
## Набор 2: было 53 -> стало 49 элементов
# Боксплоты ПОСЛЕ очистки
boxplot(clean1, main = "Набор 1 (после очистки)", ylab = "Значение",
col = "lightgreen", border = "darkgreen")
boxplot(clean2, main = "Набор 2 (после очистки)", ylab = "Значение",
col = "lightpink", border = "red")Вывод: Функция boxplot.stats()$out
возвращает значения, лежащие за пределами 1,5·IQR от границ «ящика». В
наборе 1 найдено 3 выброса (200, 250, −50), в наборе 2 — 4 (35,03; 150;
−20; 160), причём значение 35,03 возникло естественным образом при
генерации нормального распределения. После удаления выбросов масштаб
графиков стал существенно компактнее. На боксплоте набора 1 после
очистки снова виден один выброс (≈ 73): при удалении крайних значений
границы «усов» меняются, поэтому при необходимости процедуру можно
повторить.
Задание: Сгенерируйте таблицу данных, в которой
дублируются строки. Удалите строки с использованием функций
unique(), duplicated(). Сравните результаты
[5].
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")## === Исходная таблица (с дублями) ===
## 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() ===
## 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 = строка-дубль) ===
## [1] FALSE FALSE TRUE FALSE TRUE FALSE TRUE
##
## === После duplicated() ===
## product price quantity
## 1 Apple 30 5
## 2 Banana 50 3
## 4 Pear 40 7
## 6 Orange 60 2
##
## === Сравнение методов ===
## Исходно строк: 7
## После unique(): 4
## После duplicated(): 4
# Вывод: результат одинаковый.
# unique() — короче, проще
# duplicated() — гибче: сначала смотришь где дубли, потом решаешь что делатьВывод: Функции unique() и
duplicated() дали одинаковый результат: из 7 строк осталось
4 уникальных (удалены строки 3, 5 и 7 — повторы строк 1, 2 и 1).
unique() короче в записи, а duplicated()
гибче: маску дублей можно использовать отдельно, например, чтобы
посчитать повторы или проверять дубли только по части столбцов.
Задание: Обработайте пропуски в данных с
использованием пакета mice [6].
# install.packages("mice")
library(mice)
# nhanes — встроенный датасет из пакета mice с пропусками, удобен для примера
cat("=== Датасет nhanes ===\n")## === Датасет 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
##
## === NA по столбцам ===
## age bmi hyp chl
## 0 9 8 10
# md.pattern() показывает паттерны пропусков: какие комбинации NA встречаются
cat("\n=== Паттерны пропусков ===\n")##
## === Паттерны пропусков ===
## 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")##
## === Заполненные данные ===
## 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
##
## === Остались ли NA? ===
## age bmi hyp chl
## 0 0 0 0
# Синие точки = реальные данные, красные = вмененные алгоритмом
stripplot(imp, pch = 20, cex = 1.5,
main = "Реальные (синие) vs вмененные (красные) значения")Вывод: В датасете nhanes пропуски есть
в столбцах bmi (9), hyp (8) и chl
(10), всего 27 пропусков; полных строк только 13 из 25, поэтому простое
удаление неполных строк привело бы к потере почти половины данных. Пакет
mice построил 5 вариантов заполнения методом
pmm; из первого набора извлечён полный датасет без
NA. На графике stripplot красные точки
(вменённые значения) находятся в диапазоне реально наблюдаемых значений
(синие точки), то есть заполнение выглядит правдоподобно.
Задание: Разберите пример с мультиколлинеарностью [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 исходной модели ===
## wt hp disp cyl
## 4.85 3.41 10.37 6.74
##
## Предикторы с VIF > 5:
## disp cyl
## 10.373286 6.737707
# Матрица корреляций — смотрим кто с кем коррелирует
cat("\n=== Матрица корреляций предикторов ===\n")##
## === Матрица корреляций предикторов ===
## 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
##
## === VIF после удаления коллинеарных переменных ===
## wt hp
## 1.77 1.77
## Теперь все VIF < 5 — мультиколлинеарность устранена!
# Матрица графиков — визуально видна корреляция между disp, cyl, wt
pairs(mtcars[, c("mpg", "wt", "hp", "disp", "cyl")],
main = "Матрица корреляций (видна мультиколлинеарность)",
col = "steelblue", pch = 19)Вывод: В полной модели
mpg ~ wt + hp + disp + cyl значения VIF для
disp (10,37) и cyl (6,74) превышают 5, что
согласуется с матрицей корреляций (r между disp и
cyl — 0,90, между disp и wt —
0,89) и с диаграммами pairs. При этом значимым при α = 0,05
остаётся только wt. После исключения disp и
cyl модель mpg ~ wt + hp имеет VIF = 1,77 для
обоих предикторов, оба коэффициента значимы (для hp p =
0,00145), а качество модели почти не изменилось: скорректированный R²
равен 0,8148 против 0,8262.
В ходе работы выполнены все задания: создан набор данных с пропусками
при помощи функции c(); выполнена очистка данных с помощью
is.na() и complete.cases(); пропуски в
airquality заполнены медианой (preProcess из
пакета caret) и средним; выбросы обнаружены и удалены с
помощью boxplot; дубликаты строк удалены функциями
unique() и duplicated(); пропуски в
nhanes обработаны методом множественного вменения (пакет
mice); на примере mtcars продемонстрированы
диагностика мультиколлинеарности по VIF и её устранение исключением
коррелирующих предикторов.
Удаление неполных строк или выбросов проще всего, но приводит к
потере наблюдений; заполнение пропусков средним или медианой сохраняет
объём данных, однако занижает изменчивость, а множественное вменение
(mice) учитывает связи между переменными и является более
аккуратным вариантом.