Strings

Note: this is a modified version of an original notebook by Cliburn Chan. The original and this modified version are both released under the terms of the MIT license.

The process of cleaning data for analysis often requires working with text, for example, to correct typos, convert to standard nomenclature and resolve ambiguous labels. In some statistical fields that deal with (say) processing electronic medical records, information science or recommendations based on user feedback, text must be processed before analysis - for example, by converting to a bag of words.

We will use a whimsical example to illustrate Python tools for munging text data using string methods and regular expressions. Finally, we will see how to format text data for reporting.

Get “Through the Looking Glass”

In [1]:
import requests

try:
    with open('looking_glass.txt') as f:
        text = f.read()
except IOError:
    url = 'http://www.gutenberg.org/cache/epub/12/pg12.txt'
    res = requests.get(url)
    text = res.text
    with open('looking_glass.txt', 'w') as f:
        f.write(str(text))

Slice to get Jabberwocky

In [2]:
start = text.find('JABBERWOCKY')
In [3]:
text[start:start+2000]
Out[3]:
"JABBERWOCKY\n\n     'Twas brillig, and the slithy toves\n      Did gyre and gimble in the wabe;\n     All mimsy were the borogoves,\n      And the mome raths outgrabe.\n\n     'Beware the Jabberwock, my son!\n      The jaws that bite, the claws that catch!\n     Beware the Jubjub bird, and shun\n      The frumious Bandersnatch!'\n\n     He took his vorpal sword in hand:\n      Long time the manxome foe he sought--\n     So rested he by the Tumtum tree,\n      And stood awhile in thought.\n\n     And as in uffish thought he stood,\n      The Jabberwock, with eyes of flame,\n     Came whiffling through the tulgey wood,\n      And burbled as it came!\n\n     One, two! One, two! And through and through\n      The vorpal blade went snicker-snack!\n     He left it dead, and with its head\n      He went galumphing back.\n\n     'And hast thou slain the Jabberwock?\n      Come to my arms, my beamish boy!\n     O frabjous day! Callooh! Callay!'\n      He chortled in his joy.\n\n     'Twas brillig, and the slithy toves\n      Did gyre and gimble in the wabe;\n     All mimsy were the borogoves,\n      And the mome raths outgrabe.\n\n\n'It seems very pretty,' she said when she had finished it, 'but it's\nRATHER hard to understand!' (You see she didn't like to confess, even\nto herself, that she couldn't make it out at all.) 'Somehow it seems\nto fill my head with ideas--only I don't exactly know what they are!\nHowever, SOMEBODY killed SOMETHING: that's clear, at any rate--'\n\n'But oh!' thought Alice, suddenly jumping up, 'if I don't make haste I\nshall have to go back through the Looking-glass, before I've seen what\nthe rest of the house is like! Let's have a look at the garden first!'\nShe was out of the room in a moment, and ran down stairs--or, at least,\nit wasn't exactly running, but a new invention of hers for getting down\nstairs quickly and easily, as Alice said to herself. She just kept the\ntips of her fingers on the hand-rail, and floated gently down without\neven touching the stairs with her feet; then she floate"
In [4]:
end = text.find('It seems very pretty', start)
In [5]:
poem = text[start:end]
poem
Out[5]:
"JABBERWOCKY\n\n     'Twas brillig, and the slithy toves\n      Did gyre and gimble in the wabe;\n     All mimsy were the borogoves,\n      And the mome raths outgrabe.\n\n     'Beware the Jabberwock, my son!\n      The jaws that bite, the claws that catch!\n     Beware the Jubjub bird, and shun\n      The frumious Bandersnatch!'\n\n     He took his vorpal sword in hand:\n      Long time the manxome foe he sought--\n     So rested he by the Tumtum tree,\n      And stood awhile in thought.\n\n     And as in uffish thought he stood,\n      The Jabberwock, with eyes of flame,\n     Came whiffling through the tulgey wood,\n      And burbled as it came!\n\n     One, two! One, two! And through and through\n      The vorpal blade went snicker-snack!\n     He left it dead, and with its head\n      He went galumphing back.\n\n     'And hast thou slain the Jabberwock?\n      Come to my arms, my beamish boy!\n     O frabjous day! Callooh! Callay!'\n      He chortled in his joy.\n\n     'Twas brillig, and the slithy toves\n      Did gyre and gimble in the wabe;\n     All mimsy were the borogoves,\n      And the mome raths outgrabe.\n\n\n'"
In [6]:
print(poem)
JABBERWOCKY

     'Twas brillig, and the slithy toves
      Did gyre and gimble in the wabe;
     All mimsy were the borogoves,
      And the mome raths outgrabe.

     'Beware the Jabberwock, my son!
      The jaws that bite, the claws that catch!
     Beware the Jubjub bird, and shun
      The frumious Bandersnatch!'

     He took his vorpal sword in hand:
      Long time the manxome foe he sought--
     So rested he by the Tumtum tree,
      And stood awhile in thought.

     And as in uffish thought he stood,
      The Jabberwock, with eyes of flame,
     Came whiffling through the tulgey wood,
      And burbled as it came!

     One, two! One, two! And through and through
      The vorpal blade went snicker-snack!
     He left it dead, and with its head
      He went galumphing back.

     'And hast thou slain the Jabberwock?
      Come to my arms, my beamish boy!
     O frabjous day! Callooh! Callay!'
      He chortled in his joy.

     'Twas brillig, and the slithy toves
      Did gyre and gimble in the wabe;
     All mimsy were the borogoves,
      And the mome raths outgrabe.


