Цель работы — освоить базовые приёмы очистки данных: обработку пропусков разными способами, работу с выбросами, удаление дубликатов и выявление мультиколлинеарности. Эти шаги обычно выполняются до построения моделей.
Создадим числовой вектор с пропущенными значениями.
x <- c(10, 15, NA, 20, NA, 25, 30, NA, 40)
x
## [1] 10 15 NA 20 NA 25 30 NA 40
Найдём пропуски и удалим их, получив «чистый» набор данных.
is.na(x)
## [1] FALSE FALSE TRUE FALSE TRUE FALSE FALSE TRUE FALSE
x_clean <- x[!is.na(x)]
x_clean
## [1] 10 15 20 25 30 40
Короткий вывод: is.na() помогает
определить NA, после чего их можно исключить.
Сгенерируем таблицу, содержащую пропуски в числовых и текстовых полях.
df <- data.frame(
id = c(1, 2, 3, 4, 5, 6),
age = c(19, NA, 21, 20, NA, 22),
name = c("Anna", "Boris", NA, "Daria", "Elena", NA),
city = c("Moscow", "Kazan", "Moscow", NA, "Omsk", "Perm")
)
df
## id age name city
## 1 1 19 Anna Moscow
## 2 2 NA Boris Kazan
## 3 3 21 <NA> Moscow
## 4 4 20 Daria <NA>
## 5 5 NA Elena Omsk
## 6 6 22 <NA> Perm
Очистим данные, оставив только строки без пропусков.
complete.cases(df)
## [1] TRUE FALSE FALSE FALSE FALSE FALSE
df_clean <- df[complete.cases(df), ]
df_clean
## id age name city
## 1 1 19 Anna Moscow
Короткий вывод: complete.cases()
оставляет строки, где нет NA ни в одном столбце.
Встроенный датасет airquality содержит пропуски.
Посмотрим, где они есть.
library(caret)
data(airquality)
colSums(is.na(airquality))
## Ozone Solar.R Wind Temp Month Day
## 37 7 0 0 0 0
summary(airquality)
## Ozone Solar.R Wind Temp
## Min. : 1.00 Min. : 7.0 Min. : 1.700 Min. :56.00
## 1st Qu.: 18.00 1st Qu.:115.8 1st Qu.: 7.400 1st Qu.:72.00
## Median : 31.50 Median :205.0 Median : 9.700 Median :79.00
## Mean : 42.13 Mean :185.9 Mean : 9.958 Mean :77.88
## 3rd Qu.: 63.25 3rd Qu.:258.8 3rd Qu.:11.500 3rd Qu.:85.00
## Max. :168.00 Max. :334.0 Max. :20.700 Max. :97.00
## NA's :37 NA's :7
## Month Day
## Min. :5.000 Min. : 1.0
## 1st Qu.:6.000 1st Qu.: 8.0
## Median :7.000 Median :16.0
## Mean :6.993 Mean :15.8
## 3rd Qu.:8.000 3rd Qu.:23.0
## Max. :9.000 Max. :31.0
##
air_mean <- airquality
num_cols <- sapply(air_mean, is.numeric)
for (col in names(air_mean)[num_cols]) {
air_mean[[col]][is.na(air_mean[[col]])] <- mean(air_mean[[col]], na.rm = TRUE)
}
colSums(is.na(air_mean))
## Ozone Solar.R Wind Temp Month Day
## 0 0 0 0 0 0
air_median <- airquality
for (col in names(air_median)[num_cols]) {
air_median[[col]][is.na(air_median[[col]])] <- median(air_median[[col]], na.rm = TRUE)
}
colSums(is.na(air_median))
## Ozone Solar.R Wind Temp Month Day
## 0 0 0 0 0 0
Короткий вывод: медиана обычно более устойчива к выбросам, чем среднее.
Сгенерируем два числовых набора данных и добавим в них выбросы.
set.seed(202)
a <- c(rnorm(40, mean = 10, sd = 2), 30, 35) # выбросы
b <- c(rnorm(40, mean = 100, sd = 10), 180, 220) # выбросы
Построим boxplot.
boxplot(a, main = "Набор A (с выбросами)")
boxplot(b, main = "Набор B (с выбросами)")
Пояснение графика: точки за пределами «усов» boxplot считаются выбросами.
Найдём выбросы и удалим их.
out_a <- boxplot.stats(a)$out
out_b <- boxplot.stats(b)$out
out_a
## [1] 30 35
out_b
## [1] 76.4159 180.0000 220.0000
a_clean <- a[!(a %in% out_a)]
b_clean <- b[!(b %in% out_b)]
Построим boxplot после удаления.
boxplot(a_clean, main = "Набор A (без выбросов)")
boxplot(b_clean, main = "Набор B (без выбросов)")
Короткий вывод: после удаления выбросов разброс уменьшается, данные становятся более «ровными».
Создадим таблицу с повторяющимися строками.
dup_tbl <- data.frame(
product = c("A", "B", "B", "C", "D", "D", "E"),
price = c(10, 20, 20, 30, 40, 40, 50)
)
dup_tbl
## product price
## 1 A 10
## 2 B 20
## 3 B 20
## 4 C 30
## 5 D 40
## 6 D 40
## 7 E 50
Удаление через unique():
unique_tbl <- unique(dup_tbl)
unique_tbl
## product price
## 1 A 10
## 2 B 20
## 4 C 30
## 5 D 40
## 7 E 50
Удаление через duplicated():
dup_removed_tbl <- dup_tbl[!duplicated(dup_tbl), ]
dup_removed_tbl
## product price
## 1 A 10
## 2 B 20
## 4 C 30
## 5 D 40
## 7 E 50
Сравним результаты:
identical(unique_tbl, dup_removed_tbl)
## [1] TRUE
Короткий вывод: оба метода дают одинаковый итог — уникальные строки.
Используем учебный датасет nhanes из пакета
mice, где есть пропуски.
Он стабильно подходит для демонстрации множественной импутации.
library(mice)
data(nhanes, package = "mice")
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
## 7 1 22.5 1 118
## 8 1 30.1 1 187
## 9 2 22.0 1 238
## 10 2 NA NA NA
## 11 1 NA NA NA
## 12 2 NA NA NA
## 13 3 21.7 1 206
## 14 2 28.7 2 204
## 15 1 29.6 1 NA
## 16 1 NA NA NA
## 17 3 27.2 2 284
## 18 2 26.3 2 199
## 19 1 35.3 1 218
## 20 3 25.5 2 NA
## 21 1 NA NA NA
## 22 1 33.2 1 229
## 23 1 27.5 1 131
## 24 3 24.9 1 NA
## 25 2 27.4 1 186
colSums(is.na(nhanes))
## age bmi hyp chl
## 0 9 8 10
Заполним пропуски методом pmm (predictive mean
matching).
imp <- mice(nhanes, m = 5, method = "pmm", seed = 55, printFlag = FALSE)
nhanes_filled <- complete(imp, 1)
nhanes_filled
## age bmi hyp chl
## 1 1 29.6 1 187
## 2 2 22.7 1 187
## 3 1 27.5 1 187
## 4 3 21.7 1 204
## 5 1 20.4 1 113
## 6 3 20.4 2 184
## 7 1 22.5 1 118
## 8 1 30.1 1 187
## 9 2 22.0 1 238
## 10 2 22.5 1 187
## 11 1 27.2 1 187
## 12 2 27.5 2 238
## 13 3 21.7 1 206
## 14 2 28.7 2 204
## 15 1 29.6 1 187
## 16 1 20.4 1 131
## 17 3 27.2 2 284
## 18 2 26.3 2 199
## 19 1 35.3 1 218
## 20 3 25.5 2 229
## 21 1 29.6 1 238
## 22 1 33.2 1 229
## 23 1 27.5 1 131
## 24 3 24.9 1 204
## 25 2 27.4 1 186
colSums(is.na(nhanes_filled))
## age bmi hyp chl
## 0 0 0 0
Короткий вывод: mice восстанавливает
пропуски, используя связи между переменными, формируя несколько
вариантов заполнения.
Сгенерируем признаки, где два столбца сильно коррелируют, и проверим VIF.
set.seed(500)
n <- 120
x1 <- rnorm(n)
x2 <- 0.95*x1 + rnorm(n, sd = 0.05) # почти линейная зависимость от x1
x3 <- rnorm(n)
y <- 2*x1 + rnorm(n)
mc <- data.frame(y, x1, x2, x3)
cor(mc[, c("x1","x2","x3")])
## x1 x2 x3
## x1 1.00000000 0.99857385 0.07550668
## x2 0.99857385 1.00000000 0.08271294
## x3 0.07550668 0.08271294 1.00000000
Построим модель:
model <- lm(y ~ x1 + x2 + x3, data = mc)
summary(model)
##
## Call:
## lm(formula = y ~ x1 + x2 + x3, data = mc)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.58923 -0.63963 -0.05886 0.76860 2.67739
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -0.04788 0.09598 -0.499 0.619
## x1 1.96808 1.76412 1.116 0.267
## x2 0.07617 1.85299 0.041 0.967
## x3 -0.03520 0.10217 -0.345 0.731
##
## Residual standard error: 1.041 on 116 degrees of freedom
## Multiple R-squared: 0.8043, Adjusted R-squared: 0.7993
## F-statistic: 159 on 3 and 116 DF, p-value: < 2.2e-16
Проверим VIF:
if (!requireNamespace("car", quietly = TRUE)) install.packages("car")
library(car)
vif(model)
## x1 x2 x3
## 357.184113 357.594170 1.025083
Пояснение:
Если VIF высокий (обычно > 5 или > 10), это признак
мультиколлинеарности.
В данном примере x1 и x2 почти одинаковые по смыслу, поэтому VIF у них
будет большим.
В работе были выполнены все пункты задания:
создание данных с пропусками, очистка is.na() и
complete.cases(), заполнение пропусков (среднее/медиана),
обработка выбросов по boxplot, удаление дубликатов, импутация
mice и пример мультиколлинеарности с VIF.