Задание 1. Датасет с NA

my_data <- c(10, 20, NA, 40, 50, NA, 70, 80, 90, NA)
print(my_data)
##  [1] 10 20 NA 40 50 NA 70 80 90 NA

Вывод

Создан вектор из 10 числовых значений, три из которых — NA. Это типичная ситуация в реальных данных.

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

is.na(my_data)
##  [1] FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE FALSE  TRUE
clean_data <- my_data[!is.na(my_data)]
print(clean_data)
## [1] 10 20 40 50 70 80 90
length(my_data) - length(clean_data)
## [1] 3

Вывод

is.na() вернул логический вектор, где TRUE соответствует пропускам. С помощью !is.na() и индексации мы оставили только «полные» значения. Из 10 наблюдений удалены 3 пропуска, осталось 7.

Задание 3. Смешанный датасет и complete.cases()

df <- data.frame(
  num = c(1, 2, NA, 4, 5),
  txt = c("a", "b", "c", NA, "e"),
  stringsAsFactors = FALSE
)
print(df)
##   num  txt
## 1   1    a
## 2   2    b
## 3  NA    c
## 4   4 <NA>
## 5   5    e
# complete.cases — TRUE, если в строке нет ни одного NA
complete.cases(df)
## [1]  TRUE  TRUE FALSE FALSE  TRUE
df_clean <- df[complete.cases(df), ]
print(df_clean)
##   num txt
## 1   1   a
## 2   2   b
## 5   5   e

Вывод

complete.cases() вернул FALSE для 3-й и 4-й строк (в одной NA в num, в другой — в txt). Фильтрация оставила 3 полные строки. В отличие от is.na(), который проверяет один столбец, complete.cases() проверяет всю строку целиком — удобно для смешанных таблиц.

Задание 4. airquality и preProcess (среднее / медиана)

library(caret)
data(airquality)

# Сколько пропусков в каждом столбце
colSums(is.na(airquality))
##   Ozone Solar.R    Wind    Temp   Month     Day 
##      37       7       0       0       0       0
# Заполнение медианой через preProcess
preProc_median <- preProcess(airquality, method = "medianImpute")
airquality_median <- predict(preProc_median, airquality)
cat("После медианной импутации, NA:\n"); print(colSums(is.na(airquality_median)))
## После медианной импутации, NA:
##   Ozone Solar.R    Wind    Temp   Month     Day 
##       0       0       0       0       0       0
# Заполнение средним вручную (preProcess поддерживает medianImpute,
# но не meanImpute — среднее считаем сами)
airquality_mean <- airquality
for (col in names(airquality_mean)) {
  if (anyNA(airquality_mean[[col]])) {
    airquality_mean[[col]][is.na(airquality_mean[[col]])] <-
      mean(airquality_mean[[col]], na.rm = TRUE)
  }
}
cat("После средней импутации, NA:\n"); print(colSums(is.na(airquality_mean)))
## После средней импутации, NA:
##   Ozone Solar.R    Wind    Temp   Month     Day 
##       0       0       0       0       0       0
# Сравним распределения
summary(airquality$Ozone)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.     NAs 
##    1.00   18.00   31.50   42.13   63.25  168.00      37
summary(airquality_median$Ozone)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    1.00   21.00   31.50   39.56   46.00  168.00
summary(airquality_mean$Ozone)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    1.00   21.00   42.13   42.13   46.00  168.00

Вывод

В airquality пропуски только в Ozone (37) и Solar.R (7). preProcess(method = “medianImpute”) заполнил их медианой: пропусков не осталось. Среднее заполнение мы реализовали вручную — оно даёт чуть другое распределение (среднее чувствительнее к выбросам, чем медиана). Медиана предпочтительнее для скошенных распределений (как у Ozone).

Задание 5. Выбросы и boxplot

set.seed(123)

# Два набора с добавленными выбросами
data1 <- c(rnorm(100, mean = 50, sd = 10), 200, 250, -100)
data2 <- c(rnorm(100, mean = 100, sd = 20), 300, -50)

# Boxplot "до"
boxplot(data1, main = "data1: с выбросами", col = "lightblue")

boxplot(data2, main = "data2: с выбросами", col = "lightgreen")

# Функция поиска выбросов по правилу 1.5*IQR
find_outliers <- function(x) {
  q1 <- quantile(x, 0.25); q3 <- quantile(x, 0.75)
  iqr <- q3 - q1
  x[x < (q1 - 1.5*iqr) | x > (q3 + 1.5*iqr)]
}

out1 <- find_outliers(data1); out2 <- find_outliers(data2)
cat("Выбросы в data1:", out1, "\n")
## Выбросы в data1: 200 250 -100
cat("Выбросы в data2:", out2, "\n")
## Выбросы в data2: 164.8208 300 -50
# Удаление выбросов
clean1 <- data1[!(data1 %in% out1)]
clean2 <- data2[!(data2 %in% out2)]

# Boxplot "после"
boxplot(clean1, main = "data1: очищено", col = "lightblue")

boxplot(clean2, main = "data2: очищено", col = "lightgreen")

Вывод

Boxplot визуально помечает выбросы точками за пределами «усов». Правило 1.5×IQR: всё, что ниже Q1 − 1.5·IQR или выше Q3 + 1.5·IQR, считается выбросом. В data1 обнаружены 3 выброса (200, 250, −100), в data2 — 2 (300, −50). После удаления «усы» на графиках расширились, распределение стало компактнее.

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

