How to Join Two Data Frames on a Partial String Match in R
When working with messy text data or unstructured metadata in R, you often need to perform joins that standard functions like dplyr::left_join() cannot handle out of the box. A classic example is matching a reference table to a dataset where the key is buried inside a descriptive sentence or string.
If you have hundreds of potential keywords to search across, chaining nested ifelse() or grepl() calls quickly becomes unmaintainable and computationally inefficient. Here are the two best, idiomatic ways to solve this problem in R using modern packages: regex extraction with dplyr and declarative matching with fuzzyjoin.
The Sample Data
Let's use the reproducible data provided in the scenario:
specimen_origin <- data.frame(
specimen = c("Specimen 1", "Specimen 2", "Specimen 3", "Specimen 4"),
origin = c("Collected on trip to Denver, Colorado",
"Found in wild at Ohio Botanical Gardens",
"Cliffside during Yunnan 2002 expedition",
"Unknown"),
stringsAsFactors = FALSE
)
country_origin_index <- data.frame(
region = c("Colorado", "Ohio", "Missouri", "Texas", "Yunnan", "Chongqing"),
country = c("USA", "USA", "USA", "USA", "China", "China"),
stringsAsFactors = FALSE
)
Method 1: Regex Extraction + Standard Join (Fastest & Most Scalable)
If you have thousands of rows and a long list of keywords, compiling your search terms into a single regular expression pattern and extracting the match before joining is the most scalable approach.
We can use stringr::str_extract() together with word boundaries (\b) to avoid false matches (e.g., ensuring "Ind" doesn't accidentally match "India"):
library(dplyr)
library(stringr)
library(tidyr)
# 1. Create a regex alternation pattern using word boundaries
pattern <- paste0("\\b(", paste(country_origin_index$region, collapse = "|"), ")\\b")
# 2. Extract the matching region and perform a standard left_join
result <- specimen_origin %>%
mutate(region = str_extract(origin, pattern)) %>%
left_join(country_origin_index, by = "region") %>%
mutate(across(c(region, country), ~ replace_na(.x, "")))
print(result)
Output:
specimen origin region country
1 Specimen 1 Collected on trip to Denver, Colorado Colorado USA
2 Specimen 2 Found in wild at Ohio Botanical Gardens Ohio USA
3 Specimen 3 Cliffside during Yunnan 2002 expedition Yunnan China
4 Specimen 4 Unknown
Why this works well:
- Performance: The extraction runs in vectorised C code under the hood (via
stringi), which is significantly faster than performing a full cross-join with regex evaluations. - Safety: Wrapping keywords in
\bensures you match full words rather than partial character sequences inside unrelated words.
Method 2: Using the fuzzyjoin Package
If you prefer a declarative syntax without constructing regex strings manually, the fuzzyjoin package provides a dedicated helper function called regex_left_join().
Install the package if you haven't already:
install.packages("fuzzyjoin")
Then join the two tables directly by evaluating the regex relationship:
library(dplyr)
library(fuzzyjoin)
library(tidyr)
result <- regex_left_join(
specimen_origin,
country_origin_index,
by = c(origin = "region"),
ignore_case = FALSE
) %>%
mutate(across(c(region, country), ~ replace_na(.x, "")))
print(result)
Important Considerations with fuzzyjoin
- Multiple Matches: If an
originstring contains more than one region (for example, "Specimen from Colorado and Ohio border"),fuzzyjoinwill duplicate rows for each match, just like a standard SQL join. - Substrings: By default,
regex_left_join()treats the values incountry_origin_index$regionas regular expressions. If your regions contain special regex characters (such as parentheses or periods), you should escape them beforehand usingstringr::str_escape().
Summary: Which Approach Should You Use?
- Use Method 1 (
str_extract+left_join) if you expect at most one region per specimen description, need maximum performance across large datasets, or want strict word-boundary checks. - Use Method 2 (
fuzzyjoin::regex_left_join) if you want concise code or genuinely need to retain duplicate rows when multiple keywords match the same string.