rvrvrv
rvrvrv

Reputation: 911

How to dynamically change data type of columns in data frame

The platform from which I'm importing data to R does not support specifying the data type, hence all my columns are character. I have an Excel file that specifies which columns are factor, including the relevant labels and levels. Now, I'm trying to write a function to dynamically change the data type of various columns of my data.frame

Thanks to the excellent answer to this question (dplyr - mutate: use dynamic variable names), I managed to write the following function, in which I dynamically set the column name to the mutate function.

readFactorData <- function(filepath) {
    t <- read.xlsx(filepath)
    sapply(nrow(t), function(i) {
      colname <- as.character(t[i, "Item"])
      factorLevels <- t[i, 3:ncol(t)][which(!is.na(t[i, 3:ncol(t)]))]
      totalLevels <- length(factorLevels)
      listOfLabels <- as.character(unlist(factorLevels))

      mutate(d, !!colname := factor(d[[colname]], labels=(1:totalLevels), levels=listOfLabels))
        # requires dplyr v.0.7+
        # the syntax `!!variablename:=` forces evaluation of the variablename before evaluating the rest of the function
    })
}

It works, and each iteration returns the entire data frame, with the relevant column (colname) changed to factor. But, each iteration overwrites the previous, so this function only returns the last result of i. How do I make sure that I end up with 1 single data frame, in which all the relevant columns are saved?

Sample data (make sure to comment out the first line of the function above, since we're defining t here):

 d <- data.frame("id" = sample(100:999, 10), "age" = sample(18:80, 10), "factor1" = c(rep("a", 3), rep("b", 3), rep("c", 4)), "factor2" = c("x","y","y","y","y","x","x","x","x","y"), stringsAsFactors = FALSE)
 t <- data.frame("Item" = c("factor1","factor2"), "Label" = c("This is factor 1", "This is factor 2"), "level1" = c("a","x"), "level2" = c("b","y"), "level3" = c("c","NA"))

Upvotes: 1

Views: 2420

Answers (3)

Bryan Shalloway
Bryan Shalloway

Reputation: 888

The function below maps through readr::parse_* functions specified for each named column you want to change and then allows you to specify args for each (e.g. levels if using parse_factor).

library(tidyverse)

parse_cols <- function(df, f, col_names, levels, ...){
  # df: dataframe, f: char vec, col_names: char vec, levels: list of char vecs,
  # ...: list of other potential args for parse_*
  params_t <- tibble(x = map(col_names, ~df[[.x]]), levels = levels, ...) %>% transpose()

  new_cols <- map2_df(.x = structure(f, names = col_names), 
                      .y = params_t, 
                      ~R.utils::doCall(.x, args = .y, .ignoreUnusedArgs = TRUE))  

  df[names(new_cols)] <- new_cols
  df
}

# function inputs -- perhaps just requiring a tibble input would be safer
parsings_vec <- c("parse_factor","parse_double", "parse_factor")
cols_vec <- c("manufacturer", "cty", "class")
factors_list <- list(unique(mpg[["manufacturer"]]), NULL, unique(mpg[["class"]]))

parse_cols(df = mpg, f = parsings_vec, col_names = cols_vec, levels = factors_list) 
#> # A tibble: 234 x 11
#>    manufacturer model displ  year   cyl trans drv     cty   hwy fl    cla~
#>    <fct>        <chr> <dbl> <int> <int> <chr> <chr> <dbl> <int> <chr> <fc>
#>  1 audi         a4      1.8  1999     4 auto~ f        18    29 p     com~
#>  2 audi         a4      1.8  1999     4 manu~ f        21    29 p     com~
#>  3 audi         a4      2    2008     4 manu~ f        20    31 p     com~
#>  4 audi         a4      2    2008     4 auto~ f        21    30 p     com~
#>  5 audi         a4      2.8  1999     6 auto~ f        16    26 p     com~
#>  6 audi         a4      2.8  1999     6 manu~ f        18    26 p     com~
#>  7 audi         a4      3.1  2008     6 auto~ f        18    27 p     com~
#>  8 audi         a4 q~   1.8  1999     4 manu~ 4        18    26 p     com~
#>  9 audi         a4 q~   1.8  1999     4 auto~ 4        16    25 p     com~
#> 10 audi         a4 q~   2    2008     4 manu~ 4        20    28 p     com~
#> # ... with 224 more rows

Upvotes: 0

patL
patL

Reputation: 2299

If you want to convert all your factor to character within your data.frame you can use dplyr's mutate_if. Otherwise, if you want to use a vector of column names, @Eden Z's answer will do it for you.

library(tidyverse)

d_out <- d %>% 
  mutate_if(is.character, as.factor)

d_out

#    id age factor1 factor2
#1  933  61       a       x
#2  208  52       a       y
#3  193  25       a       y
#4  231  47       b       y
#5  595  78       b       y
#6  675  28       b       x
#7  387  71       c       x
#8  386  80       c       x
#9  893  20       c       x
#10 272  23       c       y

When you can check the class for your variables doing:

sapply(d_out, class)

#       id       age   factor1   factor2 
#"integer" "integer"  "factor"  "factor" 

Upvotes: 1

Eden Z
Eden Z

Reputation: 56

If I understand correctly you have one data frame with factor column values of another data frame. You want to extract these from the 1st df and mutate these columns in the 2nd df and turn them into factors.

What about keeping a vector of the column names and then mutate them all?

colnames <- t %>%
  pull(Item) %>%
  as.character()

d_with_factors <- d %>%
  mutate_at(colnames, as.factor)

Then

sapply(d_with_factors, class)

Returns

       id       age   factor1   factor2 
"integer" "integer"  "factor"  "factor" 

Upvotes: 2

Related Questions