'
In [7]:
print(poem.title())
Jabberwocky

     'Twas Brillig, And The Slithy Toves
      Did Gyre And Gimble In The Wabe;
     All Mimsy Were The Borogoves,
      And The Mome Raths Outgrabe.

     'Beware The Jabberwock, My Son!
      The Jaws That Bite, The Claws That Catch!
     Beware The Jubjub Bird, And Shun
      The Frumious Bandersnatch!'

     He Took His Vorpal Sword In Hand:
      Long Time The Manxome Foe He Sought--
     So Rested He By The Tumtum Tree,
      And Stood Awhile In Thought.

     And As In Uffish Thought He Stood,
      The Jabberwock, With Eyes Of Flame,
     Came Whiffling Through The Tulgey Wood,
      And Burbled As It Came!

     One, Two! One, Two! And Through And Through
      The Vorpal Blade Went Snicker-Snack!
     He Left It Dead, And With Its Head
      He Went Galumphing Back.

     'And Hast Thou Slain The Jabberwock?
      Come To My Arms, My Beamish Boy!
     O Frabjous Day! Callooh! Callay!'
      He Chortled In His Joy.

     'Twas Brillig, And The Slithy Toves
      Did Gyre And Gimble In The Wabe;
     All Mimsy Were The Borogoves,
      And The Mome Raths Outgrabe.


'
In [8]:
poem.count('the')
Out[8]:
15
In [9]:
print(poem.replace('the', 'XXX'))
JABBERWOCKY

     'Twas brillig, and XXX slithy toves
      Did gyre and gimble in XXX wabe;
     All mimsy were XXX borogoves,
      And XXX mome raths outgrabe.

     'Beware XXX Jabberwock, my son!
      The jaws that bite, XXX claws that catch!
     Beware XXX Jubjub bird, and shun
      The frumious Bandersnatch!'

     He took his vorpal sword in hand:
      Long time XXX manxome foe he sought--
     So rested he by XXX Tumtum tree,
      And stood awhile in thought.

     And as in uffish thought he stood,
      The Jabberwock, with eyes of flame,
     Came whiffling through XXX tulgey wood,
      And burbled as it came!

     One, two! One, two! And through and through
      The vorpal blade went snicker-snack!
     He left it dead, and with its head
      He went galumphing back.

     'And hast thou slain XXX Jabberwock?
      Come to my arms, my beamish boy!
     O frabjous day! Callooh! Callay!'
      He chortled in his joy.

     'Twas brillig, and XXX slithy toves
      Did gyre and gimble in XXX wabe;
     All mimsy were XXX borogoves,
      And XXX mome raths outgrabe.


'

Find palindromic words in poem if any

In [10]:
poem = poem.lower()

