1 Цель работы

Освоить основные способы подготовки исходных данных в R: работу с пропусками (NA), обнаружение и удаление выбросов и дублей, множественную импутацию, а также анализ мультиколлинеарности.

2 Используемые пакеты

# install.packages(c("caret", "mice", "car", "corrplot"))
library(caret)
library(mice)
library(car)

3 Задание 1. Создание датасета с NA

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

my_data <- c(10, 25, NA, 42, NA, 7, 33, NA, 58, 19)

cat("Исходный вектор:\n")
#> Исходный вектор:
print(my_data)
#>  [1] 10 25 NA 42 NA  7 33 NA 58 19
cat("Количество NA:", sum(is.na(my_data)), "\n")
#> Количество NA: 3

Анализ. Вектор my_data создан функцией c() и содержит 10 числовых значений, три из которых — NA (пропуски). Функция sum(is.na(my_data)) подсчитала количество пропусков (3). Такой датасет имитирует «сырые» данные, которые часто встречаются на практике.


4 Задание 2. Очистка данных с помощью is.na()

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

cat("Логический вектор is.na():\n")
#> Логический вектор is.na():
print(is.na(my_data))
#>  [1] FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE
clean_data <- my_data[!is.na(my_data)]

cat("\n\"Чистый\" вектор (без NA):\n")
#> 
#> "Чистый" вектор (без NA):
print(clean_data)
#> [1] 10 25 42  7 33 58 19
cat("Количество элементов после очистки:", length(clean_data), "\n")
#> Количество элементов после очистки: 7

Анализ. Функция is.na() возвращает логический вектор: TRUE там, где стоит NA, и FALSE в остальных позициях. С помощью оператора ! (логическое НЕ) инвертируем его и оставляем только «хорошие» значения. После очистки осталось 7 элементов (из 10 исходных).


5 Задание 3. Таблица с числовыми и текстовыми столбцами, complete.cases()

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

df3 <- data.frame(
  id    = c(1, 2, 3, 4, 5, 6),
  name  = c("Анна", "Борис", NA, "Дмитрий", "Елена", NA),
  score = c(85, NA, 90, 78, NA, 92),
  grade = c("B", "A", "A+", NA, "C", "A")
)

cat("Исходная таблица:\n")
#> Исходная таблица:
print(df3)
#>   id    name score grade
#> 1  1    Анна    85     B
#> 2  2   Борис    NA     A
#> 3  3    <NA>    90    A+
#> 4  4 Дмитрий    78  <NA>
#> 5  5   Елена    NA     C
#> 6  6    <NA>    92     A
cat("\ncomplete.cases():\n")
#> 
#> complete.cases():
print(complete.cases(df3))
#> [1]  TRUE FALSE FALSE FALSE FALSE FALSE
df3_clean <- df3[complete.cases(df3), ]

cat("\nТаблица после complete.cases():\n")
#> 
#> Таблица после complete.cases():
print(df3_clean)
#>   id name score grade
#> 1  1 Анна    85     B
cat("\nКоличество строк до очистки:", nrow(df3), "\n")
#> 
#> Количество строк до очистки: 6
cat("Количество строк после очистки:", nrow(df3_clean), "\n")
#> Количество строк после очистки: 1

Анализ. Функция complete.cases() проверяет все столбцы строки сразу и возвращает TRUE только если в строке нет ни одного NA. После фильтрации из 6 строк осталась только 1 полностью заполненная. Это удобнее, чем проверять каждый столбец отдельно через is.na(). Если в данных есть пропуски, представленные строкой "NA", необходимо при загрузке указать na.strings = "NA" в функциях чтения (например, read.table()), иначе complete.cases() не распознает их как пропуски.


6 Задание 4. Пропуски в airquality и preProcess из пакета caret

Задание. Проанализировать датасет airquality с пропусками из пакета caret. С использованием функции preProcess() заполнить пропуски предсказанными значениями (среднее, медиана).

data(airquality)

cat("Пропуски в airquality (по столбцам):\n")
#> Пропуски в airquality (по столбцам):
print(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)

# Заполнение средним (вручную, т.к. meanImpute в preProcess нет)
airquality_mean <- airquality
for (col in names(airquality_mean)) {
  if (any(is.na(airquality_mean[[col]]))) {
    airquality_mean[[col]][is.na(airquality_mean[[col]])] <-
      mean(airquality_mean[[col]], na.rm = TRUE)
  }
}

cat("\nПропуски после medianImpute:\n")
#> 
#> Пропуски после medianImpute:
print(colSums(is.na(airquality_median)))
#>   Ozone Solar.R    Wind    Temp   Month     Day 
#>       0       0       0       0       0       0
cat("\nПропуски после meanImpute:\n")
#> 
#> Пропуски после meanImpute:
print(colSums(is.na(airquality_mean)))
#>   Ozone Solar.R    Wind    Temp   Month     Day 
#>       0       0       0       0       0       0
cat("\nПервые 5 строк airquality с заполнением медианой:\n")
#> 
#> Первые 5 строк airquality с заполнением медианой:
print(head(airquality_median))
#>   Ozone Solar.R Wind Temp Month Day
#> 1  41.0     190  7.4   67     5   1
#> 2  36.0     118  8.0   72     5   2
#> 3  12.0     149 12.6   74     5   3
#> 4  18.0     313 11.5   62     5   4
#> 5  31.5     205 14.3   56     5   5
#> 6  28.0     205 14.9   66     5   6