df_dup <- data.frame(
  id  = c(1, 2, 2, 3, 4, 4, 4, 5),
  val = c("a", "b", "b", "c", "d", "d", "d", "e"),
  stringsAsFactors = FALSE
)
print(df_dup)
##   id val
## 1  1   a
## 2  2   b
## 3  2   b
## 4  3   c
## 5  4   d
## 6  4   d
## 7  4   d
## 8  5   e
cat("Строк всего:", nrow(df_dup), "\n")
## Строк всего: 8
# duplicated: TRUE для повторных вхождений (кроме первого)
print(duplicated(df_dup))
## [1] FALSE FALSE  TRUE FALSE FALSE  TRUE  TRUE FALSE
# Метод 1: unique()
u1 <- unique(df_dup)
print(u1)
##   id val
## 1  1   a
## 2  2   b
## 4  3   c
## 5  4   d
## 8  5   e
# Метод 2: duplicated() + индексация
u2 <- df_dup[!duplicated(df_dup), ]
print(u2)
##   id val
## 1  1   a
## 2  2   b
## 4  3   c
## 5  4   d
## 8  5   e
# Сравнение
identical(u1, u2)
## [1] TRUE

Вывод

duplicated() возвращает TRUE для строк, которые уже встречались выше (первое вхождение — FALSE). unique() и df[!duplicated(df), ] дают идентичный результат (проверено через identical()): удалены 3 дублирующиеся строки. Разница только в синтаксисе: unique() короче, а duplicated() можно комбинировать с дополнительной логикой фильтрации.

Задание 7a. Пропуски через пакет mice

library(mice)
data(airquality)

# Карта пропусков
md.pattern(airquality)

##     Wind Temp Month Day Solar.R Ozone   
## 111    1    1     1   1       1     1  0
## 35     1    1     1   1       1     0  1
## 5      1    1     1   1       0     1  1
## 2      1    1     1   1       0     0  2
##        0    0     0   0       7    37 44
# Множественная импутация (m=5 наборов, pmm = predictive mean matching)
set.seed(123)
imp <- mice(airquality, m = 5, method = "pmm", printFlag = FALSE)

# Берём первый заполненный набор
airquality_imp <- complete(imp, 1)
colSums(is.na(airquality_imp))
##   Ozone Solar.R    Wind    Temp   Month     Day 
##       0       0       0       0       0       0
# Сравним распределения до/после
summary(airquality$Ozone)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.     NAs 
##    1.00   18.00   31.50   42.13   63.25  168.00      37
summary(airquality_imp$Ozone)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    1.00   18.00   29.00   40.66   59.00  168.00

Вывод

mice выполняет множественную импутацию: создаёт m полных копий данных, в каждой пропуски заполняются по модели (метод pmm — predictive mean matching, учитывает связи между переменными). Это принципиально отличается от заполнения средним/медианой: сохраняется дисперсия и корреляционная структура. complete(imp, 1) извлекает первый заполненный набор — в нём пропусков нет. Множественная импутация считается «золотым стандартом» при работе с NA.

Задание 7b. Мультиколлинеарность

library(car)
set.seed(123)

n  <- 100
x1 <- rnorm(n)
x2 <- 0.95 * x1 + rnorm(n, sd = 0.1)   # сильно коррелирует с x1
x3 <- rnorm(n)
y  <- 2 * x1 + 3 * x3 + rnorm(n)

df_mc <- data.frame(y, x1, x2, x3)

# Матрица корреляций
round(cor(df_mc), 3)
##        y     x1     x2     x3
## y  1.000  0.430  0.432  0.783
## x1 0.430  1.000  0.994 -0.129
## x2 0.432  0.994  1.000 -0.126
## x3 0.783 -0.129 -0.126  1.000
# Модель со всеми предикторами
m1 <- lm(y ~ x1 + x2 + x3, data = df_mc)
summary(m1)
## 
## Call:
## lm(formula = y ~ x1 + x2 + x3, data = df_mc)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.49138 -0.65392  0.05664  0.67033  2.53210 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -0.01933    0.10734  -0.180    0.858    
## x1           1.50548    1.04102   1.446    0.151    
## x2           0.46218    1.09456   0.422    0.674    
## x3           2.94261    0.11223  26.219   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.052 on 96 degrees of freedom
## Multiple R-squared:  0.9003, Adjusted R-squared:  0.8972 
## F-statistic:   289 on 3 and 96 DF,  p-value: < 2.2e-16
# VIF: > 5-10 — признак мультиколлинеарности
vif(m1)
##        x1        x2        x3 
## 80.852638 80.779423  1.017576
# Убираем один из коррелирующих признаков
m2 <- lm(y ~ x1 + x3, data = df_mc)
summary(m2)
## 
## Call:
## lm(formula = y ~ x1 + x3, data = df_mc)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.44091 -0.63799  0.04562  0.69463  2.52196 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -0.02423    0.10626  -0.228     0.82    
## x1           1.94228    0.11626  16.707   <2e-16 ***
## x3           2.94377    0.11172  26.349   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.047 on 97 degrees of freedom
## Multiple R-squared:  0.9001, Adjusted R-squared:  0.8981 
## F-statistic: 437.1 on 2 and 97 DF,  p-value: < 2.2e-16
vif(m2)
##      x1      x3 
## 1.01697 1.01697

Вывод

Корреляция x1 и x2 ≈ 0.99. VIF для этих двух признаков сильно превышает порог 10 — это классический признак мультиколлинеарности: коэффициенты модели становятся неустойчивыми, стандартные ошибки растут, значимость теряется. После удаления x2 VIF для оставшихся предикторов < 5, коэффициенты модели стабильны и интерпретируемы. Практический вывод: при VIF > 10 один из коррелирующих признаков нужно удалить или объединить (например, через PCA).