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


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


Данные представляют собой выборку потребления алкоголя по странам на душу населения по типу алкогольных напитков (пиво, вино, спиртные напитки и др.) за 2010 год в процентах от потребления алкоголя на душу населения. Также данные содержат: среднее суточное потребление алкоголя на душу населения в граммах чистого алкоголя и всё (зарегистрировано + неучтенное) потребление алкоголя на душу населения (только пьющие в литрах чистого алкоголя).


В то же время каждая страна условно относится к одной из географических групп: восток, центр и запад. Разделение весьма условное и очень спорное по разным причинам, но будем отталкиваться от того, что есть. Источник данных — Global status report on alcohol and health 2014, С. 289-364



(Раскрашивал вручную, могут быть погрешности, но общая мысль, думаю, понятна)


Предварительный анализ


Подключим используемые библиотеки.


library(rgl)
library(heplots)
library(MVN)
library(klaR)
library('Morpho')
library(caret)
library(mclust)
library(ggplot2)
library(GGally)
library(plyr)
library(psych)
library(GPArotation)
library(ggpubr)

Загрузим данные и посмотрим, как они выглядят.


# Загружаем данные из файла
data <- read.table("alcohol_data.csv", header=TRUE,  sep=",")
# Делаем из первого столбца названия строк
rownames(data) <- make.names(data[,1], unique = TRUE)
# Записываем данные и удаляем пропуска, если они есть
data <- data[,-1]
data <- na.omit(data)
# Выводим несколько первых строчек
head(data)

Beer Wine Spirit Other Total Average_daily Group
Albania 31.8 19.8 48.4 0.0 13.0 27.5 center
Armenia 9.7 5.3 84.9 0.0 8.3 17.9 east
Austria 50.4 35.5 14.0 0.0 13.8 29.6 center
Azerbaijan 28.7 7.6 63.3 0.0 5.2 11.1 east
Belarus 17.3 5.2 46.6 30.9 22.1 48.0 east
Belgium 49.2 36.3 14.4 0.1 12.8 27.7 center
... ... ... ... ... ... ... ...

Посчитаем стандартные статистики


summary(data)


Глядя на эти статистики, можно многое сказать о характере данных. Например, что у Other очень большой разброс, и максимум сильно отстоит от третьего квартиля, значит, там есть как минимум одно сильно отличающееся наблюдение, так называемый выброс. Также видно по первому квартилю и среднему, что в этом столбце существенное количество нулевых значений, что, скорее всего, связано с недостатком данных. Также сразу видим сколько стран в каждой из групп, группы не сбалансированны по количеству. Аналогично можно рассматривать и статистики по другим признакам и делать какие-то полезные выводы и предположения.


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


options(rgl.useNULL=TRUE)
open3d()
mfrow3d(2,2)
levelColors <- c('west'='blue', 'east'='red', 'center'='yellow')
plot3d(data$Beer, data$Wine, data$Spirit, xlab="Beer", ylab="Wine", zlab="Spirit", col = levelColors[data$Group], size=3)

widget <- rglwidget()
widget

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



Посмотрим эмпирические плотности по группам


ggpairs(
  data,
  mapping = ggplot2::aes(color = data$Group),
  upper = list(continuous = wrap("cor", alpha = 0.5), combo = "box"),
  lower = list(continuous = wrap("points", alpha = 0.3), combo = wrap("dot", alpha = 0.4)),
  diag = list(continuous = wrap("densityDiag",alpha = 0.5)),
  title = "Alcohol"
)


Так как Average и Total сильно коррелируют, исключим из рассмотрения Average.


data <- data[, -6]

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


data[data$Wine>60,]

Beer Wine Spirit Other Total Group
Italy 23 65.6 11.5 0 9.9 west

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


data[data$Spirit>70,]
data[data$Spirit<10,]

Beer Wine Spirit Other Total Group
Armenia 9.7 5.3 84.9 0 8.3 east

Beer Wine Spirit Other Total Group
Slovenia 44.5 46.9 8.6 0 17.2 west

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


Данных не так много, поэтому посмотрим на данные по группам


