Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tagging/categorizing a string column using multiple matching patterns

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
})
like image 361
sbha Avatar asked Aug 04 '26 01:08

sbha


2 Answers

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
like image 120
Serban Tanasa Avatar answered Aug 06 '26 16:08

Serban Tanasa


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 = '|'))) 
like image 20
sbha Avatar answered Aug 06 '26 17:08

sbha



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!