Wayne's Github Page

A place to learn about statistics

Regular expression

When dealing with text data, often we encounter patterns in text but exact matches are difficult to specify, e.g. we want all the “numbers” mentioned in a particular piece of text. This is when regular expression comes in handy.

Terminology string vs character

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.

import pandas as pd

fighters = pd.read_csv("raw_fighter_details.csv")
fighters.DOB.head(20)

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 positions in the string using [].

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.

def grab_month(date_str):
    if len(date_str) != 12:
        return date_str
    return date_str[:3]


months = fighters.apply(lambda x: grab_month(str(x.DOB)), 1)
months.value_counts()

What to notice?

Parsing strings with more complicated patterns

Natural text, however, is rarely formatted in such a uniform fashion.

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

demo = "Experience with Google Analytics and Google Optimize\nKnowledge of project management tools (JIRA, Trello, Asana)\n"
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 from the built-in package re, re.sub(). This function can replace the specified patterns with a specified output.

import re
re.sub("!", "_", "Hello! IN PERSON CLASSES!!!")

Below we explain how regular expression works in more detail.

Specifying the frequency in regular expression

For the following example, we will use the demo string demo = 'rawr rawrr rawrrr!' 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 `` re.sub("r", "_", demo)
Exactly 2 times (notice 2
can be replaced with any integer)
{2} re.sub("r{2}", "_", demo)
Exactly 2 to 4 times (notice 2 and
4 can be replaced with any integer)
{2,4} re.sub("r{2,4}", "_", demo)
Zero or one occurrence ? re.sub("r?", "_", demo)
One or more occurrence + re.sub("r+", "_", demo)
Zero or more occurrence * re.sub("r*", "_", demo)

Notice that there are “empty strings” between each character by default, causing frequencies that catch “zero” occurrences to add an additional _ to the examples above.

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) re.sub("(EDU|EDUCATION)", "?", demo)
Any character between t, 0, OR 1 (t|0|1) or [t01] re.sub("(t|0|1)", "?", demo)
re.sub("[t01]", "?", demo)
Any digit between 0 to 9 \d or [0-9] or [5498732160] re.sub("\d", "?", demo)
re.sub("[0-9]", "?", demo)
Any lowercased alphabet [a-z] re.sub("[a-z]", "?", demo)
Any capital alphabet [A-Z] re.sub("[A-Z]", "?", demo)
alphanumeric or the underscore “_” \w or [a-zA-Z0-9_] re.sub("\w", "?", demo)
Any character (wild card) . re.sub(".", "?", demo)
NOT lower case alphabets [^a-z] re.sub("[^a-z]", "?", demo)

What to notice?

Combining characters and frequencies to form patterns

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

import re

demo = "Experience with Google Analytics and Google Optimize\nKnowledge of project management tools (JIRA, Trello, Asana)\n"
re.sub("[^a-zA-Z]+", " ", demo).split(" ")

The code replaced “one or more non-letter character” with " ", allowing us to use str.split() to split out the words.

Limiting the substitution frequencies

Sometimes we only wish for the substitution to happen a limited number of times, that can be achieved with the argument count in re.sub(). This is defaulted to 0 which starts for no limits on the number of substitutions.

demo = "rawr, rawrr, rwarrr"
re.sub("r{2}", "_", demo, count=1)

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 = ["www.google.com", "www.r-project.org", "www.linkedin.com", "xkcd.com"]
proper_demo = ['https://' + s for s in 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)"
[re.sub(pattern, "caught ya!", s) for s in 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. We’ll add an additional () for the subpattern that should match the website name.

pattern = "https://(www\.)?([^\.]+)\.(com|org)"
[re.sub(pattern, "\\1 AND \\2 AND \\3", s) for s in proper_demo]
[re.sub(pattern, "\\2", s) for s in proper_demo]
[re.sub(pattern, "\\1SOMETHING.\\3", s) for s in proper_demo]

What to notice:

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/embrace/accept about regular expression is that it is a very heavy trial and error process. It is hard to know all the possible ways free form text may appear. 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 :)