The Sherlock Holmes Stories Corpus

The character of Sherlock Holmes first appeared in Arthur Conan Doyle’s novel A Study in Scarlet in 1887. A second novel, The Sign of Four, followed in 1890. But the detective really got to be a hit as the protagonist of a series of stories in The Strand magazine, starting in 1891. Conan Doyle tried to kill off the character in 1893, but was persuaded by fans and a lot of money from his publisher to bring him back. The stories continued to appear in The Strand until 1927. There were two more novels as well (also published in installments in The Strand).

The short stories are the ultimate source of the data we glimpsed in my table of occurrences of “data,” “fact,” “theory,” and “clue.” But let’s look a little more carefully at the data sandwich here. For my demonstration example, I made use of the conveniently-packaged dataset created by Taylor Arnold and Lauren Tilton as an example in chapter 9 of Humanities Data in R (that link goes to a digital copy of the book available to you via the RU Library). The data itself can be found in their online supplement, in the subdirectory data/ch09/holmes-anno. Download and unzip the data. The following R code reads it in:

library(tidyverse)
sh_anno <- tibble(
    filename=Sys.glob("humanitiesDataInR/data/ch09/holmes_anno/*.Rds")) |>
  # these files are serialized R data structures; load with readRDS
  mutate(anno=map(filename, readRDS))

# extract token information
sh_tokens <- sh_anno |>
  mutate(token=map(anno, "token")) |>
  # use filename for story identifier
  transmute(story=str_extract(filename, "\\d\\d_.*\\.Rds$"), token) |>
  unnest(token)

The resulting table starts like this:

head(sh_tokens)
## # A tibble: 6 × 12
##   story sentence id    word  lemma CharacterOffset… CharacterOffset… POS   NER  
##   <chr>    <int> <fct> <chr> <chr> <chr>            <chr>            <chr> <chr>
## 1 01_a…        1 1     To    to    0                2                TO    O    
## 2 01_a…        1 2     Sher… Sher… 3                11               NNP   PERS…
## 3 01_a…        1 3     Holm… Holm… 12               18               NNP   PERS…
## 4 01_a…        1 4     she   she   19               22               PRP   O    
## 5 01_a…        1 5     is    be    23               25               VBZ   O    
## 6 01_a…        1 6     alwa… alwa… 26               32               RB    O    
## # … with 3 more variables: Speaker <chr>, NormalizedNER <chr>, Timex <chr>

What do we have here? If you squint you can see what looks like the start of a story as you run down the word column:

sh_tokens |> select(word) |> slice(1:9)
## # A tibble: 9 × 1
##   word    
##   <chr>   
## 1 To      
## 2 Sherlock
## 3 Holmes  
## 4 she     
## 5 is      
## 6 always  
## 7 THE     
## 8 woman   
## 9 .

And in fact each row represents one word of one story, in order. There are 546735 rows in all. This is not the most efficient way to store textual data, but it is a very convenient form for analyzing patterns in word use.1 (Notice that the period ending the sentence has been treated as a word.) The columns give some information about each word, all extracted by Tilton and Arnold using the CoreNLP natural-language processing pipeline, whose R interface they explain in some detail.

The only columns I then used in the first class were the filename identifying the story and the lemma column. A lemma is a dictionary entry; two words which would go under the same dictionary entry, like “fact” and “facts” or “say” and “said”—or “data” and “datum”—have the same lemma. When we are counting words, it sometimes makes more sense to count lemmas when we can get them, although sometimes it won’t be worth the bother, and sometimes it is better not to collapse pairs like “fact” and “facts.”2

There is a lot more linguistic information here produced by CoreNLP—information about grammar and meaning as well as a list of words. In addition to the other columns of the sh_tokens table, there are other components of sh_anno too, whose names can be retrieved as follows:

names(sh_anno$anno[[1]])
## [1] "token"            "parse"            "basicDep"         "collapsedDep"    
## [5] "collapsedProcDep" "coref"            "sentiment"

I used only the token element here. parse is an automatically inferred syntax tree for each sentence in the corpus:

# parse tree for 1st sentence of 1st story
sh_anno$anno[[1]]$parse[[1]] |> cat()
## (ROOT
##   (S
##     (PP (TO To)
##       (NP (NNP Sherlock) (NNP Holmes)))
##     (NP (PRP she))
##     (VP (VBZ is)
##       (ADVP (RB always))
##       (NP (DT THE) (NN woman)))
##     (. .)))

The other elements are the results of CoreNLP’s inferences about the referential relations among the words. You can read more about that in Tilton and Arnold; in our course we won’t do much with this kind of sophisticated natural-language processing.

You should know that even the apparently simple process of splitting a string into words, which is called “tokenization,” can get gnarly. No algorithm gives perfect performance, since there are genuine ambiguities in spelling and punctuation which only the meaning can resolve (and sometimes not even then). And in real life, accented and other non-English characters often trip things up through character-encoding issues.

Now, where did those texts come from that Arnold and Tilton used? They say they used texts from Project Gutenberg, which is a wonderful website that hosts “plain texts” of books that are in the public domain. Just what a “plain text” is is something we’ll discuss later in the semester; for the moment, let’s just say it refers to a format that includes only the sequence of characters in a text, without any information about their physical appearance or arrangement: no pages, no fonts, no illustrations. In computer-programming terms each story is initially just a string, which is the appropriate input to the CoreNLP pipeline.

Plain texts on Project Gutenberg are typically keyed in by hand: someone has taken a book and copied over the text into a computer file. (Someone else proofreads.) From the textual-criticism perspective, Project Gutenberg therefore can be said to produce a new edition of each work it hosts. Here is some information included in the Project Gutenberg text of The Adventures of Sherlock Holmes, a volume collecting the first dozen stories:

    Title: The Adventures of Sherlock Holmes

    Author: Arthur Conan Doyle

    Release Date: November 29, 2002 [eBook #1661]
    [Most recently updated: May 20, 2019]

    Language: English

    Character set encoding: UTF-8

    Produced by: an anonymous Project Gutenberg volunteer and Jose Menendez

We don’t have information about which particular edition of Adventures served as the copy text (the basis) of this version.3 So from a textual-criticism perspective, this source is less than ideal. Unfortunately, there is no better choice. Fortunately, the textual variation among different editions of Conan Doyle’s stories is fairly limited.

There, then, is a Sherlock Holmes short-story dataset. The next thing to do with it before embarking on further study would be to compile appropriate metadata for each story, including date and location of first publication. The next thing after that would be to consider what kind of comparison data would be illuminating: other detective stories? other fiction?


  1. And fortunately R does some space-saving stuff to make it somewhat less wasteful than it might seem.↩︎

  2. And in fact “data” and “datum” are not under the same entry in the Oxford English Dictionary because sometimes “data” is clearly not used as the plural of anything but as a mass noun, as in “This COVID-19 data is very important.”↩︎

  3. More than one version of the book may be involved: the “general information” appended to the end of the text says, “Project Gutenberg-tm eBooks are often created from several printed editions, all of which are confirmed as not protected by copyright in the U.S. unless a copyright notice is included. Thus, we do not necessarily keep eBooks in compliance with any particular paper edition.”↩︎