Wayne's Github Page

A place to learn about statistics

Analyzing Text Data

This chapter will focus on programmatically identifying key words in job descriptions. Job descriptions often introduce the company, the role they’re hiring for, then follow up with basic and preferred qualifications. As students, it would be nice to understand what qualifications are common across different employers.

To understand and analyze text, we need to learn how to read, parse, search through text data. Specifically:

Terminology string, text, and character

Both characters and strings will be stored as character values in R.

empty_string <- ""
demo_string <- "random string!"
nchar(empty_string)
nchar(demo_string)
class(empty_string)
class(demo_string)

Recognizing patterns in text data

For our first example, we’ll try to work with birthdays of UFC fighters in raw_fighter_details.csv on Kaggle.

The data is still in a csv format so we can leverage the old functions we learned.

fighters <- read.csv("YOUR_PATH/raw_fighter_details.csv",
                     stringsAsFactors = FALSE)
head(fighters$DOB)
# [1] ""             ""             ""            
# [4] ""             ""             "Nov 12, 1974"
# [7] "Mar 18, 1989" "Nov 14, 1992" "Aug 26, 1986"
#[10] ""             "Aug 05, 1989"

What to notice?

This format suggests that we could subset different characters to obtain the different pieces of information.

Getting a substring, subsections of a string

When the data follows a strict pattern as we have seen, it’s possible to extract the birth month by subsetting specific indices in the character string (indices are like locations on the string).

To get the months, we’ll write a function that can be applied to each record. This means that the function needs to handle the case when the date is missing. To capture the empty string case and potentially other cases, we’ll check if the string has 12 characters before subsetting. Specifically, in the event that there are not 12 characters, we will return the original string so we can analyze these cases later.

grab_month <- function(date_str){
    if(nchar(date_str) != 12){
        return(date_str)
    }
    month <- substr(date_str, 1, 3)
    return(month)
}
months <- sapply(fighters$DOB, grab_month)
table(months)
# months
#     Apr Aug Dec Feb Jan Jul Jun Mar May Nov Oct Sep 
# 740 199 267 184 197 180 262 202 216 225 199 226 216

What to notice?

Exercise

Reading in the data as character strings

Recall the Fisher dataset that read.csv() nicely parsed into a data frame. We can also read the data in as character strings to see the format of the data using readLines()

Please run the following code and compare it against the data frame version.

grain_csv <- read.csv('fisher_1927_grain.csv')
grain <- readLines('fisher_1927_grain.csv')
class(grain)
length(grain)
head(grain, 2)
head(grain_csv, 2)

What to notice?

Exercise

Why should I bother with readLines() given read.csv() exists?

Separating strings based on a special symbol

.csv stands for comma separated values, i.e. commas are dedicated to separate one value from another.

To split character strings by a particular symbol, we could use the function strsplit() to parse the data.

The bahvior of strplsit() is slightly more complicated so it’s best to start with a demo.

demo_str <- c("2020/04/01", "2019/12/11")
strsplit(demo_str, split="/")
strsplit(demo_str[1], split="/")

What to notice:

Exercise with the Fisher dataset

Parsing strings with more complicated patterns

Notice how strsplit() depended on the data to be formatted in a certain fashion. Natural text, however, is rarely formatted in such a uniform fashion.

Imagine trying to extract each word from the following sentence from a product manager job scription.

demo <- "Experience with Google Analytics and Google Optimize\nKnowledge of project management tools (JIRA, Trello, Asana)\n"
strsplit(demo, split=" ")