split(data[,1:5],data$Group)

$center


Beer Wine Spirit Other Total
Albania 31.8 19.8 48.4 0.0 13.0
Austria 50.4 35.5 14.0 0.0 13.8
Belgium 49.2 36.3 14.4 0.1 12.8
Bosnia.and.Herzegovina 73.3 9.7 17.0 0.0 12.3
Cyprus 40.9 24.7 33.7 0.7 10.8
Czech.Republic 53.5 20.5 26.0 0.0 14.6
Denmark 37.7 48.2 14.1 0.0 12.9
Finland 46.0 17.5 24.0 12.6 18.1
Germany 53.6 27.8 18.6 0.0 14.7
Hungary 36.3 29.4 34.3 0.0 16.3
Iceland 61.8 21.2 16.5 0.5 10.4
Ireland 48.1 26.1 18.7 7.7 14.7
Malta 39.4 32.7 27.2 0.7 11.5
Netherlands 46.8 36.4 16.9 0.0 11.2
Norway 44.2 34.7 19.0 2.1 9.0
Poland 55.1 9.3 35.5 0.0 24.2
Romania 50.0 28.9 21.1 0.0 21.3
Serbia 51.5 23.9 24.6 0.0 19.0
Sweden 37.0 46.6 15.1 1.4 13.3
Switzerland 31.8 49.4 17.6 1.2 12.1
Turkey 63.6 8.6 27.9 0.0 17.3
UK 36.9 33.8 21.8 7.5 13.8

$east


Beer Wine Spirit Other Total
Armenia 9.7 5.3 84.9 0.0 8.3
Azerbaijan 28.7 7.6 63.3 0.0 5.2
Belarus 17.3 5.2 46.6 30.9 22.1
Bulgaria 39.3 16.5 44.1 0.1 16.9
Estonia 41.2 11.1 36.8 10.9 15.7
Georgia 17.0 49.8 33.2 0.1 21.2
Israel 44.0 6.2 49.5 0.3 5.4
Latvia 46.9 10.7 37.0 5.4 18.1
Lithuania 46.5 7.8 34.1 11.6 23.6
Republic.of.Moldova 30.4 5.1 64.5 0.0 25.4
Russian.Federation 37.6 11.4 51.0 0.0 22.3
Slovakia 30.1 18.3 46.2 5.5 19.8
Ukraine 40.5 9.0 48.0 2.6 20.3

$west


Beer Wine Spirit Other Total
Croatia 39.5 44.8 15.4 0.2 15.1
France 18.8 56.4 23.1 1.7 12.9
Greece 28.1 47.3 24.2 0.4 15.6
Italy 23.0 65.6 11.5 0.0 9.9
Luxembourg 36.2 42.8 21.0 0.0 12.7
Portugal 30.8 55.5 10.9 2.8 22.6
Slovenia 44.5 46.9 8.6 0.0 17.2
Spain 49.7 20.1 28.2 1.8 16.4
Republic.of.Macedonia 47.4 39.9 12.6 0.0 11.7

ggpairs(
  data,
  mapping = ggplot2::aes(color = data$Group),
  diag=list(continuous="bar", alpha=0.4)
)


Из гистограмм видно, что группы лучше всего различаются по пиву, вину и спиртным напиткам. Если брать признак Other, заметим: мало того, что у него весьма странное распределение, так еще и в большинстве стран маленькие и даже нулевые значения, кроме нескольких (примерно 10-12 стран, а так как индивидов всего 45, исключив их, мы существенно сократим объем данных). Больше всего отличается Республика Беларусь. Немного подумав, можно сделать предположение, что там предпочитают крамбамбуля вместо пива, вина и спиртных напитков (шутка). Ну, а если более формально, то скорее всего это связано с неполнотой исходных данных или несовершенством регистрации данных. Уберем признак Other из рассмотрения.


Также на гистограммах можно заметить, что для центра превалирует пиво, для запада — вино, а для востока — спиртные напитки. Это вполне укладывается в общеизвестные представления, можно даже сказать — стереотипы, о культуре потребления спиртных напитков в этих регионах.
На диаграммах рассеяния признака Total и Other, визуально группы не выделяются. Будем иметь этот факт в виду в дальнейшем.


