Foundations of
Data Science

Spring 2026 | Data 2 (399)
Jeffrey M. Girard | Lecture 12a

Roadmap

  • Understand how R handles strings and special characters

  • Combine, separate, and extract parts of text data

  • Use regular expressions to detect and replace patterns within strings

Working with Strings

Text Data in R

  • Question: How does R handle text?

  • Text data is stored as strings (character vectors)

  • We create strings by wrapping text in double quotes ("as so") or single quotes ('as so')

  • We will use the {stringr} package (part of the {tidyverse}). All of its functions start with str_ and are built specifically to handle messy text
  • We will learn the basics of regular expressions (regex) to search for complex patterns in text

Creating Strings & Escapes

Question: How do we define text data in R?

x <- "We usually create strings by wrapping text in double quotes."
writeLines(x)
We usually create strings by wrapping text in double quotes.
y <- "Backslashes (\\) \"escape\" special symbols, treating them as characters."
writeLines(y)
Backslashes (\) "escape" special symbols, treating them as characters.
z <- "Backslashes can also create: \nNewlines (\\n) and \ttabs (\\t)"
writeLines(z)
Backslashes can also create: 
Newlines (\n) and   tabs (\t)

Combining Strings

Question: How do we stick multiple strings together?

df <- 
  tibble(
    first = c("John", "Jane", "Alice"),
    last = c("Doe", "Smith", "Johnson")
  ) |> 
  print()
# A tibble: 3 × 2
  first last   
  <chr> <chr>  
1 John  Doe    
2 Jane  Smith  
3 Alice Johnson

Combining Strings

Question: How can we flexibly combine strings?

Yes, str_glue() lets us insert R variables directly into a string using curly braces {}. This is often much easier to read than str_c().

df |> 
  mutate(
    greeting = str_glue("Hello, {first} {last}! Welcome back.")
  )
# A tibble: 3 × 3
  first last    greeting                           
  <chr> <chr>   <glue>                             
1 John  Doe     Hello, John Doe! Welcome back.     
2 Jane  Smith   Hello, Jane Smith! Welcome back.   
3 Alice Johnson Hello, Alice Johnson! Welcome back.

Flattening Strings

Question: How do we collapse a whole column into a single string?

We can use str_flatten() to combine a vector of multiple strings into one single string. We can also specify a separator.

df |> 
  summarize(
    all_names = str_flatten(first, collapse = ", ")
  )
# A tibble: 1 × 1
  all_names        
  <chr>            
1 John, Jane, Alice

Real-World Application

Question: Which carriers operate out of each NYC airport?

data("flights", package = "nycflights13")
flights |> 
  summarize(
    .by = origin,                   # For each origin airport...
    carriers = 
      carrier |>                    # Find its active carriers
      unique() |>                   # Remove all duplicates
      sort() |>                     # Sort them alphanumerically
      str_flatten(collapse = ", ")  # Flatten them into one string
  )
# A tibble: 3 × 2
  origin carriers                                          
  <chr>  <chr>                                             
1 EWR    9E, AA, AS, B6, DL, EV, MQ, OO, UA, US, VX, WN    
2 LGA    9E, AA, B6, DL, EV, F9, FL, MQ, OO, UA, US, WN, YV
3 JFK    9E, AA, B6, DL, EV, HA, MQ, UA, US, VX            

Extracting & Separating

Subsetting Strings

Question: How do we extract a specific range of characters?

# Hex color codes use #RRGGBB for red, green, and blue intensities
df <- 
  tibble(
    hex = c("#FF0000", "#00FF00", "#0000FF")
  ) |> 
  print()
# A tibble: 3 × 1
  hex    
  <chr>  
1 #FF0000
2 #00FF00
3 #0000FF

Subsetting Strings

We use str_sub(), which takes a start and end position. We can even use negative numbers to count backward from the end of the string.