Notice how we normally would not consider “(“ and “,” as part of the word. This means we need a more flexible way to manipulate text programmatically, this is where regular expression comes in.

Regular expression: specifying complex text patterns

Regular expression is a common syntax for specifying complex text patterns for programs.

The general syntax for the pattern is to specify

  1. The type of character
  2. Immediately followed by the frequency for the character

These patterns are then used as an inputs to various functions. The first common operation is the substitute function, sub(), that can replace the specified patterns with a specified output. For example:

demo <- "x = 1"
sub("=", "<-", demo)

The code replaced = with <- for the character assigned in the variable demo.

Specifying the frequency in regular expression

For the following example, we will use the demo string demo <- 'borrow some rope' with the r as the character to be replaced.

Here are a few common frequencies, you should TYPE out each example to see the impact

Frequency Regular Expression Example
Exactly once `` sub("r", "_", demo)
Exactly 2 times (notice 2
can be replaced with any integer)
{2} sub("r{2}", "_", demo)
Exactly 2 to 4 times (notice 2 and
4 can be replaced with any integer)
{2,4} sub("r{2,4}", "_", demo)
Zero or one occurrence ? sub("r?", "_", demo)
One or more occurrence + sub("r+", "_", demo)
Zero or more occurrence * sub("r*", "_", demo)

Exercise

Specifying the character in regular expression

For the following example, we will use the demo string with a diverse set of characters demo <- 'wtl_2109@COLUMBIA.EDU'

Character Regular Expression Example
a word as a character (edu|education) sub("(EDU|EDUCATION)", "?", demo)
Any character between t, 0, OR 1 (t|0|1) or [t01] sub("(t|0|1)", "?", demo)
sub("[t01]", "?", demo)
Any digit between 0 to 9 \\d or [0-9] or [5498732160] sub("\\d", "?", demo)
sub("[0-9]", "?", demo)
Any lowercased alphabet [a-z] sub("[a-z]", "?", demo)
Any capital alphabet [A-Z] sub("[A-Z]", "?", demo)
alphanumeric or the underscore “_” \\w or [a-zA-Z0-9_] sub("\\w", "?", demo)
Any character (wild card) . sub(".", "?", demo)
NOT lower case alphabets [^a-z] sub("[^a-z]", "?", demo)

What to notice?

Exercise

Combining characters and frequencies to form patterns

Returning to our original problem of parsing a sentence in the job description.

demo <- "Experience with Google Analytics and Google Optimize\nKnowledge of project management tools (JIRA, Trello, Asana)\n"
strsplit(demo, split=" ")

A simple solution is to substitute every occurrence of one or more (frequency) non-letters (characters) into a single space. Then separate the string by the space symbol.

demo_subbed <- sub("[^a-zA-Z]+", " ", demo)
demo_subbed == demo

The output is likely identical to demo because the first non-letter symbol is a single space character, which was replaced with a space. The key we’re misssing is that we wanted this substitution to happen for “every occurrence”. To do this, we should use the function gsub() instead.

demo_subbed <- gsub("[^a-zA-Z]+", " ", demo)
strsplit(demo_subbed, split=" ")

It turns out that you can enter regular expression into strsplit() directly for the split argument.

strsplit(demo_subbed, split="[^a-zA-Z]+")

Exercise

Getting a sub-pattern within multiple patterns

We can also combine multiple patterns into a single pattern. Imagine trying to get the website names from the following demo:

demo <- c("www.google.com", "www.r-project.org", "www.linkedin.com", "xkcd.com")
proper_demo <- paste0('https://', demo)

As humans, we recognize the “www” and the different endings (.org and .com) as not part of the name.

To specify the patterns we’re seeing, you most likely will come up with something like

pattern <- "https://(www\\.)?[^\\.]+\\.(com|org)"
sub(pattern, "caught ya!", proper_demo)

What to notice:

The above example, however, did not extract the website name. To do so, there’s another specific idea in regular expression substitution that you should know.

pattern <- "https://(www\\.)?([^\\.]+)\\.(com|org)"
sub(pattern, "\\1 AND \\2 AND \\3", proper_demo)
sub(pattern, "\\2", proper_demo)
sub(pattern, "\\1SOMETHING.\\3", proper_demo)

What to notice:

Exercise

Other functions with regular expression

Besides substitution, there are a few other important functions that rely on regular expression.

Regular expression in practice

One thing to know about regular expression is that it is a very heavy trial and error process. You should always double check your results and you should always Google for a solution since someone else has likely encountered the same question.

Most software systems are designed such that “free text” is changed to drop-down menus so the input is more predictable. Problems that require regular expression are likely natural text which has many edge cases that rarely can be 100% captured by regular expression.

The best way to learn is to keep trying :)