Chapter 9 Clustering

9.1 Indledning og læringsmålene

9.1.1 Læringsmålene

Du skal være i stand til at

  • Beskrive hvad k-means clustering går ud på
  • Anvende kmeans og output resultatet på en tidy måde
  • Anvende map over forskellige antal clusters og vælge antallet som passer til de data
  • Anvende funktionen hclust for at lave et simpel hierarchical clustering

9.1.2 Inledning til chapter

I clustering er det til formål at dele observationerne i et datasæt op i forskellige grupper (clusters eller klynge på dansk), således at observationerne i sammen cluster ligner hinanden. Det øger indsigten i datasættet ved at fk. bedre forstår strukturen. Fk. hvor mange forskellige clusters er repræsenteret i mit datasæt? Og hvilke individuelle observationer tilhører hvilken cluster?

I dette kapitel ser vi hvordan vi kan implementere både k-means clustering og hierarchical clustering indenfor den tidyverse ramme.

9.1.3 Video ressourcer

  • Video 1: K-means clustering

Link her hvis det ikke virker nedenunder: https://player.vimeo.com/video/553656150


  • Video 2: augment, glanced og tidy med K-means. OBS der er en lille fejl i koden omkring 6:00 - den anden geom_point skal være geom_point(data = kclust_tidy,aes(x=bill_length,y=bill_depth),shape="x,colour="black") fordi tallerne er allerede basaserede på “scaled” data i kclust_tidy - se sektion 9.2.5 for uddybelse.

Link her hvis det ikke virker nedenunder: https://player.vimeo.com/video/553656139


  • Video 3: Hvor mange clusters skal man vælge?

Link her hvis det ikke virker nedenunder: https://player.vimeo.com/video/553656129


  • Video 4: Hierarchical clustering

(OBS Video 4 mangler: se gerne kursusnotaterne og jeg laver videoen ASAP)


9.2 Method 1: K-means clustering

library(palmerpenguins)
library(tidyverse)
library(broom)

I k-means clustering bliver samtlige observationer tilknyttet den nærmeste cluster “centroid” (se “hvordan fungerer kmeans?” nedenunder). I k-means er man nødt til at specificere antallet af clusters, som observationerne skal være delt op i, i forvejen. Derfor skal der være nogle undersøgelsesarbejde for at vælge den bedste antal clusters som passer til problemstillingen, eller som bedste repræsenterer datasættet.

Lad os tage udgangspunkt i datasættet penguins. Vi begynder med at få fjernet observationerne med NA i mindst én variable med funktionen drop_na og ved at specificere at year skal være en faktor (for at skelne den fra de andre numeriske kolonner):

data(penguins)
penguins <- penguins %>% 
  mutate(year=as.factor(year)) %>%
  drop_na() 

Vi vide allerede i forvejen, at der er 3 species med i de data, som vi plotter her med forskellige farver.

penguins %>% ggplot(aes(x=bill_length_mm,y=body_mass_g,colour=species)) + 
  geom_point() + 
  theme_classic()

Vi vil gerne bruge k-means clustering på de numeriske variabler i datasættet, og beregne 3 clusters ud fra dem. Derefter kan det være interessant at sammenligne de clusters vi få med de tre arter af pingvin - hvor gode er de clusters til at skelne i mellem de forskellige species, eller fanger de noget anden struktur i datasættet (for eksempel kønnet eller øen, de bor på)?

9.2.1 Hvordan fungere kmeans?

K-means er en iterativ process. Lad os forestille os at vi gerne vil have tre clusters i de data. Man starter med tre tilfældige observationer og kalder dem for de cluster middelværdier eller “centroids”. Man tilknytter alle observationer til én af de tre clusters (efter den nærmeste af de tre centroids), og så beregner en ny middelværdi/centroid for at hver cluster. Man tilknytter samtlige observationerne igen eften den nærmeste af de tre nye cluster centroids, og så gentager man processen flere gange. Efter flere gange konvergere de tre centroids til nogle faste værdier, der ikke længere ændre sig meget hver gange med gentager processen. Disse tre centroids defininere de tre endelige clusters og samtlige observationer er tilknyttet én af de tre.

Jeg spørger ikke efter detaljerne i metoden men der er mange videoer på Youtube som bedre foreklarer hvordan k-means fungerer, for eksempel: https://www.youtube.com/watch?v=4b5d3muPQmA

Bemærk, at der er noget tilfældighed indbygget i algoritmen. Det betyder, at hver gang man anvende k-means, få man en lidt anderledes resultat.

9.2.2 Within/between sum of squares

Man kan forestille sig, at hvis man lave en gode clustering af datasæt, så ligner observationerne indenfor den sammen cluster hinanden meget, og til gengæld er observationerne i forskellige clusters meget forskellige fra hinanden. Med andre ord, er afstanden mellem observationerne i samme cluster så mindre som muligt og afstanden mellem observationerne i forskellige clusters er så stor som muligt. For at måle det kan man beregne følgende:

  • total within sum of squares - den totale squared afstand af observationerne fra deres nærmeste centroid.
  • total between sum of squares - den totale afstand af centroids til samtlige andre centroids. Det skal være så stor som muligt.

9.2.3 Run k-means i R

K-means fungerer kun på numeriske data, som vi kan vælge fra datasættet med select() sammen med hjælper where(is.numeric). Vi bruger også scale(), som betyder, at alle variabler få den samme skala og det undgår, at der er nogle som få mere indflydelse end andre i de færdige resultater.

penguins_scaled <- penguins %>% 
  select(where(is.numeric)) %>% 
  scale()

Man er også nødt til at fortælle i forvejen hvor mange clusters at opdele datasættet i, så lad os sige centers=3 indenfor funktionen kmeans() her og beregner vores clusters:

kclust <- kmeans(penguins_scaled,centers = 3)
kclust
## K-means clustering with 3 clusters of sizes 129, 85, 119
## 
## Cluster means:
##   bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
## 1     -1.0452359     0.4858944        -0.8803701  -0.7616078
## 2      0.6710153     0.8040534        -0.2889118  -0.3835267
## 3      0.6537742    -1.1010497         1.1607163   1.0995561
## 
## Clustering vector:
##   [1] 1 1 1 1 1 1 1 1 1 1 1 1 2 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
##  [38] 1 2 1 1 1 1 2 1 1 1 2 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 2 1 1 1 2 1 2 1 1 1 2
##  [75] 1 2 1 1 1 1 1 1 1 1 1 2 1 1 1 2 1 1 1 2 1 2 1 1 1 1 1 1 1 2 1 2 1 2 1 2 1
## [112] 1 1 1 1 1 1 1 1 1 1 1 1 2 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 3 3
## [149] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
## [186] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
## [223] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
## [260] 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 1 2 2 2 2 2 2 2 1
## [297] 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2
## 
## Within cluster sum of squares by cluster:
## [1] 120.7030 109.4813 139.4684
##  (between_SS / total_SS =  72.2 %)
## 
## Available components:
## 
## [1] "cluster"      "centers"      "totss"        "withinss"     "tot.withinss"
## [6] "betweenss"    "size"         "iter"         "ifault"

Man få forskellige ting frem, for eksempel:

  • Cluster means - det svarer til de centroids markerede med x i den ovenstående figur - bemærk at her er de 4-dimensionelle da vi brugt 4 variabler til at beregne resultatet.
  • Clustering vector - hvilke cluster er hver observation blevet tilknyttet.
  • Within cluster sum of squares - Jo mindre, jo bedre - hvor meget observationerne indenfor samme cluster ligner hinanden (den totale squared afstand af observationerne fra deres nærmeste centroid).

9.2.4 Tidy up k-means resultaterne med pakken broom

Fra pakken broom har vi mest beskæftiget os med glance() indstil videre. Med glance() få man enkel-linje baserede summary statistikker fra én eller flere modeller sammen i én dataramme, for at facilitete et plot/labels osv. Der er også to andre funktioner vi tager i bruge her. Her er en beskrivelse af de tre.

Broom verb Beskrivelse
glance() single line summary - make elbow plot
augment() Append dataset to clusters - make plots coloured by cluster
tidy() Multi-line summary - extract centroids

For at lave et plot af de clusters kan det især være nyttigt at benytte augment. Her kan man se, at vi har fået en kolon der hedder .cluster med i den oprindelige dataramme (jeg flyttet kolonen til første plads i følgende kode så man kan se den i de output af kursusnotater).

kc1 <- augment(kclust, penguins) #clustering = første plads, data = anden plads
kc1 %>% select(.cluster,all_of(names(penguins)))
## # A tibble: 333 × 9
##    .cluster species island    bill_length_mm bill_depth_mm flipper_length_mm
##    <fct>    <fct>   <fct>              <dbl>         <dbl>             <int>
##  1 1        Adelie  Torgersen           39.1          18.7               181
##  2 1        Adelie  Torgersen           39.5          17.4               186
##  3 1        Adelie  Torgersen           40.3          18                 195
##  4 1        Adelie  Torgersen           36.7          19.3               193
##  5 1        Adelie  Torgersen           39.3          20.6               190
##  6 1        Adelie  Torgersen           38.9          17.8               181
##  7 1        Adelie  Torgersen           39.2          19.6               195
##  8 1        Adelie  Torgersen           41.1          17.6               182
##  9 1        Adelie  Torgersen           38.6          21.2               191
## 10 1        Adelie  Torgersen           34.6          21.1               198
## # … with 323 more rows, and 3 more variables: body_mass_g <int>, sex <fct>,
## #   year <fct>

Nu benytter vi kc1 til at lave et plot. Her giver jeg en farve efter .cluster og shape efter species så at vi kan sammenligne vores beregnet clusters med de tre forskellige arter. Bemæk her, at jeg kun har to variabler i plottet, men der er faktisk fire variabler som blev brugt til at lave de clusters i med funktionen kmeans. En anden måde er at plotte de først to principal components i stedet for to af de fire variabler - det beskæftige vi os med næste gang.

ggplot(kc1, aes(x = scale(bill_length_mm), 
                y = scale(bill_depth_mm))) + 
  geom_point(aes(color = .cluster, shape = species)) + theme_minimal()

Vi kan også fk. optælle hvor mange af de tre arter vi får i hver af vores tre clusters, hvor vi kan se, at Adelie og Chinstrap er blevet mere blandet blandt to af de tre clusters end Gentoo.

kc1 %>% count(.cluster, species)
## # A tibble: 5 × 3
##   .cluster species       n
##   <fct>    <fct>     <int>
## 1 1        Adelie      124
## 2 1        Chinstrap     5
## 3 2        Adelie       22
## 4 2        Chinstrap    63
## 5 3        Gentoo      119

9.2.5 Plot cluster centroids

Næste kigger vi på resultatet af funktionen tidy() fra broom-pakken. Her har vi fået en pæn dataramme med middelværdierne (centroids) af de tre clusters over de fire variabler som var brugt i beregningerne.

kclust_tidy <- kclust %>% tidy()
kclust_tidy
## # A tibble: 3 × 7
##   bill_length_mm bill_depth_mm flipper_length_mm body_mass_g  size withinss
##            <dbl>         <dbl>             <dbl>       <dbl> <int>    <dbl>
## 1         -1.05          0.486            -0.880      -0.762   129     121.
## 2          0.671         0.804            -0.289      -0.384    85     109.
## 3          0.654        -1.10              1.16        1.10    119     139.
## # … with 1 more variable: cluster <fct>

I følgende benytter jeg kclust_tidy som et ekstra datasæt i ovenstående plot, men indenfor en anden geom_point() for at tilføje en x form på midten af de tre clusters - se følgende tre punkter, der forklarer nogle detaljer i koden:

  • Jeg bruger funktionen scale()bill_length_mm og bill_depth_mm, fordi min centroids, som også skal med i plottet, var beregnet på skaleret data.
  • Jeg behøver ikke at anvende scale() på min centroids lagrede i kclust_tidy så jeg angiver bare akser-variablerne i aes() uden at anvende scale().
  • Jeg har brugt color og shape som lokale aethetics i den første geom_point() her, der de ikke eksistere som kolonner i kclust_tidy.
ggplot(kc1, aes(x = scale(bill_length_mm), #need to scale the original data
                y = scale(bill_depth_mm))) + 
  geom_point(aes(color = .cluster, shape = species)) +
  geom_point(data = kclust_tidy, 
             aes(x = bill_length_mm, #don't need to scale again
                 y = bill_depth_mm),
             size = 10, shape = "x", show.legend = FALSE) + 
  theme_bw()

Vi kan se at vores clusters ikke fanger de samme tre gruppe som variablen species præcist - der er forskelligheder. Det kan være at vi også har fanget nogle oplysninger om fk. øen pingviner bor på, eller deres køn.

9.3 Kmeans: hvor mange clusters?

Vi gættede på 3 clusters i ovenstående analyse (da vi havde oplysninger om arter i forvejen) men det godt kunne være, at et andet antal clusters passer bedre til datasættet. Vi kan beregene flere clusterings og angiver forskellige antal clusters, og dernæst bruge outputterne fra resultaterne til at tage en beslutning om, hvor mange clusters vi gerne vil angiv i vores færdig clustering.

Det er vigtigt at kunne finde frem til en hensigtsmæssigt antal clusters -

  • For mange clusters kan resultatere i over-fitting, hvor vi har for mange til at fortolke eller giver mening,
  • For få kan betyde at vi mangler indsigter ind i strukturen eller vigtige trends i datasættet.

9.3.1 Få Broom output for forskellige antal clusters

I følgende laver jeg en custom funktion, der laver en clustering på datasættet penguins_scaled og hvor jeg angiver, at antal beregnede clusters skal være .x, der er en integer (fk. 1,3,99 osv.). Bemærk derfor, at selve data er samme hver gang jeg anvender funktionen - det er bare antal clusters jeg beregner, der kan variere.

my_func <- ~kmeans(penguins_scaled,centers = .x)

Dernæst laver jeg en tibble med variablen k som indeholder heltal fra 1 op til 9. Når man anvender funktionen map på kolonnen k med ovenstående funktion my_func, svarer det til, at jeg anvender kmeans ni gange, med antal clusters fra 1 til 9. Jeg gemmer clustering resultaterne i en kolon der hedder kclust, og så anvende tidy, glance og augment til at få de forskellige outputter fra mine clusterings.

kclusts <- 
  tibble(k = 1:9) %>%
  mutate( kclust = map(k, my_func),
          tidied = map(kclust, tidy),
          glanced = map(kclust, glance),
          augmented = map(kclust, ~.x %>% augment(penguins))
        )

Husk at for at få frem resultaterne i de forskellige former fra tidy,glance og augment er vi nødt til at anvende funktionen unnest() - her gemme jeg resultaterne i tre nye dataframes, som vi kan referere til efterfølgende.

kclusts_tidy    <- kclusts %>% unnest(tidied)
kclusts_augment <- kclusts %>% unnest(augmented)
kclusts_glance <- kclusts %>% unnest(glanced)

9.3.2 Elbow plot (glance)

Vi bruger tot.withinness fra outputtet fra glance() (dataframen kclusts_glance). Det giver målinger for den totale afstand af observationerne fra deres nærmeste centroid (within sum of squares).

kclusts_glance
## # A tibble: 9 × 8
##       k kclust   tidied           totss tot.withinss betweenss  iter augmented
##   <int> <list>   <list>           <dbl>        <dbl>     <dbl> <int> <list>   
## 1     1 <kmeans> <tibble [1 × 7]>  1328        1328.  9.09e-13     1 <tibble> 
## 2     2 <kmeans> <tibble [2 × 7]>  1328         551.  7.77e+ 2     1 <tibble> 
## 3     3 <kmeans> <tibble [3 × 7]>  1328         370.  9.58e+ 2     3 <tibble> 
## 4     4 <kmeans> <tibble [4 × 7]>  1328         304.  1.02e+ 3     2 <tibble> 
## 5     5 <kmeans> <tibble [5 × 7]>  1328         228.  1.10e+ 3     3 <tibble> 
## 6     6 <kmeans> <tibble [6 × 7]>  1328         200.  1.13e+ 3     3 <tibble> 
## 7     7 <kmeans> <tibble [7 × 7]>  1328         206.  1.12e+ 3     4 <tibble> 
## 8     8 <kmeans> <tibble [8 × 7]>  1328         174.  1.15e+ 3     5 <tibble> 
## 9     9 <kmeans> <tibble [9 × 7]>  1328         232.  1.10e+ 3     4 <tibble>

Jo flere clusters, jo mindre statistikken tot.withinness er som regel, men vi kan se i følgende plot, at efter 2 eller 3 clusters, er der ikke meget gevinst ved at bruge flere clusters. Derfor vælger man enten 2 eller 3. Plottet er ofte kaldes for en ‘elbow’ plot - man vælger de tal på den ‘elbow’, hvor der ikke er meget gevinst med at have flere clusters i datasættet (men det er selvfølgelig meget subjektiv, det tal man vælger til sidste).

kclusts_glance %>% 
  ggplot(aes(x = k, y = tot.withinss)) + 
  geom_line() + 
  geom_point() + 
  theme_bw()

9.3.3 Automatistke beslutning med pakken NbClust

Man kan man også overvejer at prøve noget mere automatisk. For eksempel pakken NbClust lave 30 forskellige clustering algoritme på datasættet fra antal clusters = 2 op til til antal cluster = 9 og for hver af de 30 tager en beslutning om de bedste antal clusters. Man kan således se hvilket antal clusters blev valgt fleste gange af de forskellige algoritme.

library(NbClust)
set.seed(24) #fordi outputt af NbClust har indbygget tilfældighed
cluster_30_indexes <- NbClust(data = penguins_scaled, 
                              distance = "euclidean", 
                              min.nc = 2, 
                              max.nc = 9, 
                              method = "complete")

Man kan se i følgende, at enten 2 eller 3 er optimelt, som passer sammen med den elbow plot methode.

as_tibble(cluster_30_indexes$Best.nc[1,]) %>%
  ggplot(aes(x=factor(value))) + 
  geom_bar(stat="count",fill="blue") + 
  xlab("Number of clusters") + ylab("Number of clustering algorithms choosing this number") +
  coord_flip() +
  theme_minimal()

9.3.4 Plot de forskellige antal clusters (augment)

Vi kan også visualisere hvordan de forskellige antal clusters ser ud. Her kan vi bruge vores resultater fra funktionen augment (kclusts_augment), som indeholder tilknytninger af observationerne til clusters for hver af de 9 clusterings. Læg mærk til at kclusts_agument har 2997 observationer, der svarer til 9 (antal clusterings) x 333 (antal observationer i penguins), fordi vi brugt unnest til at lægge samtlige resultaterne sammen.

kclusts_augment %>% glimpse()
## Rows: 2,997
## Columns: 13
## $ k                 <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ kclust            <list> [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
## $ tidied            <list> [<tbl_df[1 x 7]>], [<tbl_df[1 x 7]>], [<tbl_df[1 x …
## $ glanced           <list> [<tbl_df[1 x 4]>], [<tbl_df[1 x 4]>], [<tbl_df[1 x …
## $ species           <fct> Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adel…
## $ island            <fct> Torgersen, Torgersen, Torgersen, Torgersen, Torgerse…
## $ bill_length_mm    <dbl> 39.1, 39.5, 40.3, 36.7, 39.3, 38.9, 39.2, 41.1, 38.6…
## $ bill_depth_mm     <dbl> 18.7, 17.4, 18.0, 19.3, 20.6, 17.8, 19.6, 17.6, 21.2…
## $ flipper_length_mm <int> 181, 186, 195, 193, 190, 181, 195, 182, 191, 198, 18…
## $ body_mass_g       <int> 3750, 3800, 3250, 3450, 3650, 3625, 4675, 3200, 3800…
## $ sex               <fct> male, female, female, female, male, female, male, fe…
## $ year              <fct> 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007…
## $ .cluster          <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…

I følgende kode plotter jeg flipper_length_mm vs bill_length_mm, og anvende facet_wrap så at hver clustering får sit eget plot (så der er 333 observationer pr. plot).

kclusts_augment %>% 
  ggplot(aes(x = flipper_length_mm, y = bill_length_mm,colour=.cluster)) +
        geom_point(aes(shape=factor(species)), alpha = 0.8) + 
        facet_wrap(~ k) + 
        theme_bw() 

Vi kan nemt inddrage kclusts_tidy() og lave “X”-former, bare ved at tilføje en ekstra geom_point og angive kclusts_tidy. Jeg anvender først funktionen rename så at variablen cluster fra klusts_tidy matcher til .cluster fra kclusts_augment.

kclusts_tidy <- kclusts_tidy %>% rename(.cluster=cluster)

kclusts_augment %>% 
  ggplot(aes(x = scale(flipper_length_mm), y = scale(bill_length_mm),colour=.cluster)) + #scale here
        geom_point(aes(shape=factor(species)), alpha = 0.8) + 
        facet_wrap(~ k) + 
        geom_point(data = kclusts_tidy,
                   aes(x=flipper_length_mm,y=bill_length_mm), #already based on scaled data, so don't scale
                   size = 10, shape = "x",col="black", show.legend = FALSE) + 
        theme_bw()

Vi kan prøve at kigge endnu dyber ind i resultaterne - her introducerer jeg sex som en ekstra variabel i plottet. Husk at variablen sex var ikke blevet brugt i vores k-means clusterings, men det kan være, at der er nogle aspekter af de fire variabler, som kan fortælle os nogle om kønnet af pingvingerne. For at spare plads, har jeg kun plottet antal clusters fra 2 til 5.

kclusts_augment %>% filter(k %in% 2:5) %>% 
  ggplot(aes(x = scale(flipper_length_mm), y = scale(bill_length_mm),colour=.cluster)) +
        geom_point(aes(shape=factor(species)), alpha = 0.8) + 
        facet_grid(sex ~ k) + 
        geom_point(data = kclusts_tidy %>% filter(k %in% 2:5),
                   aes(x = flipper_length_mm,
                       y = bill_length_mm), 
                   size = 10, shape = "x", colour = "black",show.legend = FALSE) +
        theme_bw()

9.3.5 Nest/map ramme fra sidste gange

Som sidste bemærk med k-means, kan man også lave en clustering til de tre arter hver for sig. I følgende opretter jeg en nested dataframe, som indeholder 3 datasæt (penguins opdelt efter variablen species), og jeg anvender den custom funktion scale_me til at udvælge de numeriske variabler og anvende scale() i hvert datasæt.

scale_me <- ~.x %>% select(where(is.numeric)) %>% scale

penguins_nest <- penguins %>% 
  group_by(species) %>%
  nest() %>%
  mutate("data_scaled" = map(data,scale_me))

Næste laver jeg en custom funktion til at lave en clustering på datasættet .x, og angiver at antal clusters skal være 3. Bemærk at i ovenstående sektion varierede vi på antal clusters (indstilling centers), men her fastlægger vi antal clusters og så variere selve datasæt i stedet for.

cluster_me <- ~.x %>% kmeans(centers=3)

Jeg anvender cluster_me på mine skaleret datasæts, og så anvender glance, augment og tidy på clustering resultater ligesom i ovenpå (bemærk brugen af map til at augment de opdelte datasæt).

penguins_nest <- penguins_nest %>% 
  mutate(clusters = map(data_scaled,cluster_me),
         clusters_glance = map(clusters,glance),
         clusters_augment = map2(clusters,data_scaled,~.x %>% augment(.y)), #I augment the scaled data so the correct scaling (based on individual datasets) appears in the next plot
         clusters_tidy = map(clusters,tidy))

nested_clusters_augment <- penguins_nest %>% unnest(clusters_augment)
nested_clusters_tidy <- penguins_nest %>% unnest(clusters_tidy)

Til sidste laver jeg en plot af resultaterne:

nested_clusters_augment %>% 
  ggplot(aes(x=bill_length_mm,y=flipper_length_mm,colour=.cluster)) + #data already scaled
  geom_point() +
  facet_grid(~species) + 
  geom_point(data=nested_clusters_tidy,,
             shape="X",colour="black",
             size = 10) +
  theme_bw()

9.4 Method 2: Hierarchical clustering

K-means er en meget populær metode til at lave clustering, men der er mange andre metoder, fk. hierarchical clustering. Vi skifter over til mtcars, og ligesom i kmeans skal vi første bruge scale på de numeriske kolonner i de data.

mtcars_scaled <- mtcars %>% select(where(is.numeric)) %>% scale()

I modsætning til k-means, for at lave hierarchical clustering skal man første beregne afstanden mellem alle de observationer i de data. Det gør man med funktionen dist() (som bruger den Euclidean distance som default):

d <- dist(mtcars_scaled)

For at lave en hierarchical clustering anvender man funktionen hclust(). Metoden complete er default men man kan afprøve de andre methoder (der er ikke en fast regel over for, hvilken metode man skal bruge).

mtcars_hc <- hclust(d, method = "complete" )
# Metoder: "average", "single", "complete", "ward.D"

I følgende arbejder vi lidt med mtcars_hc til at få nogle clusters frem, og til at lave et plot.

9.4.1 Vælge ønkset antal clusters

Funktionen cutree anvendes til at få clusters fra resultaterne af funktionen hclust. For eksempel hvis man gerne vil have 4 clusters, bruger man k = 4. Jeg specificerer order_clusters_as_data = FALSE for at få clusters i rækkefølgen, som passer til plottet (dendrogram) vi laver (bemærk at man skal have pakken dendextend installeret for at få den til at fungere).

library(dendextend)

clusters <- cutree(mtcars_hc, k = 4, order_clusters_as_data = FALSE)

Her laver jeg et overblik over, hvor mange observationerne fra mtcars er i hver cluster:

tibble("cluster"=clusters) %>% group_by(cluster) %>% summarise(n())
FALSE # A tibble: 4 × 2
FALSE   cluster `n()`
FALSE     <int> <int>
FALSE 1       1     7
FALSE 2       2     8
FALSE 3       3    12
FALSE 4       4     5

9.4.2 Lav et pænt plot af dendrogram med ggplot2

Første anvender jeg funktionen dendro_data() til at udtrække den “dendrogram” fra de hclust() resultater.

library(ggdendro)
dend_data <- dendro_data(mtcars_hc %>% as.dendrogram, type = "rectangle")

Vi tilføjer vores clusters som vi beregnede ovenpå (det er derfor vi sikret rækkefølgen af de clusters):

dend_data$labels <- dend_data$labels %>% 
  mutate(cluster = clusters)

Vi benytter dend_data$segments og dend_data$labels til at lave et informativ plot af de data i ggplot2.

ggplot(dend_data$segments) + 
  geom_segment(aes(x = x, y = y, xend = xend, yend = yend)) +
  coord_flip() +
  geom_text(data = dend_data$labels, 
            aes(x, y, label = label,col=factor(cluster)),
            hjust=1,size=3) +
  ylim(-3, 10) + 
  theme_dendro()

Så kan man se, der er fire clusters i dengrammet, og biler der er tætest på hinanden ligner hinanden mest - fk. Merc 280C og Merc 280 må være meget éns, og er som forventet lige ved siden af hinanden i plottet.

Man kan godt tilpasse ovenstående kode til et andet datasæt - se problemstillinger, men man må også gerne udvide plottet med de forskellige viden vi har om ggplot2.

9.4.3 Ekstra (valgfri): afprøve andre metoder på hierachical clustering

Valfri ekstra hvis du vil afprøve de fire metoder i hclust - “average”, “single”, “complete” og “ward.D”.

# samme ggplot kommando som ovenpå lavet til en funktion 
den_plot <- ~ggplot(.x$segments) + 
  geom_segment(aes(x = x, y = y, xend = xend, yend = yend)) +
  coord_flip() +
  geom_text(data = .x$labels, 
            aes(x, y, label = label),
            hjust=1,size=2) +
  ylim(-4, 10) + theme_dendro()

Vi iterate over de fire metoder og lave samme process som ovenpå med map. Derefter kan man lave et plot fk. med grid.arrange:

# fire metoder:
m <- c( "average", "single", "complete", "ward.D")

hc_results <- 
  tibble(method = m) %>%
  mutate( kclust = map(method, ~hclust(d, method = .x)), 
          dendrogram = map(kclust,as.dendrogram),
          den_dat = map(dendrogram,~dendro_data(.x,type="rectangle")),
          plot = map(den_dat,den_plot))

library(gridExtra)
grid.arrange(grobs = hc_results %>% pull(plot),ncol=2)

9.5 Problemstillinger

Problem 1) Quiz - Clustering