df |> 
  mutate(
    red = str_sub(hex, start = 2, end = 3),     # 2 from left to 3 from left 
    green = str_sub(hex, start = -4, end = -3), # 4 from right to 3 from right
    blue = str_sub(hex, start = 6, end = -1)    # 6 from left to 1 from right
  )
# A tibble: 3 × 4
  hex     red   green blue 
  <chr>   <chr> <chr> <chr>
1 #FF0000 FF    00    00   
2 #00FF00 00    FF    00   
3 #0000FF 00    00    FF   

Separating Columns

Question: How do we split one messy column into multiple clean ones?

df <- 
  tibble(
    id = 1:3,
    blood_pressure = c("120/80", "135/90", "110/70")
  ) |> 
  print()
# A tibble: 3 × 2
     id blood_pressure
  <int> <chr>         
1     1 120/80        
2     2 135/90        
3     3 110/70        

Separating Columns

df |> 
  separate_wider_delim(
    cols = blood_pressure,              # original/messy col name
    delim = "/",                        # character to separate on
    names = c("systolic", "diastolic")  # new col names to create
  )
# A tibble: 3 × 3
     id systolic diastolic
  <int> <chr>    <chr>    
1     1 120      80       
2     2 135      90       
3     3 110      70       

Lengthening Columns

Question: Can we separate into rows instead of columns?

df <- 
  tibble(
    name = c("Alice", "Bob"),
    classes = c("Math, Science", "History, Art, Math")
  ) |> 
  print()
# A tibble: 2 × 2
  name  classes           
  <chr> <chr>             
1 Alice Math, Science     
2 Bob   History, Art, Math

Lengthening Columns

df |> 
  separate_longer_delim(
    cols = classes, 
    delim = ", "
  )
# A tibble: 5 × 2
  name  classes
  <chr> <chr>  
1 Alice Math   
2 Alice Science
3 Bob   History
4 Bob   Art    
5 Bob   Math   

Real-World Application

Question: Why is this useful in data science?

df <- tibble(sku = c("US-APP-2023-11", "CA-BAN-2024-01"))
df |> 
  separate_wider_delim(
    cols = sku, 
    names = c("country", "product", "year", "month"), 
    delim = "-"
  )
# A tibble: 2 × 4
  country product year  month
  <chr>   <chr>   <chr> <chr>
1 US      APP     2023  11   
2 CA      BAN     2024  01   

Regular Expressions

Intro to Regex

  • Question: What if our target isn’t a fixed position or a simple delimiter?

  • Regular Expressions (regex) are a concise language for describing patterns in strings. They are very powerful but can look like gibberish

  • Most {stringr} functions take in a regex pattern
  • We will use str_view() to preview what our regex is matching. Matches will be surrounded by angle brackets: <...>
  • Then we will learn how to apply them

Exact Matching

The simplest regex pattern is just an exact match of characters.

x <- c("apple", "banana", "pear", "pineapple")

# Show me elements that contain "apple"
str_view(x, "apple")
[1] │ <apple>
[4] │ pine<apple>

Anchors

Question: What if we only want matches at the start or end of a string?

We use anchors. ^ matches the start of the string, and $ matches the end.

# Show me elements that START with "a"
str_view(x, "^a")
[1] │ <a>pple
# Show me elements that END with "e"
str_view(x, "e$")
[1] │ appl<e>
[4] │ pineappl<e>

Regex Building Blocks

Question: How do we match flexible patterns instead of exact matches?

  • Wildcards (.): Matches any single character
  • Character Classes ([]): Matches any one of the characters listed
  • Shortcuts: Built-in classes for common patterns
    • e.g., \\d (digits) and \\s (spaces)
  • Quantifiers: Control how many times a pattern repeats
    • {n}: matches exactly n times
    • +: matches one or more times

Examples

x <- c("sun", "sand", "soon", "July 4", "2026")
# Single Wildcard
str_view(
  string = x, 
  pattern = "s.n"
)
[1] │ <sun>
[2] │ <san>d
# One or More Wildcards
str_view(
  string = x, 
  pattern = "s.+n"
)
[1] │ <sun>
[2] │ <san>d
[3] │ <soon>
# Spaces
str_view(
  string = x, 
  pattern = "\\s"
)
[4] │ July< >4
# Exactly 4 digits
str_view(
  string = x, 
  pattern = "\\d{4}"
)
[5] │ <2026>

