I have a data frame with a column of strings that need to be classified based on another data frame that has the category labels in one column and the matching terms/patterns in another.
There are 50+ categories and each string could match multiple categories while others will have no matches. How do I efficiently tag these strings with category labels?
Below is a simple sample dataset and the output I hope to get. If it makes any difference, the strings in the real data set are much longer than these sample strings and there are a couple hundred thousand of them.
recipes <- c('fresh asparagus', 'a bunch of bananas', 'one pound pork', 'no fruits, no veggies, no nothing', 'broccoli or spinach','I like apples, asparagus, and pork', 'meats like lamb', 'venison sausage and fried eggs', 'spinach and arugula salad', 'scrambled or poached eggs', 'sourdough english muffins')
recipes_df <- data.frame(recipes, stringsAsFactors = FALSE)
category <- c('vegetable', 'fruit', 'meat','bread','dairy')
items <- c('arugula|asparagus|broccoli|peas|spinach', 'apples|bananas|blueberries|oranges', 'lamb|pork|turkey|venison', 'sourdough', 'buttermilk|butter|cream|eggs')
category_df <- data.frame(category, items)
This is the output I hope to get:
recipes recipes_category
1 fresh asparagus vegetable
2 a bunch of bananas fruit
3 one pound pork meat
4 no fruits, no veggies, no nothing <NA>
5 broccoli or spinach vegetable
6 I like apples, asparagus, and pork fruit, vegetable, meat
7 meats like lamb meat
8 venison sausage and fried eggs meat, dairy
9 spinach and arugula salad vegetable
10 scrambled or poached eggs dairy
11 sourdough english muffins breads
I believe that some combination of grepl and a for loop or a version of apply is necessary, but the examples I've tried below really expose how little I understand R. For example using sapply gives the results I expect, sapply(category_df$items, grepl, recipes_df$recipes)but I am not sure how I can convert those results to the simple column I need.
If I use the categorize function found here, it only matches one category to each string:
categorize_food <- function(df, searchString, category) {
df$category <- "OTHER"
for(i in seq_along(searchString)) {
list <- grep(searchString[i], df[,1], ignore.case=TRUE)
if (length(list) > 0) {
df$category[list] <- category[i]
}
}
df
}
recipes_cat <- categorize_food(recipes_df, category_df$items, category_df$category)
Likewise, the function found here is the closest to what I am looking for, but I do not understand why the category numbers map the way that they do. I would expect the vegetable category would be 1 not 2 and dairy would be 5 not 3.
vec = category_df$items
recipes_df$category = apply(recipes_df, 1, function(u){
bool = sapply(vec, function(x) grepl(x, u[['recipes']]))
if(any(bool)) vec[bool] else NA
})
The aggregate near the end is a bit slow for large datasets, so perhaps look up a faster way (data.table?) to turn rows to strings, but this should generally work:
tmplist <- strsplit(items, "|", fixed=TRUE)
#Removes horrid '|' separated values into neat rows
searchterms <- data.frame(category=rep(category, sapply(tmplist, length)),
items=unlist(tmplist), stringsAsFactors=FALSE)
#Recreates data frame, neatly
res <- lapply(searchterms$items, grep, x=recipes, value=TRUE)
#throws an lapply on the neat data pattern against recipes
matched_times <- sapply(res, length)
df_matched <- data.frame( category = rep(searchterms$category[matched_times!=0],
matched_times[matched_times != 0]),
recipes = unlist(res))
# Combines category names the correct nr of times with grep
#results (recipe names), to create a tidy result
df_ummatched <- data.frame( category = NA, recipes = recipes[!recipes %in% unlist(res)])
df <- rbind(df_matched, df_ummatched)
#gets the nonmatched, plops it in with NA values.
final <- aggregate(category~recipes, data=df, paste, sep=",", na.action=na.pass)
#makes the data untidy, as you asked.
But this still leaves us with duplicate vegetable, vegetable entries. Can't have that:
SplitFunction <- function(x) {
b <- unlist(strsplit(x, ','))
c <- b[!duplicated(b)]
return(paste(c, collapse=", "))
}
SplitFunctionV <- Vectorize(SplitFunction)
final$category <- SplitFunctionV(final$category)
And the results:
final
recipes category
1 a bunch of bananas fruit
2 broccoli or spinach vegetable
3 fresh asparagus vegetable
4 I like apples, asparagus, and pork vegetable, fruit, meat
5 meats like lamb meat
6 one pound pork meat
7 scrambled or poached eggs dairy
8 sourdough english muffins bread
9 spinach and arugula salad vegetable
10 venison sausage and fried eggs meat, dairy
11 no fruits, no veggies, no nothing NA
Here's a tidyverse option that is pretty straightforward:
library(tidyverse)
# reformat category data frame so each item has its own line:
category_df <-
category_df %>%
mutate(items = str_split(items, "\\|")) %>%
unnest()
# then use string_extract_all() to find every item in each recipe string:
recipes_df %>%
mutate(recipe_category = str_extract_all(recipes, paste(category_df$items, collapse = '|')))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With