Problem 2) Funktionen kmeans. I ovenstående anvendt vi mtcars i hierarchical clustering, men lad os se, hvordan det ser ud med k-means. Du er velkommen til at tilpasse min ovenstående kode fa det penguins datasæt:

a) Benyt kmeans til at finde 2 clusters i datasættet mtcars:

  • husk at vælge kun de numeriske kolonner og scale datasættet i forvejen
  • gem din clustering som my_clusters.
  • hvor mange observationer er der i hver af de to clusters?

b) Anvend funktionen augment til at forbinde det oprindelige datasæt til dine clusters fra my_clusters (skriv mtcars indenfor funktionen augment).

c) Brug dit “augmented” datasæt til at lave et scatter plot mellem to af de numeriske variabler (vælg selv) i datasættet og giv dem farver efter din clusters, som du har beregnet. Da du har forbundet det oprindeligt datasæt (der ikke var scaled) i augment, scale din variabler i plottet.

d) Tilføj tidy til at få fat i de middelværdier/centroids af hver af de 2 clusters og tilpas min kode fra notaterne (sektion 9.2.5) til at tilføje dem til plottet som ‘x’ (husk at din “centers”/centroids er allerede baserede på scaled data så du behøver ikke at anvende scale på deres værdier).


Problem 3) Hierarchical clustering øvelse