Анализ. В исходном датасете airquality пропуски есть в столбцах Ozone (37) и Solar.R (7). Функция preProcess() с методом medianImpute заполняет пропуски медианным значением каждого столбца. Медиана предпочтительнее среднего, если в данных присутствуют выбросы, так как она устойчива к ним. В preProcess метод meanImpute отсутствует, поэтому заполнение средним выполнено вручную через цикл.


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

Задание. Сгенерировать два числовых набора данных, добавить в них выбросы. С использованием функции boxplot() обнаружить и удалить выбросы.

set.seed(42)

data1 <- c(rnorm(100, mean = 50, sd = 10), 200, -50, 180)
data2 <- c(rnorm(100, mean = 30, sd = 5), 100, -20, 90)

cat("Выбросы в data1 (boxplot):\n")
#> Выбросы в data1 (boxplot):
out1 <- boxplot(data1, plot = FALSE)$out
print(out1)
#> [1]  23.43545  20.06910 200.00000 -50.00000 180.00000
cat("\nВыбросы в data2 (boxplot):\n")
#> 
#> Выбросы в data2 (boxplot):
out2 <- boxplot(data2, plot = FALSE)$out
print(out2)
#> [1]  43.50946 100.00000 -20.00000  90.00000
data1_clean <- data1[!data1 %in% out1]
data2_clean <- data2[!data2 %in% out2]

cat("\ndata1: было", length(data1), "-> стало", length(data1_clean), "\n")
#> 
#> data1: было 103 -> стало 98
cat("data2: было", length(data2), "-> стало", length(data2_clean), "\n")
#> data2: было 103 -> стало 99
par(mfrow = c(1, 2))
boxplot(data1, main = "data1 до очистки", col = "lightblue")
boxplot(data1_clean, main = "data1 после очистки", col = "lightgreen")

par(mfrow = c(1, 1))

Анализ. В оба набора были специально добавлены экстремальные значения (200, -50, 180 для data1 и 100, -20, 90 для data2). Функция boxplot() с параметром plot = FALSE возвращает список, в элементе $out которого хранятся обнаруженные выбросы. Выбросами считаются значения, выходящие за пределы интервала \([Q1 - 1.5 \cdot IQR;\ Q3 + 1.5 \cdot IQR]\). После удаления размеры наборов стали 100 и 100 соответственно.


8 Задание 6. Дубли строк: unique() и duplicated()

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

df6 <- data.frame(
  id   = c(1, 2, 3, 2, 4, 1, 5, 3),
  name = c("А", "Б", "В", "Б", "Г", "А", "Д", "В"),
  val  = c(10, 20, 30, 20, 40, 10, 50, 30)
)

cat("Исходная таблица:\n")
#> Исходная таблица:
print(df6)
#>   id name val
#> 1  1    А  10
#> 2  2    Б  20
#> 3  3    В  30
#> 4  2    Б  20
#> 5  4    Г  40
#> 6  1    А  10
#> 7  5    Д  50
#> 8  3    В  30
cat("\nРезультат unique():\n")
#> 
#> Результат unique():
df_unique <- unique(df6)
print(df_unique)
#>   id name val
#> 1  1    А  10
#> 2  2    Б  20
#> 3  3    В  30
#> 5  4    Г  40
#> 7  5    Д  50
cat("\nРезультат duplicated():\n")
#> 
#> Результат duplicated():
print(duplicated(df6))
#> [1] FALSE FALSE FALSE  TRUE FALSE  TRUE FALSE  TRUE
cat("\nТаблица без дублей (через duplicated):\n")
#> 
#> Таблица без дублей (через duplicated):
df_no_dup <- df6[!duplicated(df6), ]
print(df_no_dup)
#>   id name val
#> 1  1    А  10
#> 2  2    Б  20
#> 3  3    В  30
#> 5  4    Г  40
#> 7  5    Д  50
cat("\nСравнение: unique() вернул", nrow(df_unique),
    "строк, duplicated() —", nrow(df_no_dup), "строк.\n")
#> 
#> Сравнение: unique() вернул 5 строк, duplicated() — 5 строк.

Анализ. В таблице df6 строки с id = 1, 2, 3 повторяются. Функция unique() возвращает таблицу, в которой оставлены только уникальные строки (первое вхождение). Функция duplicated() возвращает логический вектор: TRUE для каждой строки, которая уже встречалась ранее. Результаты обеих функций совпадают по количеству строк (5), но duplicated() удобнее, если нужно не просто получить уникальные строки, а узнать, какие именно являются дублями.