We need to remove the punctuation characters to only leave plain text:

In [11]:
import string
string.punctuation
Out[11]:
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
In [12]:
poem  = poem.translate(dict.fromkeys(map(ord, string.punctuation)))
poem
Out[12]:
'jabberwocky\n\n     twas brillig and the slithy toves\n      did gyre and gimble in the wabe\n     all mimsy were the borogoves\n      and the mome raths outgrabe\n\n     beware the jabberwock my son\n      the jaws that bite the claws that catch\n     beware the jubjub bird and shun\n      the frumious bandersnatch\n\n     he took his vorpal sword in hand\n      long time the manxome foe he sought\n     so rested he by the tumtum tree\n      and stood awhile in thought\n\n     and as in uffish thought he stood\n      the jabberwock with eyes of flame\n     came whiffling through the tulgey wood\n      and burbled as it came\n\n     one two one two and through and through\n      the vorpal blade went snickersnack\n     he left it dead and with its head\n      he went galumphing back\n\n     and hast thou slain the jabberwock\n      come to my arms my beamish boy\n     o frabjous day callooh callay\n      he chortled in his joy\n\n     twas brillig and the slithy toves\n      did gyre and gimble in the wabe\n     all mimsy were the borogoves\n      and the mome raths outgrabe\n\n\n'
In [13]:
words = poem.split()
words[:10]
Out[13]:
['jabberwocky',
 'twas',
 'brillig',
 'and',
 'the',
 'slithy',
 'toves',
 'did',
 'gyre',
 'and']
In [14]:
def is_palindrome(word):
    return word == word[::-1]
In [15]:
{word for word in words if is_palindrome(word)}
Out[15]:
{'did', 'o'}

Top 10 most frequent words

In [16]:
import collections
In [17]:
poem_counter = collections.Counter(words)
In [18]:
poem_counter.most_common(10)
Out[18]:
[('the', 19),
 ('and', 14),
 ('he', 7),
 ('in', 6),
 ('jabberwock', 3),
 ('my', 3),
 ('through', 3),
 ('twas', 2),
 ('brillig', 2),
 ('slithy', 2)]
In [19]:
poem_counter.items()
Out[19]:
dict_items([('jabberwocky', 1), ('twas', 2), ('brillig', 2), ('and', 14), ('the', 19), ('slithy', 2), ('toves', 2), ('did', 2), ('gyre', 2), ('gimble', 2), ('in', 6), ('wabe', 2), ('all', 2), ('mimsy', 2), ('were', 2), ('borogoves', 2), ('mome', 2), ('raths', 2), ('outgrabe', 2), ('beware', 2), ('jabberwock', 3), ('my', 3), ('son', 1), ('jaws', 1), ('that', 2), ('bite', 1), ('claws', 1), ('catch', 1), ('jubjub', 1), ('bird', 1), ('shun', 1), ('frumious', 1), ('bandersnatch', 1), ('he', 7), ('took', 1), ('his', 2), ('vorpal', 2), ('sword', 1), ('hand', 1), ('long', 1), ('time', 1), ('manxome', 1), ('foe', 1), ('sought', 1), ('so', 1), ('rested', 1), ('by', 1), ('tumtum', 1), ('tree', 1), ('stood', 2), ('awhile', 1), ('thought', 2), ('as', 2), ('uffish', 1), ('with', 2), ('eyes', 1), ('of', 1), ('flame', 1), ('came', 2), ('whiffling', 1), ('through', 3), ('tulgey', 1), ('wood', 1), ('burbled', 1), ('it', 2), ('one', 2), ('two', 2), ('blade', 1), ('went', 2), ('snickersnack', 1), ('left', 1), ('dead', 1), ('its', 1), ('head', 1), ('galumphing', 1), ('back', 1), ('hast', 1), ('thou', 1), ('slain', 1), ('come', 1), ('to', 1), ('arms', 1), ('beamish', 1), ('boy', 1), ('o', 1), ('frabjous', 1), ('day', 1), ('callooh', 1), ('callay', 1), ('chortled', 1), ('joy', 1)])

Words that appear exactly twice.

