Set Up

library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.2.3
## Warning: package 'ggplot2' was built under R version 4.2.3
## Warning: package 'tibble' was built under R version 4.2.3
## Warning: package 'tidyr' was built under R version 4.2.3
## Warning: package 'readr' was built under R version 4.2.3
## Warning: package 'purrr' was built under R version 4.2.3
## Warning: package 'dplyr' was built under R version 4.2.3
## Warning: package 'stringr' was built under R version 4.2.3
## Warning: package 'forcats' was built under R version 4.2.3
## Warning: package 'lubridate' was built under R version 4.2.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.4.4     ✔ tibble    3.2.1
## ✔ lubridate 1.9.2     ✔ tidyr     1.3.1
## ✔ 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(broom) # tidy model results
## Warning: package 'broom' was built under R version 4.2.3
library(umap) # dimension reduction
## Warning: package 'umap' was built under R version 4.2.3
library(plotly) # interactive visualization
## Warning: package 'plotly' was built under R version 4.2.3
## 
## Attaching package: 'plotly'
## 
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## 
## The following object is masked from 'package:stats':
## 
##     filter
## 
## The following object is masked from 'package:graphics':
## 
##     layout
library(reticulate)
## Warning: package 'reticulate' was built under R version 4.2.3
employed <- read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-02-23/employed.csv")
## Rows: 8184 Columns: 7
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): industry, major_occupation, minor_occupation, race_gender
## dbl (3): industry_total, employ_n, year
## 
## ℹ 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.

1 Convert data to standardized form

employed_group <- employed %>%
  
  filter(!is.na(employ_n)) %>%
  group_by(occupation = paste(industry, minor_occupation), race_gender) %>%
  summarise(n = sum(employ_n)) %>%
  ungroup()
## `summarise()` has grouped output by 'occupation'. You can override using the
## `.groups` argument.
employed_tidy <- employed_group %>%

  # Remove total category
  filter(race_gender != "TOTAL") %>%
  
  # Add total column
  left_join(employed_group %>%
              filter(race_gender == "TOTAL") %>%
              select(occupation, total = n)) %>%
  
    # Get pct in total
    mutate(pct = n / total) %>%
  
    # Remove Outliers
    filter(total > 1000) %>%
    mutate(total = total %>% log() %>% scale() %>% as.numeric()) %>%
    select(-n)
## Joining with `by = join_by(occupation)`
employed_tidy
## # A tibble: 1,160 × 4
##    occupation                                          race_gender  total    pct
##    <chr>                                               <chr>        <dbl>  <dbl>
##  1 Agriculture and related Construction and extractio… Asian       -1.30  0.0274
##  2 Agriculture and related Construction and extractio… Black or A… -1.30  0.0822
##  3 Agriculture and related Construction and extractio… Men         -1.30  0.973 
##  4 Agriculture and related Construction and extractio… White       -1.30  0.863 
##  5 Agriculture and related Construction and extractio… Women       -1.30  0.0274
##  6 Agriculture and related Farming, fishing, and fore… Asian        0.819 0.0139
##  7 Agriculture and related Farming, fishing, and fore… Black or A…  0.819 0.0342
##  8 Agriculture and related Farming, fishing, and fore… Men          0.819 0.789 
##  9 Agriculture and related Farming, fishing, and fore… White        0.819 0.911 
## 10 Agriculture and related Farming, fishing, and fore… Women        0.819 0.211 
## # ℹ 1,150 more rows
employed_standard <- employed_tidy %>%  
    
    # Standardize
    group_by(race_gender) %>% 
    mutate(pct = pct %>% scale() %>% as.numeric()) %>% 
    ungroup() %>% 
    mutate(total = total %>% log() %>% scale() %>% as.numeric()) 
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `total = total %>% log() %>% scale() %>% as.numeric()`.
## Caused by warning in `log()`:
## ! NaNs produced
employed_standard
## # A tibble: 1,160 × 4
##    occupation                                         race_gender   total    pct
##    <chr>                                              <chr>         <dbl>  <dbl>
##  1 Agriculture and related Construction and extracti… Asian       NaN     -0.539
##  2 Agriculture and related Construction and extracti… Black or A… NaN     -0.405
##  3 Agriculture and related Construction and extracti… Men         NaN      1.31 
##  4 Agriculture and related Construction and extracti… White       NaN      0.725
##  5 Agriculture and related Construction and extracti… Women       NaN     -1.30 
##  6 Agriculture and related Farming, fishing, and for… Asian         0.401 -0.928
##  7 Agriculture and related Farming, fishing, and for… Black or A…   0.401 -1.21 
##  8 Agriculture and related Farming, fishing, and for… Men           0.401  0.510
##  9 Agriculture and related Farming, fishing, and for… White         0.401  1.38 
## 10 Agriculture and related Farming, fishing, and for… Women         0.401 -0.503
## # ℹ 1,150 more rows

