Введение

В работе показаны базовые шаги предобработки данных: пропуски (удаление и заполнение), выбросы, дубликаты, импутация с mice и пример мультиколлинеарности.


1. Собственный набор данных с NA (c)

Создадим числовой вектор с пропусками.

v <- c(2, NA, 6, 8, NA, 13, 21, 34, NA, 55)
v
##  [1]  2 NA  6  8 NA 13 21 34 NA 55

2. Очистка с is.na()

Найдём пропуски и получим «чистый» вектор без NA.

mask_na <- is.na(v)
mask_na
##  [1] FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE FALSE  TRUE FALSE
v_ok <- v[!mask_na]
v_ok
## [1]  2  6  8 13 21 34 55

Вывод: is.na() отмечает пропуски TRUE/ FALSE, а !mask_na оставляет только реальные значения.


3. Таблица с числами и текстом + complete.cases()

Сгенерируем таблицу с пропусками.

tbl <- data.frame(
  code  = c("X1", "X2", "X3", "X4", "X5"),
  qty   = c(5, NA, 12, 10, NA),
  note  = c("ok", "ok", NA, "check", "ok"),
  store = c("A", NA, "A", "B", "B")
)
tbl
##   code qty  note store
## 1   X1   5    ok     A
## 2   X2  NA    ok  <NA>
## 3   X3  12  <NA>     A
## 4   X4  10 check     B
## 5   X5  NA    ok     B

Оставим строки, где нет пропусков.

tbl_good <- tbl[complete.cases(tbl), ]
tbl_good
##   code qty  note store
## 1   X1   5    ok     A
## 4   X4  10 check     B

Вывод: complete.cases() удаляет строки, где есть хотя бы один NA.


4. airquality: пропуски и заполнение (среднее/медиана)

Датасет airquality содержит NA. Посмотрим их количество.

data(airquality)
colSums(is.na(airquality))
##   Ozone Solar.R    Wind    Temp   Month     Day 
##      37       7       0       0       0       0

4.1 Заполнение средним

aq_mean <- airquality
num_cols <- names(aq_mean)[sapply(aq_mean, is.numeric)]

for (nm in num_cols) {
  aq_mean[[nm]][is.na(aq_mean[[nm]])] <- mean(aq_mean[[nm]], na.rm = TRUE)
}

colSums(is.na(aq_mean))
##   Ozone Solar.R    Wind    Temp   Month     Day 
##       0       0       0       0       0       0

4.2 Заполнение медианой

aq_med <- airquality

for (nm in num_cols) {
  aq_med[[nm]][is.na(aq_med[[nm]])] <- median(aq_med[[nm]], na.rm = TRUE)
}

colSums(is.na(aq_med))
##   Ozone Solar.R    Wind    Temp   Month     Day 
##       0       0       0       0       0       0

Вывод: медиана обычно лучше, если в данных возможны выбросы.


5. Выбросы: обнаружить boxplot и удалить

Сделаем два набора и добавим явные выбросы.

set.seed(7)
p <- c(rnorm(30, 50, 4), 85, 90)     # выбросы сверху
q <- c(rnorm(30, 200, 15), 120, 320) # выбросы снизу и сверху

Построим boxplot.

boxplot(p, main="Boxplot набора p (с выбросами)")

boxplot(q, main="Boxplot набора q (с выбросами)")

Вывод: точки за «усами» — это выбросы (слишком большие/маленькие значения).

Найдём и удалим выбросы:

p_out <- boxplot.stats(p)$out
q_out <- boxplot.stats(q)$out

p2 <- p[!(p %in% p_out)]
q2 <- q[!(q %in% q_out)]

Boxplot после удаления:

boxplot(p2, main="Набор p после удаления выбросов")

boxplot(q2, main="Набор q после удаления выбросов")

Вывод: после удаления выбросов коробка становится более «ровной», разброс уменьшается.


6. Дубликаты: unique() и duplicated()

Создадим таблицу с повторяющимися строками.

dup <- data.frame(
  id = c(1, 1, 2, 3, 3, 3, 4),
  tag = c("A", "A", "B", "C", "C", "C", "D")
)
dup
##   id tag
## 1  1   A
## 2  1   A
## 3  2   B
## 4  3   C
## 5  3   C
## 6  3   C
## 7  4   D

Через unique():

u1 <- unique(dup)
u1
##   id tag
## 1  1   A
## 3  2   B
## 4  3   C
## 7  4   D

Через duplicated():

u2 <- dup[!duplicated(dup), ]
u2
##   id tag
## 1  1   A
## 3  2   B
## 4  3   C
## 7  4   D

Проверка:

identical(u1, u2)
## [1] TRUE