In [20]:
[(k, v) for (k, v) in poem_counter.items() if v==2]
Out[20]:
[('twas', 2),
 ('brillig', 2),
 ('slithy', 2),
 ('toves', 2),
 ('did', 2),
 ('gyre', 2),
 ('gimble', 2),
 ('wabe', 2),
 ('all', 2),
 ('mimsy', 2),
 ('were', 2),
 ('borogoves', 2),
 ('mome', 2),
 ('raths', 2),
 ('outgrabe', 2),
 ('beware', 2),
 ('that', 2),
 ('his', 2),
 ('vorpal', 2),
 ('stood', 2),
 ('thought', 2),
 ('as', 2),
 ('with', 2),
 ('came', 2),
 ('it', 2),
 ('one', 2),
 ('two', 2),
 ('went', 2)]

Trigrams

All possible sequences of 3 words in the poem.

In [21]:
list(zip(words[:], words[1:], words[2:]))[:10]
Out[21]:
[('jabberwocky', 'twas', 'brillig'),
 ('twas', 'brillig', 'and'),
 ('brillig', 'and', 'the'),
 ('and', 'the', 'slithy'),
 ('the', 'slithy', 'toves'),
 ('slithy', 'toves', 'did'),
 ('toves', 'did', 'gyre'),
 ('did', 'gyre', 'and'),
 ('gyre', 'and', 'gimble'),
 ('and', 'gimble', 'in')]
In [22]:
import itertools as it
In [23]:
def window(x, n):
    """Sliding window of size n from iterable x."""
    s = (it.islice(x, i, None) for i in range(n))
    return zip(*s)
In [24]:
list(window(words, 3))[:10]
Out[24]:
[('jabberwocky', 'twas', 'brillig'),
 ('twas', 'brillig', 'and'),
 ('brillig', 'and', 'the'),
 ('and', 'the', 'slithy'),
 ('the', 'slithy', 'toves'),
 ('slithy', 'toves', 'did'),
 ('toves', 'did', 'gyre'),
 ('did', 'gyre', 'and'),
 ('gyre', 'and', 'gimble'),
 ('and', 'gimble', 'in')]

Using Regular Expressions

Find all words with a sequence of two or more identical letters e.g. “look”

In [25]:
import re
In [26]:
regex = re.compile(r'(\w*(\w)\2+\w*)', re.IGNORECASE | re.MULTILINE)
In [27]:
for match in regex.finditer(poem):
    print(match.group(2), match.group(1))
b jabberwocky
l brillig
l all
b jabberwock
o took
e tree
o stood
f uffish
o stood
b jabberwock
f whiffling
o wood
b jabberwock
o callooh
l callay
l brillig
l all

Convert the identical sequences to uppercase

In [28]:
def f(match):
    word, letter = match.groups()
    return word.replace(letter, letter.upper())

print(regex.sub(f, poem))
jaBBerwocky

     twas briLLig and the slithy toves
      did gyre and gimble in the wabe
     aLL mimsy were the borogoves
      and the mome raths outgrabe

     beware the jaBBerwock my son
      the jaws that bite the claws that catch
     beware the jubjub bird and shun
      the frumious bandersnatch

     he tOOk his vorpal sword in hand
      long time the manxome foe he sought
     so rested he by the tumtum trEE
      and stOOd awhile in thought

     and as in uFFish thought he stOOd
      the jaBBerwock with eyes of flame
     came whiFFling through the tulgey wOOd
      and burbled as it came

     one two one two and through and through
      the vorpal blade went snickersnack
     he left it dead and with its head
      he went galumphing back

     and hast thou slain the jaBBerwock
      come to my arms my beamish boy
     o frabjous day callOOh caLLay
      he chortled in his joy

     twas briLLig and the slithy toves
      did gyre and gimble in the wabe
     aLL mimsy were the borogoves
      and the mome raths outgrabe



Natural language processing

If you intend to perform statistical analysis on natural language, you should probably use NLTK to pre-process the text instead of using string methods and regular expressions. For example, a simple challenge is to first parse the paragraph below into sentences, then parse each sentence into words.

Paragraph from random Pubmed abstract.