Detecting Patterns

Question: How do we filter a dataset based on text patterns?

df <- 
  tibble(name = c("Ann", "Andy", "Amanda", "Bob", "Cindy")) |> 
  print()
# A tibble: 5 × 1
  name  
  <chr> 
1 Ann   
2 Andy  
3 Amanda
4 Bob   
5 Cindy 

Detecting Patterns

We can use str_detect() to ask if the pattern was detected

# Is "an" or "An" detected?
df |> 
  mutate(
    match = str_detect(name, "[aA]n")
  )
# A tibble: 5 × 2
  name   match
  <chr>  <lgl>
1 Ann    TRUE 
2 Andy   TRUE 
3 Amanda TRUE 
4 Bob    FALSE
5 Cindy  FALSE
# Retain row if it is detected
df |>
  filter(
    str_detect(name, "[aA]n")
  )
# A tibble: 3 × 1
  name  
  <chr> 
1 Ann   
2 Andy  
3 Amanda

Replacing Patterns

Question: How do we fix or replace specific text elements?

(x <- "We often utilize fancy words when we could utilize simple ones.")
[1] "We often utilize fancy words when we could utilize simple ones."
# Replace the FIRST instance of the pattern only
str_replace(x, pattern = "utilize", replacement = "use")
[1] "We often use fancy words when we could utilize simple ones."
# Replace all instances of the pattern
str_replace_all(x, pattern = "utilize", replacement = "use")
[1] "We often use fancy words when we could use simple ones."

Removing Patterns

Question: How do we delete parts of a string?

(x <- c("(555) 123-4567", "800-555-0199"))
[1] "(555) 123-4567" "800-555-0199"  
# Remove any of the following: [...]
#   Parentheses: ()
#   Hyphens: \\-
#   Spaces: \\s
str_remove_all(x, "[()\\-\\s]")
[1] "5551234567" "8005550199"

Removing Whitespace

x <- "\t  Sometimes strings have\n   too   much white space "
writeLines(x)
      Sometimes strings have
   too   much white space 
# Forcibly remove all whitespace
str_remove_all(x, "\\s")
[1] "Sometimesstringshavetoomuchwhitespace"
# Remove outer whitespace and compress inner whitespace
str_squish(x)
[1] "Sometimes strings have too much white space"

Real-World Application

Question: Why is this useful in data science?

Data entered by humans in free-text fields is notoriously messy. We can string these tools together to sanitize it.

df <- 
  tibble(
    email = c("jane AT uni DOT edu", "\njohn@gmail.com ", 
              "educator@khan.org", "SMITH@  college.edu ")
  ) |> 
  print()
# A tibble: 4 × 1
  email                 
  <chr>                 
1 "jane AT uni DOT edu" 
2 "\njohn@gmail.com "   
3 "educator@khan.org"   
4 "SMITH@  college.edu "

Real-World Application

df |> 
  mutate(
    clean_email = 
      email |> 
      str_squish() |>               # Collapse whitespace
      str_replace(" AT ", "@") |>   # Replace AT with @
      str_replace(" DOT ", ".") |>  # Replace DOT with .
      str_to_lower() |>             # Make all lowercase
      str_remove_all("\\s"),        # Remove all spaces
    dotedu = str_detect(clean_email, "\\.edu$")
  )
# A tibble: 4 × 3
  email                  clean_email       dotedu
  <chr>                  <chr>             <lgl> 
1 "jane AT uni DOT edu"  jane@uni.edu      TRUE  
2 "\njohn@gmail.com "    john@gmail.com    FALSE 
3 "educator@khan.org"    educator@khan.org FALSE 
4 "SMITH@  college.edu " smith@college.edu TRUE