The Great Library Heist
Solutions
The crime
Case briefing
Late last night, someone broke into the library and went mad!
- An unknown number of books were taken from their shelves.
- Their spines were cut and every page torn loose.
- The pieces were shuffled together and dumped into a single evidence box.
- The covers and title pages are gone.
- Nobody knows which scrap came from which book, how many books were taken, or what the books are.
Your assignment
You have no witnesses and no confession. All you have is the words the books left behind. Your goal is to:
- Work out how many books were stolen.
- Return every scrap to its book.
- Name each book.
Rules of the investigation
Rules
- Your research team is your detective squad.
- Every claim needs evidence you produced in R.
- You must keep your own evidence log (quarto file). You’ll be asked to divide up the work later and everyone should be at the same point. Write notes in here as well, you’ll turn it in at the end of the investigation.
- No outside tips. No search engines or chatbots on the scraps. That’s tampering with evidence.
- You can talk to the tech team (aka google, your notes or our helper page) to ask for help with code functions.
- This is a friendly competition, no rules against helping other squads. But… there has to be a winner.
The evidence box will self destruct in T-1 hour.
Clues
- The lead detectives (Dr. D and Amy) have 6 clues available for you during this investigation.
- Clue #1 is already provided in these instructions. Bring your answer and your code to one of the lead detectives.
- If it’s right, you get a lead for your next step, and the next Clue.
- Each Clue correctly solved earns your squad 1 point toward your homework grade.
- First team to correctly name the books gets a bonus point.
- An answer without cold hard code evidence to back it doesn’t count.
The investigation
Phase 0: Name your squad
Sign your squad into the crime scene by logging your name with the lead investigators.
Phase 1: Secure the scene
Before touching anything, take inventory of the evidence box.
- What is a single row? What columns came with it?
- How are scraps labeled?
How many exhibits are in the evidence box, and what are the fewest and the most scraps any one exhibit holds? What part of a book do you think an exhibit is?
Key: Clue 1 Reasonable code includes:
(exhibit_sizes <- dta |> count(exhibit))# A tibble: 159 × 2
exhibit n
<chr> <int>
1 EX-103 223
2 EX-118 438
3 EX-139 278
4 EX-140 84
5 EX-159 370
6 EX-170 483
7 EX-176 353
8 EX-178 342
9 EX-182 263
10 EX-183 416
# ℹ 149 more rows
nrow(exhibit_sizes)[1] 159
# A tibble: 3 × 2
exhibit n
<chr> <int>
1 EX-742 39
2 EX-628 60
3 EX-573 64
# A tibble: 3 × 2
exhibit n
<chr> <int>
1 EX-313 655
2 EX-989 593
3 EX-201 586
Clue #1 answer: 159 exhibits, holding from 39 to 655 scraps each. Accept any reasonable guess for what an exhibit is (a chapter, a section, a stack of pages).
Lead: An exhibit is one chapter of one book. Every scrap in an exhibit comes from the same book.
Phase 2: Dust for prints
A whole scrap is too big to compare. Break the evidence into the smallest pieces that still carry a fingerprint: individual words.
- Split every line so each row holds one word, still tagged with its exhibit number.
- Some filler words appear everywhere (“the,” “and,” “of”). They don’t provide us information, so clear them out.
After the filler is cleared, which word appears most often in the whole box, and how many times?
Key: Clue 2. Choices made: lowercase (unnest_tokens() default), tidytext::stop_words as the filler list.
tidy_words <- dta |>
unnest_tokens(word, text) |>
anti_join(stop_words, by = "word")
word_totals <- tidy_words |> count(word, sort = TRUE)
head(word_totals, 10)# A tibble: 10 × 2
word n
<chr> <int>
1 time 1027
2 night 706
3 joe 688
4 day 647
5 captain 636
6 looked 617
7 hand 593
8 _nautilus_ 507
9 eyes 502
10 miss 447
Clue #2 answer: “time” is used 1027 times.
Lead: Count each word within each exhibit, not across the whole box. Then rearrange the counts into a grid: one row per exhibit, one column per word, the count in each cell. This grid is the format a profiler needs.
Phase 3: Build the evidence board
Pin every exhibit to the wall and record how often each word shows up in it.
How many rows and columns does your evidence board have, and what percent of its cells are empty?
Key: Clue 3
<<DocumentTermMatrix (documents: 159, terms: 19640)>>
Non-/sparse entries: 106994/3015766
Sparsity : 97%
Maximal term length: 19
Weighting : term frequency (tf)
Clue #3 Answer: 159 rows, 19,380 columns, 97% sparsity.
Lead: the profiler is the topic model from class, LDA(). The grid goes in, along with the number of sources. The seed goes in control, and each squad member should use a different seed (but consistently across runs). Run it several times, each with a different number of sources, and compare the runs.
Phase 4: Call in the profiler, size up the crew
You need an expert who has never seen these books.
- The profiler assumes each exhibit is a blend of a fixed number of hidden sources, each with its own favorite vocabulary.
- It works out both from the evidence board alone.
- It needs one thing from you: how many sources to look for. Nobody knows that yet.
- Run the profiler several times, each with a different number of sources.
- The profiler is slow and can get side tracked. Divide the work across your squad to check for consistency in profiler results.
- Ensure reproducibility so your profile can be reproduced in court.
How many books were vandalized? Defend your number with evidence comparing at least three different proposed sources and two different profilers.
Key: Clue 4. The minimum Clue #4 asks for: 3, 4 and 5 sources, each fit with two different seeds. This assumes 4 books, so shift the range if the book list changes.
Top 8 words per source, for each run:
# A tibble: 3 × 2
topic top_words
<int> <chr>
1 1 time, night, van, helsing, day, dear, hand, door
2 2 captain, _nautilus_, sea, nemo, ned, water, conseil, land
3 3 joe, miss, time, pip, looked, herbert, don’t, hand
# A tibble: 3 × 2
topic top_words
<int> <chr>
1 1 time, night, van, helsing, day, door, hand, eyes
2 2 joe, miss, time, looked, don’t, pip, herbert, hand
3 3 captain, _nautilus_, sea, nemo, ned, conseil, water, land
# A tibble: 4 × 2
topic top_words
<int> <chr>
1 1 time, van, helsing, night, dear, lucy, hand, day
2 2 captain, _nautilus_, sea, nemo, ned, conseil, land, water
3 3 joe, miss, time, pip, looked, herbert, don’t, hand
4 4 people, time, martians, night, black, road, water, heard
# A tibble: 4 × 2
topic top_words
<int> <chr>
1 1 people, martians, time, black, road, night, brother, water
2 2 joe, miss, time, pip, looked, don’t, herbert, hand
3 3 captain, _nautilus_, sea, nemo, ned, conseil, water, land
4 4 time, van, night, helsing, dear, lucy, hand, day
# A tibble: 5 × 2
topic top_words
<int> <chr>
1 1 van, time, helsing, night, dear, lucy, hand, mina
2 2 captain, _nautilus_, sea, nemo, ned, conseil, land, water
3 3 joe, miss, pip, time, looked, don’t, wemmick, havisham
4 4 time, night, herbert, found, looked, round, day, heard
5 5 people, martians, black, time, night, road, pit, smoke
# A tibble: 5 × 2
topic top_words
<int> <chr>
1 1 martians, people, black, time, road, night, brother, water
2 2 joe, miss, time, pip, looked, herbert, havisham, estella
3 3 captain, _nautilus_, sea, nemo, ned, conseil, land, water
4 4 time, van, helsing, night, lucy, dear, poor, day
5 5 wemmick, jaggers, time, door, looked, night, don’t, found
Clue #4 Answer: There are 4 books. With more than 4 sources, one book’s vocabulary is split across sources. Judge a squad’s comparison of runs, not just its number.
Lead: All of the books are public-domain novels from the 1800s. Sometimes a source mixes vocabularies from more than one book. The four vandalized books are among these eight suspects:
| Title | Author | Year | Known for |
|---|---|---|---|
| Dracula | Bram Stoker | 1897 | Gothic horror |
| Frankenstein | Mary Shelley | 1818 | A scientist’s creation |
| Great Expectations | Charles Dickens | 1861 | A Victorian coming-of-age story |
| Jane Eyre | Charlotte Brontë | 1847 | A governess at a mysterious manor |
| Moby-Dick | Herman Melville | 1851 | A whale hunt |
| Treasure Island | Robert Louis Stevenson | 1883 | Pirates and buried treasure |
| Twenty Thousand Leagues under the Sea | Jules Verne | 1870 | An undersea voyage |
| The war of the worlds | H. G. Wells | 1898 | An alien invasion |
Phase 5: Interrogate the suspects
Using your chosen number, the profiler scores every word under every source: how strongly that word belongs there.
- Pull out each source’s top 10 words.
- Plot all sources side by side.
- Look for places, objects, and activities. What kind of story does each vocabulary tell?
Name every vandalized book. Cite the words that convinced you.
Key: Clue 5
heist_lda <- lda4a # the run with the true number of books
top_words <- tidy(heist_lda, matrix = "beta") |>
group_by(topic) |>
slice_max(beta, n = 10) |>
ungroup() |>
arrange(topic, desc(beta))
top_words |>
mutate(term = reorder_within(term, beta, topic)) |>
ggplot(aes(beta, term, fill = factor(topic))) +
geom_col(show.legend = FALSE) +
facet_wrap(~ topic, scales = "free") +
scale_y_reordered()
# the book behind each source, in source order (seed 1234)
source_names <- c("Dracula", "Twenty Thousand Leagues under the Sea",
"Great Expectations", "The war of the worlds")Clue #5 answer: The correct titles are: “Dracula”, “Twenty Thousand Leagues under the Sea”, “Great Expectations”, “The war of the worlds”.
Lead: Each exhibit could be attributed to multiple sources. Explore the share of the exhibit that comes from each source. Using functions like slice_max() within each exhibit could be helpful here.
Lead: Here is some code to help you name your sources:
Lead: To get the exhibits with the top X strongest matches you would use code similar to the following:
Phase 6: Attribute the exhibits to the most likely source
The profiler also scores every exhibit: what share of it seems to come from each source.
- For each exhibit, find the source with the largest share. That’s your best match.
Plot the shares of the five best-matched and five worst-matched exhibits. Which exhibits are hardest to place, and which sources are they split between?
Key: Clue 6
heist_gamma <- tidy(heist_lda, matrix = "gamma")
# each exhibit's best-matching source
best_match <- heist_gamma |>
group_by(document) |>
slice_max(gamma) |>
ungroup()
# the lead's naming code, filled in with the correct names
best_match <- best_match |>
mutate(book = factor(topic, levels = 1:4, labels = source_names))