In [29]:
para = """When compared with the control group no significant associations were found for the NS-PEecl group after adjustment of confounding variables. For the S-PEecl group, antiβ2GP1 IgG (OR 16.91, 95% CI 3.71-77.06) was associated, as well as age, obesity, smoking and multiparity. Antiβ2GP1-domain I IgG were associated with aCL, antiβ2GP1 and aPS/PT IgG in the three groups. aPS/PT IgG were associated with aCL IgG, and aPS/PT IgM were associated with aCL and antiβ2GP1 IgM in the three groups CONCLUSION: S-PEecl is a distinct entity from NS-PEecl and is mainly associated with the presence of antiβ2GP1 IgG. Antiβ2GP1 domain I correlate with other aPL IgG tests, and aPS/PT may be promising in patients in which LA tests cannot be interpreted."""

Naive splitting of sentences as anything separated by ., ! or ?

In [30]:
sep = re.compile(r'[\?\!\.]')
In [31]:
ss = sep.split(para)
In [32]:
for i, s in enumerate(ss, 1):
    print(i, ':', s, end='\n\n')
1 : When compared with the control group no significant associations were found for the NS-PEecl group after adjustment of confounding variables

2 :  For the S-PEecl group, antiβ2GP1 IgG (OR 16

3 : 91, 95% CI 3

4 : 71-77

5 : 06) was associated, as well as age, obesity, smoking and multiparity

6 :  Antiβ2GP1-domain I IgG were associated with aCL, antiβ2GP1 and aPS/PT IgG in the three groups

7 :  aPS/PT IgG were associated with aCL IgG, and aPS/PT IgM were associated with aCL and antiβ2GP1 IgM in the three groups CONCLUSION: S-PEecl is a distinct entity from NS-PEecl and is mainly associated with the presence of antiβ2GP1 IgG

8 :  Antiβ2GP1 domain I correlate with other aPL IgG tests, and aPS/PT may be promising in patients in which LA tests cannot be interpreted

9 :

Using NLTK

In [33]:
import nltk
In [34]:
ss_nltk = nltk.sent_tokenize(para)
In [35]:
for i, s in enumerate(ss_nltk, 1):
    print(i, ':', s, end='\n\n')
1 : When compared with the control group no significant associations were found for the NS-PEecl group after adjustment of confounding variables.

2 : For the S-PEecl group, antiβ2GP1 IgG (OR 16.91, 95% CI 3.71-77.06) was associated, as well as age, obesity, smoking and multiparity.

3 : Antiβ2GP1-domain I IgG were associated with aCL, antiβ2GP1 and aPS/PT IgG in the three groups.

4 : aPS/PT IgG were associated with aCL IgG, and aPS/PT IgM were associated with aCL and antiβ2GP1 IgM in the three groups CONCLUSION: S-PEecl is a distinct entity from NS-PEecl and is mainly associated with the presence of antiβ2GP1 IgG.

5 : Antiβ2GP1 domain I correlate with other aPL IgG tests, and aPS/PT may be promising in patients in which LA tests cannot be interpreted.

Naive parsing of the second sentence into words

In [36]:
s = ss_nltk[1]
s
Out[36]:
'For the S-PEecl group, antiβ2GP1 IgG (OR 16.91, 95% CI 3.71-77.06) was associated, as well as age, obesity, smoking and multiparity.'
In [37]:
# remove punctuation and split on whit space
table = dict.fromkeys(map(ord, string.punctuation))
s.translate(table).split()
Out[37]:
['For',
 'the',
 'SPEecl',
 'group',
 'antiβ2GP1',
 'IgG',
 'OR',
 '1691',
 '95',
 'CI',
 '3717706',
 'was',
 'associated',
 'as',
 'well',
 'as',
 'age',
 'obesity',
 'smoking',
 'and',
 'multiparity']

Using NLTK

In [38]:
text = nltk.word_tokenize(s)
text
Out[38]:
['For',
 'the',
 'S-PEecl',
 'group',
 ',',
 'antiβ2GP1',
 'IgG',
 '(',
 'OR',
 '16.91',
 ',',
 '95',
 '%',
 'CI',
 '3.71-77.06',
 ')',
 'was',
 'associated',
 ',',
 'as',
 'well',
 'as',
 'age',
 ',',
 'obesity',
 ',',
 'smoking',
 'and',
 'multiparity',
 '.']