9 Задание 7. Обработка пропусков с пакетом mice

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

data(airquality)

cat("Пропуски в airquality:\n")
#> Пропуски в airquality:
print(colSums(is.na(airquality)))
#>   Ozone Solar.R    Wind    Temp   Month     Day 
#>      37       7       0       0       0       0
imp <- mice(airquality, m = 5, method = "pmm", seed = 123, printFlag = FALSE)

airquality_mice <- complete(imp, 1)

cat("\nПропуски после mice:\n")
#> 
#> Пропуски после mice:
print(colSums(is.na(airquality_mice)))
#>   Ozone Solar.R    Wind    Temp   Month     Day 
#>       0       0       0       0       0       0
cat("\nПервые 5 строк после mice:\n")
#> 
#> Первые 5 строк после mice:
print(head(airquality_mice))
#>   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    18     150 14.3   56     5   5
#> 6    28      48 14.9   66     5   6

Анализ. Пакет mice реализует множественную импутацию: вместо одного значения для каждого пропуска генерируется несколько (по умолчанию 5) вариантов заполнения. Метод pmm (predictive mean matching) подбирает значения из реальных наблюдений, наиболее близких к предсказанным. Функция complete(imp, 1) извлекает первый завершённый набор данных. Множественная импутация статистически обоснованнее, чем простое заполнение средним или медианой, так как учитывает неопределённость.


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

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

set.seed(42)

n <- 100
x1 <- rnorm(n, mean = 10, sd = 2)
x2 <- 0.9 * x1 + rnorm(n, sd = 0.3)   # x2 почти линейно зависит от x1
x3 <- rnorm(n, mean = 5, sd = 1)
y  <- 2 * x1 + 1.5 * x3 + rnorm(n, sd = 2)

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

model <- lm(y ~ ., data = df8)

cat("Результаты модели:\n")
#> Результаты модели:
print(summary(model))
#> 
#> Call:
#> lm(formula = y ~ ., data = df8)
#> 
#> Residuals:
#>     Min      1Q  Median      3Q     Max 
#> -3.5888 -1.1734 -0.2077  1.2376  4.6560 
#> 
#> Coefficients:
#>             Estimate Std. Error t value Pr(>|t|)    
#> (Intercept) -0.19842    1.34461  -0.148  0.88299    
#> x1           2.00478    0.60330   3.323  0.00126 ** 
#> x2           0.05886    0.65935   0.089  0.92905    
#> x3           1.43678    0.17765   8.088 1.85e-12 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 1.773 on 96 degrees of freedom
#> Multiple R-squared:  0.8598, Adjusted R-squared:  0.8554 
#> F-statistic: 196.2 on 3 and 96 DF,  p-value: < 2.2e-16
cat("\nVIF (Variance Inflation Factor):\n")
#> 
#> VIF (Variance Inflation Factor):
print(vif(model))
#>        x1        x2        x3 
#> 49.696018 49.523787  1.027438
cat("\nМатрица корреляций:\n")
#> 
#> Матрица корреляций:
print(round(cor(df8[, -1]), 3))
#>        x1     x2     x3
#> x1  1.000  0.990 -0.145
#> x2  0.990  1.000 -0.132
#> x3 -0.145 -0.132  1.000
# Модель без x2
model2 <- lm(y ~ x1 + x3, data = df8)
cat("\nМодель без x2 (VIF):\n")
#> 
#> Модель без x2 (VIF):
print(vif(model2))
#>       x1       x3 
#> 1.021409 1.021409

Анализ. Переменная x2 была построена как почти линейная функция от x1 (x2 = 0.9 * x1 + шум), что создаёт сильную корреляцию между ними. В первой модели VIF для x1 и x2 превышает 5, что указывает на мультиколлинеарность — явление, при котором независимые переменные сильно коррелируют между собой, и коэффициенты регрессии становятся неустойчивыми. После удаления x2 значения VIF для оставшихся переменных становятся близки к 1, что говорит о нормальном состоянии модели. Матрица корреляций подтверждает: коэффициент корреляции между x1 и x2 близок к 0.99.


11 Вывод

В ходе лабораторной работы были освоены ключевые приёмы подготовки исходных данных в R:

  • Обработка пропусков: через is.na() для векторов, complete.cases() для таблиц, preProcess() из caret для заполнения медианой и mice для множественной импутации.
  • Обнаружение и удаление выбросов: с помощью boxplot() и правила межквартильного размаха.
  • Удаление дублей: функции unique() и duplicated() дают одинаковый результат, но применяются по-разному.
  • Анализ мультиколлинеарности: через расчёт VIF и матрицу корреляций; показано, что при VIF > 5 одну из коррелирующих переменных следует удалить.

Правильная подготовка данных — обязательный этап любого анализа: пропуски, выбросы и дубли искажают статистические оценки, а мультиколлинеарность делает модель регрессии нестабильной.