Примечательно, что между признаками Beer, Spirit и Wine отрицательные корреляции. Возможно, это также относится к тому, что по этим переменным можно выделять группы предпочтения в алкоголе, и они будут близки к географическим. После того как изучили данные, получили некие априорные представления, убрали лишние, на наш взгляд, признаки, перейдем к кластерному анализу.


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


Уберем разметку данных на группы и уберем признак Total. Это позволит не нормировать данные, так как остальные признаки — в одной шкале.


data.group = data[,5]
data <- data[,-5]
data<- data[,-4]

Определим число кластеров Elbow method (“метод согнутого колена”, он же “метод каменистой осыпи”). Построим график, где по оси абсцисс отмечено число кластеров k, а по оси ординат – значения функции W(K), которая определяет внутригрупповой разброс в зависимости от числа кластеров.


library(factoextra)
fviz_nbclust(data, kmeans, method = "wss") +
  labs(subtitle = "Elbow method") +
  geom_vline(xintercept = 4, linetype = 2)


data.dist <- dist((data))
hc <- hclust(data.dist, method = "ward.D2")
plot(hc, cex = 0.7)


Нарисуем график поинтереснее. Цвета будут означать исходную географическую маркировку.


colors=c('green', 'red', 'blue')
hcd = as.dendrogram(hc)
clusMember = cutree(hc, 4)
colLab <- function(n) {
    if (is.leaf(n)) {
        a <- attributes(n)
        labCol <- colors[data.group[n]]
        attr(n, "nodePar") <- c(a$nodePar, lab.col = labCol)
    }
    n
}
clusDendro = dendrapply(hcd, colLab)
plot(clusDendro, main = "Cool Dendrogram", type = "triangle")

rect.hclust(hc, k = 4)


Грузия из восточного кластера единственная не попала в свой географический кластер. Пока отложим интерпретацию, посмотрим на другие методы.
Причем здесь, наверное, лучше использовать три кластера, так как в четвертый кластер странно выделились всего 4 страны.


plot(clusDendro, main = "Cool Dendrogram", type = "triangle")
data.hclas_group <- factor(cutree(hc, k = 3))

rect.hclust(hc, k = 3)


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


library(FactoMineR)
res.pca <- PCA(data,scale.unit=T, graph = F)
fviz_pca_biplot(res.pca, 
                col = colors[data.hclas_group], palette = "jco", 
                label = "var",
                ellipse.level = 0.8,
                 addEllipses = T,
                col.var = "black",
                legend.title = "groups4")


Третья группа, которая по центру, портит всю картину. Если бы она была покучнее, можно было говорить о кластеризации, а так это, скорее, сегментация. В целом, между группами заметно различие, посмотрим, как справится метод k-средних++.


library(flexclust)
data.kk <- kcca(data, k=3, family=kccaFamily("kmeans"),
control=list(initcent="kmeanspp"))

fviz_pca_biplot(res.pca, 
                col.ind =as.factor(data.kk@cluster), palette = "jco", 
                label = "var",
                ellipse.level = 0.8,
                 addEllipses = T,
                col.var = "black", repel = TRUE,
                legend.title = "clusters")


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


Больше всего, исходя из исходных классов, на правду похож hclust. Проинтерпретируем результат.



Наиболее полный кластер, в смысле исходной классификации, образовали восточные страны. К ним попала Албания из центральной части. Стоит отметить, что Албания пространственно находится недалеко от восточных стран.


Второй кластер также очень похож на исходное разделение. В третьем же все плохо. В исходной выборке там меньше всего индивидов, и они очень близки по предпочтениям в алкоголе, согласно данным, к центральной части. Можно попробовать условно разделить на два кластера, так как видно, что для интерпретации лучше всего так и сделать и резюмировать биполярность Европы. Тогда кластера практически совпадут с Восточной и Западной Европой, где в Западную войдет центральная и Западная по исходным обозначениям.


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