2 Spread to object-characteristics format

occupation_demo_tbl <- employed_tidy %>% 
    pivot_wider(names_from = race_gender, values_from = pct) %>% 
    janitor::clean_names()

occupation_demo_tbl
## # A tibble: 232 × 7
##    occupation            total   asian black_or_african_ame…¹   men white  women
##    <chr>                 <dbl>   <dbl>                  <dbl> <dbl> <dbl>  <dbl>
##  1 Agriculture and re… -1.30   0.0274                 0.0822  0.973 0.863 0.0274
##  2 Agriculture and re…  0.819  0.0139                 0.0342  0.789 0.911 0.211 
##  3 Agriculture and re… -0.827  0.0155                 0.0309  0.985 0.918 0.0103
##  4 Agriculture and re… -0.0262 0.00992                0.00794 0.739 0.967 0.261 
##  5 Agriculture and re…  0.773  0.00997                0.00882 0.741 0.962 0.259 
##  6 Agriculture and re… -0.353  0.0233                 0.0155  0.159 0.938 0.841 
##  7 Agriculture and re… -0.786  0.0332                 0.104   0.815 0.820 0.185 
##  8 Agriculture and re… -0.623  0.0339                 0.0373  0.675 0.902 0.329 
##  9 Agriculture and re… -1.21   0                      0.0682  0.864 0.875 0.136 
## 10 Agriculture and re… -1.18   0                      0.0213  0.585 0.968 0.426 
## # ℹ 222 more rows
## # ℹ abbreviated name: ¹​black_or_african_american

3 Perform k-means clustering

occupation_cluster <- kmeans(occupation_demo_tbl %>% select(-occupation), centers = 3, nstart = 20)
summary(occupation_cluster)
##              Length Class  Mode   
## cluster      232    -none- numeric
## centers       18    -none- numeric
## totss          1    -none- numeric
## withinss       3    -none- numeric
## tot.withinss   1    -none- numeric
## betweenss      1    -none- numeric
## size           3    -none- numeric
## iter           1    -none- numeric
## ifault         1    -none- numeric
tidy(occupation_cluster)
## # A tibble: 3 × 9
##    total  asian black_or_african_amer…¹   men white women  size withinss cluster
##    <dbl>  <dbl>                   <dbl> <dbl> <dbl> <dbl> <int>    <dbl> <fct>  
## 1  0.932 0.0609                  0.114  0.570 0.792 0.430    94     26.9 1      
## 2 -0.219 0.0405                  0.109  0.745 0.814 0.255    96     21.0 2      
## 3 -1.59  0.0258                  0.0833 0.731 0.842 0.258    42     15.4 3      
## # ℹ abbreviated name: ¹​black_or_african_american
glance(occupation_cluster)
## # A tibble: 1 × 4
##   totss tot.withinss betweenss  iter
##   <dbl>        <dbl>     <dbl> <int>
## 1  259.         63.3      195.     3
augment(occupation_cluster, occupation_demo_tbl) %>% 
    
    ggplot(aes(total, asian, color = .cluster)) + 
    geom_point()

4 Select optimal number of clusters

kclusts <- tibble(k = 1:9) %>% 
    mutate(kclust = map(.x = k, .f = ~ kmeans(occupation_demo_tbl %>% select(-occupation), centers = .x, nstart = 20)), 
           glanced = map(.x = kclust, .f = glance))

kclusts %>% 
    unnest(glanced) %>% 
    ggplot(aes(k, tot.withinss)) + 
    geom_point() + 
    geom_line()

