Формулировка: сформировать собственный датасет с
помощью функции c() (конкатенация), в котором содержатся
числовые данные и NA значения.
my_data <- c(12, 5, NA, 8, NA, 20, 15, NA, 3, 9, 100, NA, 45)
my_data
## [1] 12 5 NA 8 NA 20 15 NA 3 9 100 NA 45
Формулировка: провести очистку данных с
использованием функции is.na() и вывести “чистый”
датасет.
is.na(my_data)
## [1] FALSE FALSE TRUE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE TRUE
## [13] FALSE
clean_data <- my_data[!is.na(my_data)]
clean_data
## [1] 12 5 8 20 15 3 9 100 45
Вывод: is.na() возвращает логический
вектор — TRUE там, где стоит пропуск. Инвертировав его
(!is.na(...)) и применив как индекс к исходному вектору, мы
оставляем только те элементы, где пропуска нет. Из 13 исходных значений
осталось 9 — ровно столько, сколько было не-NA элементов.
Формулировка: сгенерировать таблицу данных с
числовыми и текстовыми столбцами (аналогично с помощью функции
c()), очистить данные с помощью функции
complete.cases().
df <- data.frame(
id = c(1, 2, 3, NA, 5, 6),
name = c("Alice", "Bob", NA, "David", "Eve", "Frank"),
score = c(85, NA, 90, 75, NA, 60),
stringsAsFactors = FALSE
)
df
## id name score
## 1 1 Alice 85
## 2 2 Bob NA
## 3 3 <NA> 90
## 4 NA David 75
## 5 5 Eve NA
## 6 6 Frank 60
complete.cases(df)
## [1] TRUE FALSE FALSE FALSE FALSE TRUE
clean_df <- df[complete.cases(df), ]
clean_df
## id name score
## 1 1 Alice 85
## 6 6 Frank 60
Вывод: в отличие от is.na(), которая
работает поэлементно, complete.cases() работает
построчно — возвращает TRUE только для тех
строк, где нет ни одного пропуска ни в одном столбце. Это удобно именно
для таблиц (data.frame), где нужно отбросить строку целиком, если хотя
бы одно значение в ней отсутствует.
Формулировка: проанализировать датасет
airquality с пропусками, с использованием функции
preProcess из пакета caret заполнить пропуски
предсказанными значениями (среднее, медиана).
data(airquality)
sum(is.na(airquality))
## [1] 44
colSums(is.na(airquality))
## Ozone Solar.R Wind Temp Month Day
## 37 7 0 0 0 0
air_mean <- airquality
for (col in names(air_mean)) {
if (is.numeric(air_mean[[col]])) {
air_mean[[col]][is.na(air_mean[[col]])] <- mean(air_mean[[col]], na.rm = TRUE)
}
}
sum(is.na(air_mean))
## [1] 0
preProc_median <- preProcess(airquality, method = "medianImpute")
air_median <- predict(preProc_median, airquality)
sum(is.na(air_median))
## [1] 0
head(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
head(air_mean)
## Ozone Solar.R Wind Temp Month Day
## 1 41.00000 190.0000 7.4 67 5 1
## 2 36.00000 118.0000 8.0 72 5 2
## 3 12.00000 149.0000 12.6 74 5 3
## 4 18.00000 313.0000 11.5 62 5 4
## 5 42.12931 185.9315 14.3 56 5 5
## 6 28.00000 185.9315 14.9 66 5 6
head(air_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
шт.). Сам caret::preProcess() “из коробки” не умеет
заполнять средним (метода meanImpute там нет — есть только
medianImpute, knnImpute,
bagImpute), поэтому заполнение средним пришлось сделать
вручную циклом по столбцам, а медианное заполнение — через
preProcess(method = "medianImpute") +
predict(). После обеих операций пропусков в данных не
остаётся (sum(is.na(...)) равен 0), но конкретные
подставленные значения отличаются — среднее чувствительно к выбросам,
медиана устойчивее.
Формулировка: сгенерировать два числовых набора
данных и добавить в них выбросы, с использованием функции
boxplot обнаружить выбросы и удалить их.
set.seed(1)
data1 <- c(rnorm(100, mean = 50, sd = 10), 150, -50)
data2 <- c(rnorm(100, mean = 20, sd = 5), 100, -60)
bp1 <- boxplot(data1, main = "Данные 1 (с выбросами)")
bp1$out
## [1] 150 -50
bp2 <- boxplot(data2, main = "Данные 2 (с выбросами)")
bp2$out
## [1] 31.53989 100.00000 -60.00000
data1_clean <- data1[!(data1 %in% bp1$out)]
data2_clean <- data2[!(data2 %in% bp2$out)]
boxplot(data1_clean, main = "Данные 1 (без выбросов)")
boxplot(data2_clean, main = "Данные 2 (без выбросов)")
length(data1); length(data1_clean)
## [1] 102
## [1] 100
length(data2); length(data2_clean)
## [1] 102
## [1] 99
Вывод: boxplot() определяет выбросы по
правилу “1.5 межквартильных размаха” (IQR) — всё, что выходит за пределы
[Q1 - 1.5*IQR, Q3 + 1.5*IQR], попадает в $out.
Искусственно добавленные экстремальные значения (150 и -50 для первого
набора, 100 и -60 для второго) уверенно определяются как выбросы, после
их удаления диапазон значений на boxplot становится заметно уже и
compact, а “ящик” (межквартильный размах) визуально не меняется — это
ожидаемо, так как выбросы уже не участвовали в его расчёте.
Формулировка: сгенерировать таблицу данных, в
которой дублируются строки, удалить строки с использованием функций
unique(), duplicated(), сравнить
результаты.
df_dup <- data.frame(
x = c(1, 2, 2, 3, 3, 3, 4),
y = c("a", "b", "b", "c", "c", "c", "d"),
stringsAsFactors = FALSE
)
df_dup
## x y
## 1 1 a
## 2 2 b
## 3 2 b
## 4 3 c
## 5 3 c
## 6 3 c
## 7 4 d
df_unique <- unique(df_dup)
df_unique
## x y
## 1 1 a
## 2 2 b
## 4 3 c
## 7 4 d
duplicated(df_dup)
## [1] FALSE FALSE TRUE FALSE TRUE TRUE FALSE
df_no_dup <- df_dup[!duplicated(df_dup), ]
df_no_dup
## x y
## 1 1 a
## 2 2 b
## 4 3 c
## 7 4 d
identical(df_unique, df_no_dup)
## [1] TRUE
Вывод: оба способа дают одинаковый результат —
unique() сразу возвращает таблицу без повторяющихся строк,
а duplicated() возвращает логический вектор
(TRUE для повторов, кроме первого вхождения), который затем
используется как фильтр. identical() подтверждает, что
итоговые таблицы совпадают. Разница чисто в стиле: unique()
короче, а duplicated() даёт больше контроля (например,
можно инвертировать логику, оставить только повторы:
df_dup[duplicated(df_dup), ]).
Формулировка: обработать пропуски в данных с использованием пакета 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
imputed <- mice(airquality, m = 5, method = "pmm", seed = 500)
##
## iter imp variable
## 1 1 Ozone Solar.R
## 1 2 Ozone Solar.R
## 1 3 Ozone Solar.R
## 1 4 Ozone Solar.R
## 1 5 Ozone Solar.R
## 2 1 Ozone Solar.R
## 2 2 Ozone Solar.R
## 2 3 Ozone Solar.R
## 2 4 Ozone Solar.R
## 2 5 Ozone Solar.R
## 3 1 Ozone Solar.R
## 3 2 Ozone Solar.R
## 3 3 Ozone Solar.R
## 3 4 Ozone Solar.R
## 3 5 Ozone Solar.R
## 4 1 Ozone Solar.R
## 4 2 Ozone Solar.R
## 4 3 Ozone Solar.R
## 4 4 Ozone Solar.R
## 4 5 Ozone Solar.R
## 5 1 Ozone Solar.R
## 5 2 Ozone Solar.R
## 5 3 Ozone Solar.R
## 5 4 Ozone Solar.R
## 5 5 Ozone Solar.R
completed_data <- complete(imputed, 1)
sum(is.na(airquality))
## [1] 44
sum(is.na(completed_data))
## [1] 0
head(completed_data)
## 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 25 14.3 56 5 5
## 6 28 31 14.9 66 5 6
Вывод: в отличие от простого заполнения
средним/медианой, mice (Multivariate Imputation by Chained
Equations) строит несколько (m = 5) правдоподобных
вариантов заполнения пропусков, используя связи между остальными
переменными (метод pmm — predictive mean matching подбирает
похожее реально наблюдавшееся значение). Это статистически более
корректный подход, чем константное заполнение, так как учитывает
неопределённость и сохраняет распределение данных, а не “сплющивает” его
вокруг среднего.
Формулировка: разобрать пример с мультиколлинеарностью.
library(car)
set.seed(1)
x1 <- rnorm(100)
x2 <- x1 * 0.95 + rnorm(100, sd = 0.1)
x3 <- rnorm(100)
y <- 3 * x1 + 2 * x3 + rnorm(100)
cor(data.frame(x1, x2, x3))
## x1 x2 x3
## x1 1.00000000 0.99375671 0.01838219
## x2 0.99375671 1.00000000 0.01274276
## x3 0.01838219 0.01274276 1.00000000
model_collinear <- lm(y ~ x1 + x2 + x3)
summary(model_collinear)
##
## Call:
## lm(formula = y ~ x1 + x2 + x3)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.58201 -0.52738 0.00281 0.65058 1.82364
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.05273 0.10065 0.524 0.601547
## x1 3.46399 1.00226 3.456 0.000818 ***
## x2 -0.54942 1.04845 -0.524 0.601467
## x3 2.10462 0.09712 21.671 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.998 on 96 degrees of freedom
## Multiple R-squared: 0.9252, Adjusted R-squared: 0.9229
## F-statistic: 395.8 on 3 and 96 DF, p-value: < 2.2e-16
vif(model_collinear)
## x1 x2 x3
## 80.548502 80.534361 1.002798
model_fixed <- lm(y ~ x1 + x3)
summary(model_fixed)
##
## Call:
## lm(formula = y ~ x1 + x3)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.60977 -0.54846 0.01427 0.67975 1.86336
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.05473 0.10020 0.546 0.586
## x1 2.94205 0.11127 26.439 <2e-16 ***
## x3 2.10714 0.09664 21.805 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.9943 on 97 degrees of freedom
## Multiple R-squared: 0.925, Adjusted R-squared: 0.9234
## F-statistic: 598.1 on 2 and 97 DF, p-value: < 2.2e-16
vif(model_fixed)
## x1 x3
## 1.000338 1.000338
Вывод: переменная x2 искусственно
построена как почти линейная функция x1 (корреляция между
ними близка к 0.95+), из-за чего в модели model_collinear
значения VIF (variance inflation factor) для x1 и
x2 получаются заметно выше порога 5-10, что является
классическим признаком мультиколлинеарности — модель “не может
разделить” вклад x1 и x2 в объяснение
y, их коэффициенты становятся нестабильными и статистически
незначимыми, хотя по отдельности каждая переменная связана с
y. После исключения избыточной x2 (модель
model_fixed) VIF резко падает к значениям, близким к 1, а
коэффициенты при x1 и x3 становятся
стабильными и хорошо оцененными. Мультиколлинеарность не портит прогноз
модели в целом, но делает интерпретацию отдельных коэффициентов
ненадёжной — именно поэтому её нужно выявлять и устранять.
В работе рассмотрены базовые техники подготовки данных в R:
обнаружение и удаление пропусков поэлементно (is.na()) и
построчно (complete.cases()), содержательное заполнение
пропусков — от простого среднего/медианы через
caret::preProcess() до статистически обоснованного
множественного восстановления через пакет mice, обнаружение
и удаление выбросов через boxplot(), удаление дублирующихся
строк двумя эквивалентными способами (unique() и
duplicated()), а также диагностика мультиколлинеарности
предикторов с помощью car::vif(). Все эти шаги —
стандартная часть предобработки данных перед построением любой модели
машинного обучения.