24  Многомерные методы анализа данных

Автор

И.С. Поздняков

Многомерные методы анализа данных – методы работы с данными, в которых много колонок. Мы уже сталкивались с некоторыми многомерными методами, например, с множественной линейной регрессией (Глава 21.5). Поэтому вы знаете, что многомерность создает новые проблемы. Например, при множественных корреляциях или попарных сравнениях возникает проблема множественных сравнений, а при использовании множественной регрессии лишние предикторы могут ловить только шум и приводить к переобучению (если говорить в терминах машинного обучения). Короче говоря, больше - не значит лучше. Нужно четко понимать, зачем мы используем данные и что пытаемся измерить.

Однако в некоторых случаях мы в принципе не можем ничего интересного сделать с маленьким набором переменных. Много ли мы можем измерить личностным тестом с одним единственным вопросом? Можем ли мы точно оценить уровень интеллекта по успешности выполнения одного единственного задания? Очевидно, что нет. Более того, даже концепция интеллекта в современном его представлении появилась во многом благодаря разработке многомерных методов анализа! Ну или наоборот: исследования интеллекта подстегнули развитие многомерных методов.

24.1 Уменьшение размерности

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

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

library(tidyverse)
Warning: package 'dplyr' was built under R version 4.2.3
Warning: package 'stringr' was built under R version 4.2.3
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.4.4     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.0
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(palmerpenguins)

penguins <- penguins %>%
  drop_na(bill_length_mm:body_mass_g)

penguins %>%
  select(bill_length_mm:body_mass_g) %>%
  plot(col = penguins$species)

Даже представить как устроены многомерные данные очень трудно, а ведь мы отобразили только четыре числовых колонки! Понять связи между отдельными переменными мы можем, если посмотрим на каждую диаграмму рассеяния по отдельности, но и то если их не слишком много. Но связи в многомерных данных могут быть гораздо более сложными и на простых диаграммах рассеяния их иногда нельзя заметить. Это как смотреть на тени, поворачивая какой-нибудь предмет различными сторонами.

Уменьшение размерности позволяет вытащить из данных самую мякотку, уместив исходные данные в небольшое количество переменных. Остальное из данных удаляется.

Осторожно: уменьшение размерности приводит к потере данных

Уменьшение размерности – не “бесплатная” операция. Уменьшение размерности всегда приводит к потере части данных и может значительно их изменить.

Данные сниженной размерности гораздо проще визуализировать. Например, снизив размерность данных до двух шкал, можно просто нарисовать диаграмму рассеяния. Анализируя связь полученных шкал с изначальными шкалами и взаимное расположение точек в новых шкалах, можно многое понять об исследуемых данных.

Однако снижение размерности используется не только для эксплораторного анализа, но и для снижения количества фичей в машинном обучения, для удаления шума из данных, для более экономного хранения данных и многих других задач.

24.2 Анализ главных компонент (Principal component analysis)

Анализ главных компонент (АГК; Principal component analysis) – это, пожалуй, самый известный метод уменьшения размерности. АГК просто поворачивает систему координат многомерного пространства, которое задано имеющимися числовыми колонками. Координатная система поворачивается таким образом, чтобы первые оси объясняли как можно больший разброс данных, а последние - как можно меньший. Тогда мы могли бы отбросить последние оси и не очень-то многое потерять в данных. Для двух осей это выглядит вот так:

Первая ось должна минимизировать красные расстояния. Вторая ось будет просто перпендикулярна первой оси.