final_cluster <- occupation_cluster <- kmeans(occupation_demo_tbl %>% select(-occupation), centers = 5, nstart = 20)
augment(final_cluster, occupation_demo_tbl) %>% 
    
    ggplot(aes(total, asian, color = .cluster)) + 
    geom_point()

Reduce dimension using UMAP

umap_results <- occupation_demo_tbl %>% 
    select(-occupation) %>% 
    umap()

umap_results_tbl <- umap_results$layout %>% 
    as_tibble() %>% 
    bind_cols(occupation_demo_tbl %>% select(occupation))
## Warning: The `x` argument of `as_tibble.matrix()` must have unique column names if
## `.name_repair` is omitted as of tibble 2.0.0.
## ℹ Using compatibility `.name_repair`.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
umap_results_tbl
## # A tibble: 232 × 3
##        V1    V2 occupation                                                      
##     <dbl> <dbl> <chr>                                                           
##  1 -3.99  -5.09 Agriculture and related Construction and extraction occupations 
##  2  1.59   4.11 Agriculture and related Farming, fishing, and forestry occupati…
##  3 -3.09  -3.81 Agriculture and related Installation, maintenance, and repair o…
##  4  0.256 -1.39 Agriculture and related Manage-ment, business, and financial op…
##  5  1.99   3.89 Agriculture and related Management, business, and financial ope…
##  6  0.669 -2.53 Agriculture and related Office and administrative support occup…
##  7 -1.99  -4.03 Agriculture and related Production occupations                  
##  8 -1.44  -3.83 Agriculture and related Professional and related occupations    
##  9 -3.89  -5.15 Agriculture and related Protective service occupations          
## 10 -3.43  -5.65 Agriculture and related Sales and related occupations           
## # ℹ 222 more rows
umap_results_tbl %>% 
    ggplot(aes(V1, V2, text = occupation)) + 
    geom_point()

Visualize clusters by adding k-means results

kmeans_umap_tbl <- final_cluster %>% 
    augment(occupation_demo_tbl) %>% 
    select(occupation, .cluster) %>% 
    
    # Add umap results
    left_join(umap_results_tbl) %>% 
    
    # Add employment info
    left_join(employed_tidy %>% 
                  select(-total) %>% 
                  pivot_wider(names_from = race_gender, values_from = pct) %>% 
    janitor::clean_names())
## Joining with `by = join_by(occupation)`
## Joining with `by = join_by(occupation)`
kmeans_umap_tbl
## # A tibble: 232 × 9
##    occupation   .cluster     V1    V2   asian black_or_african_ame…¹   men white
##    <chr>        <fct>     <dbl> <dbl>   <dbl>                  <dbl> <dbl> <dbl>
##  1 Agriculture… 5        -3.99  -5.09 0.0274                 0.0822  0.973 0.863
##  2 Agriculture… 3         1.59   4.11 0.0139                 0.0342  0.789 0.911
##  3 Agriculture… 5        -3.09  -3.81 0.0155                 0.0309  0.985 0.918
##  4 Agriculture… 4         0.256 -1.39 0.00992                0.00794 0.739 0.967
##  5 Agriculture… 3         1.99   3.89 0.00997                0.00882 0.741 0.962
##  6 Agriculture… 4         0.669 -2.53 0.0233                 0.0155  0.159 0.938
##  7 Agriculture… 5        -1.99  -4.03 0.0332                 0.104   0.815 0.820
##  8 Agriculture… 5        -1.44  -3.83 0.0339                 0.0373  0.675 0.902
##  9 Agriculture… 5        -3.89  -5.15 0                      0.0682  0.864 0.875
## 10 Agriculture… 5        -3.43  -5.65 0                      0.0213  0.585 0.968
## # ℹ 222 more rows
## # ℹ abbreviated name: ¹​black_or_african_american
## # ℹ 1 more variable: women <dbl>
g <- kmeans_umap_tbl %>% 
    
    # Create text label
    mutate(text_label = str_glue("Occupation: {occupation} 
                                 Cluster: {.cluster}
                                 Asian: {asian %>% scales::percent(1)}
                                 Women: {women %>% scales::percent(1)}")) %>% 
    
    # Plot
    ggplot(aes(V1, V2, color= .cluster, text = text_label)) + 
    geom_point()

g %>% ggplotly(tooltip = "text")