Skip to Content
Course content

175: Text Cleaning Pipelines

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, we've all been there. You get a dataset of user-submitted product reviews, and it's a disaster. You've got HTML tags like &, random double spaces, currency symbols mixed with text, and people who hit the Caps Lock key for three paragraphs. Your first instinct is usually to just start hacking away with gsub().

The "Matryoshka Doll" approach

When I first started working with text in R, I used to write what I call "Matryoshka" code—functions nested inside functions until the parentheses at the end looked like a picket fence. It usually looks something like this:

clean_text <- gsub("[[:punct:]]", "", gsub("<.*?>", "", tolower(trimws(raw_reviews))))

In the short term, this works. It's a one-liner, and you feel efficient. But as soon as your boss comes back and says, "Actually, we need to preserve hashtags but remove everything else," you're in trouble. To change one rule, you have to carefully peel back the layers of the nesting, hoping you don't accidentally delete a closing parenthesis and spend twenty minutes hunting for a syntax error. It's brittle, it's hard to read, and it's nearly impossible to unit test. If a specific piece of text is coming out wrong, you can't easily see which of those five nested calls caused the corruption.

Building a text assembly line

The professional way to handle this is to stop thinking about "cleaning a string" and start thinking about a "text pipeline." I want a sequence of discrete transformations where the output of one step is the input to the next. In R, the native pipe |> (or the dplyr %>%) is the obvious choice here, but the real magic happens when you decouple your cleaning rules from the execution logic.

Instead of hard-coding the replacements into a chain, I prefer to define a named list of regex patterns and their replacements. This turns your logic into data, which is much easier to manage.

# Define the "rules of the road" separately
cleaning_rules <- list(
  html_tags = c("<.*?>", ""),
  extra_whitespace = c("\\s+", " "),
  currency_symbols = c("[\\$\\€\\£]", ""),
  special_chars = c("[^a-zA-Z0-9\\s]", "")
)

# Create a helper to apply all rules
apply_cleaning_pipeline <- function(text, rules) {
  for (rule_name in names(rules)) {
    pattern <- rules[[rule_name]][1]
    replacement <- rules[[rule_name]][2]
    text <- gsub(pattern, replacement, text)
  }
  return(text)
}

# Now the actual execution is clean and readable
final_reviews <- raw_reviews |>
  tolower() |>
  trimws() |>
  apply_cleaning_pipeline(cleaning_rules)

Why this survives production

The trade-off here is a bit more boilerplate code upfront, but the payoff is massive. First, readability. If a colleague looks at this, they don't have to parse a complex regex nest; they just look at the cleaning_rules list and see exactly what's being removed. Second, maintainability. If you need to stop removing currency symbols, you just delete one line from a list—you don't touch the logic of the function itself.

More importantly, this approach allows you to debug the pipeline. I often add a print() or a message() inside that for loop during development. That way, I can watch the text evolve step-by-step: "Okay, the HTML is gone, but wait, the currency symbol rule just deleted something it shouldn't have." You can't do that with nested functions without rewriting the whole block into temporary variables.




📋 Practical Task

Exercise: Building a Log-File Sanitizer

You have been handed a vector of messy system log entries that contain sensitive IP addresses, timestamps, and erratic capitalization. Your goal is to build a pipeline that sanitizes these logs for a public report.

The Data:

logs <- c("2023-10-01 12:00:01 [ERROR] User 192.168.1.1 failed to login!!", 
            "2023-10-01 12:05:22 [INFO] Connection from 10.0.0.50 established...", 
            "2023-10-01 12:10:00 [WARN] High latency detected on 172.16.254.1")

Your Task:

  1. Create a named list called log_rules. It should contain regex patterns to:
    • Remove the date/time stamp at the start (e.g., 2023-10-01 12:00:01).
    • Replace any IP address (four groups of digits separated by dots) with the string [IP_REDACTED].
    • Remove trailing punctuation (like !! or ...).
  2. Write a function sanitize_logs() that takes a character vector and your log_rules list, applying each transformation sequentially.
  3. Use a pipe to pass the logs vector through tolower() and then through your sanitize_logs() function.

The final output should look like: "[error] user [ip_redacted] failed to login"

Rating
0 0

There are no comments for now.

to be the first to leave a comment.