Search code examples
rdplyrlevels

Reduce number of levels for each factor dplyr approach


I am trying to reduce the number of levels in each factor variable in my data. I want to reduce the number of levels doing 2 operations:

  1. If the number of levels is larger than a cut-off then replace the less frequent levels to a new level until the number of levels has reached the cut-off
  2. Replace levels in a factor with not enough observations to a new level

I wrote a function which works fine, but I don't like the code. It does not matter if the level REMAIN has not enough observations. I prefer a dplyr approach.

ReplaceFactor <- function(data, max_levels, min_values_factor){
    # First make sure that not to many levels are in a factor
    for(i in colnames(data)){
        if(class(data[[i]]) ==  "factor"){
            if(length(levels(data[[i]])) > max_levels){
                levels_keep <- names(sort(table(data[[i]]), decreasing = T))[1 : (max_levels - 1)]
                data[!get(i) %in% levels_keep, (i) := "REMAIN"]
                data[[i]] <- as.factor(as.character(data[[i]]))
            }
        } 
    }
    # Now make sure that in each level has enough observations
    for(i in colnames(data)){
        if(class(data[[i]]) ==  "factor"){
            if(min(table(data[[i]])) < min_values_factor){
                levels_replace <- table(data[[i]])[table(data[[i]]) < min_values_factor]
                data[get(i) %in% names(levels_replace), (i) := "REMAIN"]
                data[[i]] <- as.factor(as.character(data[[i]]))
            }
        } 
    }
    return(data)
}
df <- data.frame(A = c("A","A","B","B","C","C","C","C","C"), 
                 B = 1:9, 
                 C = c("A","A","B","B","C","C","C","D","D"), 
                 D = c("A","B","E", "E", "E","E","E", "E", "E"))
str(df)
'data.frame':   9 obs. of  4 variables:
 $ A: Factor w/ 3 levels "A","B","C": 1 1 2 2 3 3 3 3 3
 $ B: int  1 2 3 4 5 6 7 8 9
 $ C: Factor w/ 4 levels "A","B","C","D": 1 1 2 2 3 3 3 4 4
 $ D: Factor w/ 3 levels "A","B","E": 1 2 3 3 3 3 3 3 3

dt2 <- ReplaceFactor(data = data.table(df),
              max_levels = 3,
              min_values_factor = 2)
str(dt2)
Classes ‘data.table’ and 'data.frame':  9 obs. of  4 variables:
 $ A: Factor w/ 3 levels "A","B","C": 1 1 2 2 3 3 3 3 3
 $ B: int  1 2 3 4 5 6 7 8 9
 $ C: Factor w/ 3 levels "A","C","REMAIN": 1 1 3 3 2 2 2 3 3
 $ D: Factor w/ 2 levels "E","REMAIN": 2 2 1 1 1 1 1 1 1
 - attr(*, ".internal.selfref")=<externalptr>
 dt2
   A B      C      D
1: A 1      A REMAIN
2: A 2      A REMAIN
3: B 3 REMAIN      E
4: B 4 REMAIN      E
5: C 5      C      E
6: C 6      C      E
7: C 7      C      E
8: C 8 REMAIN      E
9: C 9 REMAIN      E

Solution

  • Using forcats:

    library(dplyr)
    library(forcats)
    
    max_levels <- 3
    min_values_factor <- 2
    df %>% 
      mutate_if(is.factor, fct_lump, n = max_levels, 
                other_level = "REMAIN", ties.method = "first") %>% 
      mutate_if(is.factor, fct_lump, prop = (min_values_factor - 1) / nrow(.), 
                other_level = "REMAIN")
    
    #   A B      C      D
    # 1 A 1      A REMAIN
    # 2 A 2      A REMAIN
    # 3 B 3      B      E
    # 4 B 4      B      E
    # 5 C 5      C      E
    # 6 C 6      C      E
    # 7 C 7      C      E
    # 8 C 8 REMAIN      E
    # 9 C 9 REMAIN      E
    

    (Oh, and I wasn't able to replicate the exact behavior of your function, but you might get what you want by tweaking ties.method and substracting 1 to max_levels).