Для продвинутых: какая математика стоит за АГК?
  1. Для начала мы посчитаем корреляционную матрицу (как вариант – ковариационную матрицу, если мы не хотим делать z-преобразование переменных
penguins_cor <- penguins %>%
  select(bill_length_mm:body_mass_g) %>%
  cor()
penguins_cor
                  bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
bill_length_mm         1.0000000    -0.2350529         0.6561813   0.5951098
bill_depth_mm         -0.2350529     1.0000000        -0.5838512  -0.4719156
flipper_length_mm      0.6561813    -0.5838512         1.0000000   0.8712018
body_mass_g            0.5951098    -0.4719156         0.8712018   1.0000000
  1. Находим собственные векторы (eigenvectors) и собственные значения (eigenvalues) матрицы корреляций или ковариаций.
eigens <- eigen(penguins_cor)
eigens
eigen() decomposition
$values
[1] 2.7537551 0.7725168 0.3652359 0.1084922

$vectors
           [,1]         [,2]       [,3]       [,4]
[1,]  0.4552503 -0.597031143  0.6443012  0.1455231
[2,] -0.4003347 -0.797766572 -0.4184272 -0.1679860
[3,]  0.5760133 -0.002282201 -0.2320840 -0.7837987
[4,]  0.5483502 -0.084362920 -0.5966001  0.5798821

Собственные вектора - это такие векторы матрицы, умножив которые на данную матрицу, можно получи вектор с тем же направлением, но другой длины.

eigens$vectors
           [,1]         [,2]       [,3]       [,4]
[1,]  0.4552503 -0.597031143  0.6443012  0.1455231
[2,] -0.4003347 -0.797766572 -0.4184272 -0.1679860
[3,]  0.5760133 -0.002282201 -0.2320840 -0.7837987
[4,]  0.5483502 -0.084362920 -0.5966001  0.5798821

А вот коэффициент множителя длины нового вектора - это собственное значение.

eigens$values
[1] 2.7537551 0.7725168 0.3652359 0.1084922

В контексте АГК, собственные вектора - это новые оси (т.е. те самые новые компоненты), а собственные значения - это размер объясняемой дисперсии с помощью новых осей. Чтобы перейти от дисперсии к стандартному отклонению, нам нужно взять корень из собственных значений:

sqrt(eigens$values)
[1] 1.6594442 0.8789293 0.6043475 0.3293816

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

Именно таким образом считается АГК в функции princomp().

penguins_princomp <- penguins %>%
  select(bill_length_mm:body_mass_g) %>%
  princomp(cor = TRUE)
penguins_princomp$loadings

Loadings:
                  Comp.1 Comp.2 Comp.3 Comp.4
bill_length_mm     0.455  0.597  0.644  0.146
bill_depth_mm     -0.400  0.798 -0.418 -0.168
flipper_length_mm  0.576        -0.232 -0.784
body_mass_g        0.548        -0.597  0.580

               Comp.1 Comp.2 Comp.3 Comp.4
SS loadings      1.00   1.00   1.00   1.00
Proportion Var   0.25   0.25   0.25   0.25
Cumulative Var   0.25   0.50   0.75   1.00
penguins_princomp$sdev
   Comp.1    Comp.2    Comp.3    Comp.4 
1.6594442 0.8789293 0.6043475 0.3293816 

Функция prcomp() идет другим путем, используя сингулярное разложение матрицы (singular value decomposition) исходных данных, это дает чуть более точные результаты.

  1. Последний этап – поворот исходных данных в новом пространстве с помощью матричного перемножения изначальных данных (\(z\)-трансформированных или нет).
penguins_original_scales <- penguins %>%
  select(bill_length_mm:body_mass_g) %>%
  as.matrix() %>%
  scale()

head(penguins_original_scales %*% eigens$vectors)
          [,1]        [,2]       [,3]       [,4]
[1,] -1.840748 -0.04763243 -0.2324536  0.5231365
[2,] -1.304850  0.42772154 -0.0295191  0.4018377
[3,] -1.367178  0.15425039  0.1983816 -0.5272343
[4,] -1.876078  0.00204541 -0.6176912 -0.4776785
[5,] -1.908951 -0.82799642 -0.6855795 -0.2071241
[6,] -1.760446  0.35096537  0.0276311  0.5039784
penguins_prcomp <- penguins %>%
  select(bill_length_mm:body_mass_g) %>%
  prcomp(center = TRUE, scale. = TRUE)

head(penguins_prcomp$x)
           PC1         PC2        PC3        PC4
[1,] -1.840748 -0.04763243  0.2324536  0.5231365
[2,] -1.304850  0.42772154  0.0295191  0.4018377
[3,] -1.367178  0.15425039 -0.1983816 -0.5272343
[4,] -1.876078  0.00204541  0.6176912 -0.4776785
[5,] -1.908951 -0.82799642  0.6855795 -0.2071241
[6,] -1.760446  0.35096537 -0.0276311  0.5039784

Итак, для начала нам нужно центрировать и нормировать данные - вычесть среднее и поделить на стандартное отклонение, т.е. посчитать \(z\)-оценки (см. Глава 12.5.3.1). Это нужно для того, чтобы сделать все шкалы равноценными. Это особенно важно делать когда разные шкалы используют несопоставимые единицы измерения. Скажем, одна колонка - это масса человека в килограммах, а другая - рост в метрах. Если применять АГК на этих данных, то ничего хорошего не выйдет: вклад роста будет слишком маленьким. А вот если мы сделаем \(z\)-преобразование, то приведем и вес, и рост к “общему знаменателю”.

В базовом R уже есть инструменты для АГК princomp() и prcomp(), считают они немного по-разному. Возьмем более рекомендуемый вариант, prcomp(). Эта функция умеет самостоятельно поводить \(z\)-преобразования, для чего нужно поставить center = TRUE и scale. = TRUE.

penguins_prcomp <- penguins %>%
  select(bill_length_mm:body_mass_g) %>%
  prcomp(center = TRUE, scale. = TRUE)

Уже много раз встречавшаяся нам функция summary(), примененная на результат проведения АГК, выдаст информацию о полученных компонентах. Наибольший интерес представляют строчки “Proportion of Variance” (доля дисперсии, объясненная компонентой) и “Cumulative Proportion” (накопленная доля дисперсии для компоненты данной и всех предыдущих компонент).

summary(penguins_prcomp)
Importance of components:
                          PC1    PC2     PC3     PC4
Standard deviation     1.6594 0.8789 0.60435 0.32938
Proportion of Variance 0.6884 0.1931 0.09131 0.02712
Cumulative Proportion  0.6884 0.8816 0.97288 1.00000

Функция plot() повзоляет визуализировать соотношение разных компонент.

plot(penguins_prcomp)

Как мы видим, первый компонент объясняет большую часть дисперсии, второй и третий компоненты заметно меньше, последний – совсем немного, скорее всего, этот компонент репрезентирует некоторый шум в данных.

Теперь мы можем визуализировать первые два компонента. Это можно сделать с помощью базовых инструментов R.

plot(penguins_prcomp$x[,1:2], col=penguins$species)

Однако пакет {factoextra} представляет гораздо более широкие возможности для визуализации.

install.packages("factoextra")

Во-первых, как и с помощью базового plot(), можно нарисовать диаграмму рассеяния. Для этого воспользуемся функцией fviz_pca_ind():

library(factoextra)
Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
fviz_pca_ind(penguins_prcomp)

Параметром axes = можно выбрать нужные компоненты для отображения.

fviz_pca_ind(penguins_prcomp,
             axes = c(3, 4))

Добавить расцветку можно с помощью параметра habillage =, в которой надо подставить нужный вектор из изначальных данных.

fviz_pca_ind(penguins_prcomp, 
             habillage = penguins$species)

Параметр addEllipses = TRUE позволяет обрисовать эллипсы вокруг точек, если те раскрашены по группам с помощью habillage =.

fviz_pca_ind(penguins_prcomp, 
             habillage = penguins$species,
             addEllipses = TRUE)

Чтобы рассмотреть отдельные точки, можно поставить repel = TRUE:

fviz_pca_ind(penguins_prcomp, 
             habillage = penguins$species,
             addEllipses = TRUE,
             repel = TRUE)

В дополнение к диаграмме рассеяния можно нарисовать график переменных функцией fviz_pca_var(). По осям \(x\) и \(y\) будут отображены первая и вторая компоненты соответственно. Как и для функции fviz_pca_ind(), можно выбрать и другие компоненты с помощью параметра axes =.

fviz_pca_var(penguins_prcomp)

Вместо отдельных точек мы видим здесь стрелочки, которые представляют собой изначальные шкалы. Чем ближе эти стрелочки к осям \(x\) и \(y\), тем больше коэффициент корреляции между исходной шкалой и выбранной компонентой.

Если коэффициент корреляции отрицательный, то стрелочка исходной шкалы направлена в противоположную сторону от оси (влево или вниз).

Наконец, есть так называемый биплот (biplot), в котором объединены оба графика: и индивидуальные точки, и стрелочки с изначальными шкалами!

fviz_pca_biplot(penguins_prcomp,
                addEllipses = TRUE,
                habillage = penguins$species)

Еще один способ нарисовать биплот – с помощью пакета {ggfortify}. В этом пакете есть функция autoplot(), которая автоматически рисует что-то в зависимости от класса объекта на входе. Если это объект класса prcomp, то мы получим тот самый биплот.

library(ggfortify)
autoplot(penguins_prcomp, data = penguins, colour = "species")

24.3 tSNE

t-Distributed Stochastic Neighbor Embedding (t-SNE) - это еще один метод снижения размерности, который используется, в основном, для визуализации многомерных данных со сложными нелинейными связями между переменными в пространстве меньшей размерности (двумерном или трехмерном).

Идея t-SNE состоит в том, чтобы расположить точки данных на такой картинке таким образом, чтобы близкие объекты оказались близко друг к другу, а далекие объекты - далеко друг от друга. Для этого используется математический подход, основанный на вероятностях и расстояниях между объектами.

Для примера возьмем знаменитый набор данных MNIST (“Modified National Institute of Standards and Technology”). MNIST – это своего рода iris или penguins в мире глубокого обучения: самый базовый учебный набор данных, на котором учатся писать искусственные нейросети. Этот набор данных очень большой, поэтому мы будем работать с его уменьшенной версией:

mnist_small <- read_csv("https://raw.githubusercontent.com/Pozdniakov/tidy_stats/master/data/mnist_small.csv")
Rows: 6000 Columns: 785
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (785): label, 1x1, 1x2, 1x3, 1x4, 1x5, 1x6, 1x7, 1x8, 1x9, 1x10, 1x11, 1...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
mnist_small %>%
  select(1,400:410) %>%
  head()
# A tibble: 6 × 12
  label `15x7` `15x8` `15x9` `15x10` `15x11` `15x12` `15x13` `15x14` `15x15`
  <dbl>  <dbl>  <dbl>  <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
1     2      0      0     22       0       0       0       0       0       0
2     8      0      0      0       0     116     254      89      54     235
3     6      0      0      0      56     252     252     252     252     252
4     1      0      0      0       0       0       0      13     200     253
5     5      0      0      0       0      32     191     233     187     145
6     3      0     27     76      44     194     204     226     253     253
# ℹ 2 more variables: `15x16` <dbl>, `15x17` <dbl>

MNIST содержит большое количество написанных от руки цифр, каждый столбец – это яркость отдельного пикселя.

Пример рукописных цифр из набора данных MNIST

Особенность этого набора данных в том, что стандартными линейными методами снижения размерности такими как АГК здесь не обойтись: нужно “поймать” сложные взаимоотношения между различными пикселями, которые создают различные палки, кружочки и закорючки.

Давайте посмотрим, как справится в этой ситуации АГК:

mnist_pca <- mnist_small %>%
  select(!label) %>% #удалим колонку с цифрой
  select(where(function(x) !all(x == 0))) %>% #и пустые колонки удалим
  prcomp(center = TRUE, scale. = TRUE)

pca_df <- mnist_pca$x[, 1:2] %>%
  as_tibble() %>%
  bind_cols(mnist_small$label) %>%
  slice_sample(prop = .2)
New names:
• `` -> `...3`
names(pca_df) <- c("x", "y", "label")
pca_df %>%
  ggplot() +
  geom_text(aes(x = x, 
                y = y, 
                label = label, 
                colour = as.factor(label))) +
  guides(colour = "none") +
  theme_minimal()

Какое-то разделение произошло, но большая часть цифр находится в общей куче.

Теперь попробуем посмотреть, что сделает с теми же данными t-SNE. Для этого воспользуемся пакетом {Rtsne}.

install.packages("Rtsne")

t-SNE работает итеративно, перемещая точки на картинке, чтобы улучшить соответствие между исходными данными и их представлением на картинке. t-SNE – довольно ресурсоемкий алгоритм (особенно по сравнению с АГК), поэтому в функции Rtsne() есть множество настроек для контроля скорости ее работы. Например, можно выбрать максимальное количество итераций с помощью параметра max_iter =. Но самый главный параметр – dims =, в котором нужно задать количество измерений, которое мы хотим получить. По умолчанию dims = 2, но можно поставить, например, dims = 3, чтобы получить точки в трехмерном пространстве.

library(Rtsne)
set.seed(42)
tsne <- mnist_small %>%
  select(!label) %>%
  Rtsne()

Теперь соединим результаты с исходными цифрами и визуализируем результаты:

tsne_df <- bind_cols(tsne$Y, mnist_small$label)
New names:
• `` -> `...1`
• `` -> `...2`
• `` -> `...3`
names(tsne_df) <- c("x", "y", "label")

tsne_df %>%
  slice_sample(prop = .2) %>%
  ggplot() +
  geom_text(aes(x = x, 
                y = y, 
                label = label, 
                colour = as.factor(label))) +
  guides(colour = "none") +
  theme_minimal()

Как видите, t-SNE гораздо лучше справился с разделением цифр на группы: схожие цифры находятся рядом, непохожие – далеко. На получившемся графике заметно, что цифры 4 и 9 плохо разделись, но это довольно закономерно: эти цифры похожи по написанию.

Важно отметить, что t-SNE не сохраняет все абсолютные расстояния между объектами, поэтому важно рассматривать результат в контексте относительных расстояний. Также стоит помнить, что t-SNE не является методом для точного измерения расстояний или сравнения объектов, а скорее для визуального представления структуры данных.

24.4 Эксплораторный факторный анализ

24.5 Конфирматорный факторный анализ

24.6 Кластерный анализ

iris_3means <- kmeans(iris %>% select(!Species), centers = 3)
table(iris$Species, iris_3means$cluster)
            
              1  2  3
  setosa     50  0  0
  versicolor  0  2 48
  virginica   0 36 14
plot(iris %>% select(!Species), col = iris$Species, pch = iris_3means$cluster)

24.7 Многомерное шкалирование

24.8 Сетевой анализ

24.9 Другие методы многомерного анализа данных