Clue #6 answer: Look for bars that are pretty evenly split between 2 or 3 sources.
Lead: An evidence key that has the correct book for each exhibit has been provided for you in Jupyterhub under heist_answer_key.rds. One way to open this key is to use the following code:
Lead: You can attach exhibit’s true book by using a left_join by c(document = "exhibit").
Lead: An exhibit is wrongly classified when the book it was returned to differs from its true book (the off-diagonal cells of a two-way table).
Lead: To view the full scrap text you can use:
knitr::kable(col.names = "Scrap", align = "l") |>
column_spec(1, width = "35em")Phase 7: The reveal
You have the evidence key: the true book for every exhibit.
- What percent of exhibits did you get correct?
- Pick an exhibit that was returned to the wrong book. Filter the evidence box for that exhibit and read some of its scraps.
- Why do you think the profiler put it with the other book?
Key: Phase 7
Dracula
Dracula 24
Great Expectations 1
The war of the worlds 0
Twenty Thousand Leagues under the Sea 0
Twenty Thousand Leagues under the Sea
Dracula 0
Great Expectations 0
The war of the worlds 0
Twenty Thousand Leagues under the Sea 46
Great Expectations
Dracula 0
Great Expectations 57
The war of the worlds 0
Twenty Thousand Leagues under the Sea 0
The war of the worlds
Dracula 3
Great Expectations 1
The war of the worlds 27
Twenty Thousand Leagues under the Sea 0
wrong_exhibits <- checked |> filter(title != book)
wrong_exhibits# A tibble: 5 × 6
document topic gamma book title chapter
<chr> <int> <dbl> <fct> <chr> <int>
1 EX-246 4 0.661 The war of the worlds Great Expectations 54
2 EX-607 4 0.786 The war of the worlds Dracula 7
3 EX-694 4 0.755 The war of the worlds Dracula 1
4 EX-807 1 0.530 Dracula Great Expectations 56
5 EX-826 4 0.592 The war of the worlds Dracula 6
Read one misassigned exhibit: filter the evidence box for it and look at some of its scraps.
wrong_exhibits$document[1][1] "EX-246"
dta |>
filter(exhibit == wrong_exhibits$document[1]) |>
select(text) |>
head(10) |>
knitr::kable(col.names = "Scrap", align = "l") |>
column_spec(1, width = "35em")| Scrap |
|---|
| we were rising and falling in a troubled wake of water. The look-out |
| “But for your face I should think you were a little despondent,” said |
| charge was the least anxious of the party. It was very likely that the |
| come, and Magwitch could go, and nobody’s head would be troubled about |
| had led accounted for it) that he was the least anxious of any of us. |
| cordially, and neither Herbert’s eyes nor mine were quite dry, when I |
| No. I had thought about that, while we had been there side by side. No. |
| the last o’ many times, and I don’t ask no more.” |
| be much heightened when he heard that it had stockings on. Probably, it |
| accord that grace to my two friends. |
Answer: 5 exhibits misattributed. For the reading, accept any explanation grounded in the scraps they read, such as scraps with few distinctive words, or names, places or events that also fit the other book.
Case Closed - Report out
By end of week the following should be submitted to our weekly Google folder.
Your most completed quarto file for the team. It does not have to render, but it should contain the majority of the evidence and findings. One file per team only. Be sure your team name is in the file name.
A personal reflection on the activity.
- How far did your squad get?
- What did you learn and what challenges did you encounter?
- How did your squad work through obstacles?
- How did you contribute to your squads success? What skillsets did you provide?