Вывод: оба способа оставляют только первые уникальные строки.


7. Пропуски через пакет mice (стабильный пример)

Возьмём учебный датасет nhanes2 из пакета mice и заполним пропуски.

library(mice)

data(nhanes2, package = "mice")
nhanes2
##      age  bmi  hyp chl
## 1  20-39   NA <NA>  NA
## 2  40-59 22.7   no 187
## 3  20-39   NA   no 187
## 4  60-99   NA <NA>  NA
## 5  20-39 20.4   no 113
## 6  60-99   NA <NA> 184
## 7  20-39 22.5   no 118
## 8  20-39 30.1   no 187
## 9  40-59 22.0   no 238
## 10 40-59   NA <NA>  NA
## 11 20-39   NA <NA>  NA
## 12 40-59   NA <NA>  NA
## 13 60-99 21.7   no 206
## 14 40-59 28.7  yes 204
## 15 20-39 29.6   no  NA
## 16 20-39   NA <NA>  NA
## 17 60-99 27.2  yes 284
## 18 40-59 26.3  yes 199
## 19 20-39 35.3   no 218
## 20 60-99 25.5  yes  NA
## 21 20-39   NA <NA>  NA
## 22 20-39 33.2   no 229
## 23 20-39 27.5   no 131
## 24 60-99 24.9   no  NA
## 25 40-59 27.4   no 186
colSums(is.na(nhanes2))
## age bmi hyp chl 
##   0   9   8  10

Импутация:

imp <- mice(nhanes2, m = 3, method = "pmm", seed = 7, printFlag = FALSE)
filled <- complete(imp, 1)
filled
##      age  bmi hyp chl
## 1  20-39 30.1  no 187
## 2  40-59 22.7  no 187
## 3  20-39 22.0  no 187
## 4  60-99 28.7  no 186
## 5  20-39 20.4  no 113
## 6  60-99 28.7  no 184
## 7  20-39 22.5  no 118
## 8  20-39 30.1  no 187
## 9  40-59 22.0  no 238
## 10 40-59 22.7  no 187
## 11 20-39 35.3  no 206
## 12 40-59 27.5 yes 218
## 13 60-99 21.7  no 206
## 14 40-59 28.7 yes 204
## 15 20-39 29.6  no 187
## 16 20-39 30.1  no 229
## 17 60-99 27.2 yes 284
## 18 40-59 26.3 yes 199
## 19 20-39 35.3  no 218
## 20 60-99 25.5 yes 186
## 21 20-39 30.1  no 229
## 22 20-39 33.2  no 229
## 23 20-39 27.5  no 131
## 24 60-99 24.9  no 206
## 25 40-59 27.4  no 186
colSums(is.na(filled))
## age bmi hyp chl 
##   0   0   0   0

Вывод: mice заполняет NA, используя связи между столбцами (а не просто среднее/медиану).


8. Пример мультиколлинеарности (корреляция + VIF)

Сгенерируем зависимые признаки: один почти копия другого.

set.seed(9)
n <- 150
a1 <- rnorm(n)
a2 <- a1 * 0.98 + rnorm(n, sd = 0.03)  # почти то же самое
a3 <- rnorm(n)
y  <- 5*a1 + rnorm(n)

dat <- data.frame(y, a1, a2, a3)
cor(dat[, c("a1","a2","a3")])
##           a1        a2        a3
## a1 1.0000000 0.9995297 0.1651020
## a2 0.9995297 1.0000000 0.1683141
## a3 0.1651020 0.1683141 1.0000000

Модель:

fit <- lm(y ~ a1 + a2 + a3, data = dat)
summary(fit)
## 
## Call:
## lm(formula = y ~ a1 + a2 + a3, data = dat)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.66766 -0.60266  0.02922  0.62948  2.51922 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)
## (Intercept)  0.04493    0.08150   0.551    0.582
## a1           3.98990    2.73658   1.458    0.147
## a2           0.88927    2.78279   0.320    0.750
## a3          -0.04736    0.08552  -0.554    0.581
## 
## Residual standard error: 0.9899 on 146 degrees of freedom
## Multiple R-squared:  0.9587, Adjusted R-squared:  0.9578 
## F-statistic:  1129 on 3 and 146 DF,  p-value: < 2.2e-16

VIF:

if (!requireNamespace("car", quietly = TRUE)) install.packages("car")
library(car)
vif(fit)
##         a1         a2         a3 
## 1074.92115 1076.10591    1.04033

Вывод:
Если VIF большой (обычно > 5 или > 10), значит признаки сильно зависят друг от друга.
Здесь a1 и a2 почти одинаковые, поэтому у них будет высокий VIF.