Vi laver en analyse af det msleep datasæt. Jeg har lavet oprydningen og scaling for jer:

data(msleep)
msleep_clean <- msleep %>% select(name,where(is.numeric)) %>% drop_na()
msleep_scaled <- msleep_clean %>% select(-name) %>% scale
row.names(msleep_scaled) <- msleep_clean$name

Tilpas min kode fra kursusnotaterne (sektion 9.4) til at lave følgende:

a) Benyt funktioner dist og dernæst hclust på datasættet msleep_scaled.

b) Benyt cutree for at finde 5 clusters fra dine hclust-resultater, og kalde det for clusters. Husk at anvende order_clusters_as_data = FALSE så at vi har den korrekt rækkefølge for et plot (OBS man skal installere/indlæse pakken dendextend)

c) Benyt dendro_data til at udtrække de dendrogram fra resultaterne og tilføj clusters til dend_data$labels (kopier kode fra 9.4.2).

d) Lav et dengrogram plot: igen tilpas koden (9.4.2) for mtcars eksempel for nuværende data


Problem 4)

Inlæs data

wholesale <- read.csv("https://www.dropbox.com/s/7nb5pkruqt4fqn4/Wholesale%20customers%20data.csv?dl=1", header = TRUE)

a) Ændre på datasættet efter følgende instruks:

  • Channel - anvend recode for at ændre til navne
    • 1 = horeca
    • 2 = retail
  • Region - anvend recode for at ændre til navne
    • 1 = Lisnon
    • 2 = Oporto
    • 3 = Other
  • Anvend map_if til at transformere samtlige numeriske variabler med log (kode er: wholesale <- wholesale %>% map_if(is.numeric,log) %>% as_tibble()).

b) Udvælg de numeriske variabler fra dit datasæt og anvende scale() - kalde dit nye datasæt for wholescale_scale

c) Tilpas min kode fra sektion 9.3.1 til at lave 10 clusterings (k=1:10) på wholesale_scale og gem dem i en dataframe, sammen med din clusterings resultater i “tidy”, “glance” og “augment” form.

d) Lav et elbow plot fra dit output fra glance (sektion 9.3.2)

e) Udvælg clusterings hvor k er fra 2 til 7 fra dit output fra augment og lav scatter plots af variabler Frozen VS Fresh, hvor du:

  • Giv farve efter .cluster
  • Adskil plots efter k
  • Prøv bagefter at også adskil dit plots yderligere efter Channel.

f) Tilpas koden fra 9.3.5 til at lave en analyse for “hoerca” og “retail” (variablen Channel) hver for sig. Angiv 4 clusters i din analyse.

g) Lav et plot af din clustering (adskilt efter variablen Channel) og få “x” på plotterne til at vise din cluster middelværdier for Frozen og Fresh.