NLTK is a rich library for natural language processing

See http://www.nltk.org for details.

Tag tokens with part-of-speech labels

In [39]:
tagged_text = nltk.pos_tag(text)
tagged_text
Out[39]:
[('For', 'IN'),
 ('the', 'DT'),
 ('S-PEecl', 'NNP'),
 ('group', 'NN'),
 (',', ','),
 ('antiβ2GP1', 'RB'),
 ('IgG', 'NNP'),
 ('(', '('),
 ('OR', 'NNP'),
 ('16.91', 'CD'),
 (',', ','),
 ('95', 'CD'),
 ('%', 'NN'),
 ('CI', 'NNP'),
 ('3.71-77.06', 'CD'),
 (')', ')'),
 ('was', 'VBD'),
 ('associated', 'VBN'),
 (',', ','),
 ('as', 'RB'),
 ('well', 'RB'),
 ('as', 'IN'),
 ('age', 'NN'),
 (',', ','),
 ('obesity', 'NN'),
 (',', ','),
 ('smoking', 'NN'),
 ('and', 'CC'),
 ('multiparity', 'NN'),
 ('.', '.')]
In [40]:
s
Out[40]:
'For the S-PEecl group, antiβ2GP1 IgG (OR 16.91, 95% CI 3.71-77.06) was associated, as well as age, obesity, smoking and multiparity.'

A simplistic way to pick up nouns

In [41]:
[w for w, t in tagged_text if t.startswith('N')]
Out[41]:
['S-PEecl',
 'group',
 'IgG',
 'OR',
 '%',
 'CI',
 'age',
 'obesity',
 'smoking',
 'multiparity']

String formatting

Selection

In [42]:
import math
In [43]:
stuff = ('bun', 'shoe', ['bee', 'door'], 2, math.pi, 0.05)
In [44]:
'One: {}, Two {}'.format(*stuff)
Out[44]:
'One: bun, Two shoe'
In [45]:
'One: {0}, Two {1}'.format(*stuff)
Out[45]:
'One: bun, Two shoe'
In [46]:
'One: {1}, Two {1}'.format(*stuff)
Out[46]:
'One: shoe, Two shoe'
In [47]:
'One: {0}, Two {2[1]}'.format(*stuff)
Out[47]:
'One: bun, Two door'

Formatting

In [48]:
'One: {0:^10s}, Two {1:_>15s}'.format(*stuff)
Out[48]:
'One:    bun    , Two ___________shoe'
In [49]:
'One: {3}, Two {4}'.format(*stuff)
Out[49]:
'One: 2, Two 3.141592653589793'
In [50]:
'One: {3:+10d}, Two {4:.4f}'.format(*stuff)
Out[50]:
'One:         +2, Two 3.1416'
In [51]:
'One: {3:04d}, Two {4:.4g}'.format(*stuff)
Out[51]:
'One: 0002, Two 3.142'
In [52]:
'One: {3:.4e}, Two {4:.4e}'.format(*stuff)
Out[52]:
'One: 2.0000e+00, Two 3.1416e+00'
In [53]:
'One: {5:.2%}, Two {5:f}'.format(*stuff)
Out[53]:
'One: 5.00%, Two 0.050000'

Old style formatting

In [54]:
'%s, %s, %a, %d, %.4f, %.2f' % stuff
Out[54]:
"bun, shoe, ['bee', 'door'], 2, 3.1416, 0.05"

Formatting numpy arrays

In [55]:
import numpy as np
In [56]:
x = np.arange(1, 13).reshape(3,4)
x
Out[56]:
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])
In [57]:
np.set_printoptions(formatter={'int': lambda x: '%8.2f' % x})
In [58]:
x
Out[58]:
array([[    1.00,     2.00,     3.00,     4.00],
       [    5.00,     6.00,     7.00,     8.00],
       [    9.00,    10.00,    11.00,    12.00]])
In [59]:
np.set_printoptions()
In [60]:
x
Out[60]:
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])