Character Presence#
Navigation: ← Previous: Word Clouds | Next: Project Overview →
Who is on the page? Mention rates reconstruct plot occupancy without a parser or a spoiler-heavy summary.
Method#
Named-entity models need a downloaded spaCy pipeline and still confuse Miss Bennet with the wrong sister. For a small, famous corpus, curated alias lists are more reliable: Elizabeth / Lizzy / Eliza, Mina / Mrs. Harker, Scrooge, Heathcliff.
Each alias is counted with a word-boundary regex on the page window. The chart is an 8-page rolling mean of mentions per page — a stage-direction track.
import sys
from pathlib import Path
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.express as px
from IPython.display import HTML, display
import warnings
warnings.filterwarnings('ignore')
plt.style.use('seaborn-v0_8-whitegrid')
PROJ_DIR = Path('.').resolve()
if not (PROJ_DIR / 'gutenberg_utils.py').exists():
PROJ_DIR = Path('projects/literary-nlp').resolve()
if str(PROJ_DIR) not in sys.path:
sys.path.insert(0, str(PROJ_DIR))
from gutenberg_utils import (
load_pages, book_catalog, title_of, BOOK_COLORS, BOOKS,
THEMATIC_KEYWORDS, all_stopwords, ensure_nltk_data,
add_vader_sentiment, add_nrc_emotions, NRC_EMOTIONS,
keyword_counts, character_mentions, third_label,
)
def display_plotly(fig):
"""Embed Plotly with CDN JS — fig.show() is blank in Jupyter Book HTML."""
display(HTML(fig.to_html(include_plotlyjs='cdn', full_html=False)))
PAGES = load_pages()
CATALOG = book_catalog()
print(f"Loaded {len(PAGES):,} pages across {PAGES['book_id'].nunique()} books")
Loaded 2,653 pages across 8 books
def character_figure(book_id):
df = character_mentions(PAGES, book_id)
names = [c for c in df.columns if c not in PAGES.columns]
fig = go.Figure()
for name in names:
smooth = df[name].rolling(8, min_periods=1, center=True).mean()
fig.add_trace(go.Scatter(
x=df['progress'] * 100, y=smooth, mode='lines', name=name,
))
fig.update_layout(
title=f'{title_of(book_id)} — character mentions vs progress',
xaxis_title='Progress (%)', yaxis_title='Mentions per page (rolling mean)',
template='plotly_white', height=420, legend=dict(orientation='h', y=-0.25),
)
return fig, names, df
Four plots as occupancy charts#
for book_id in ['dracula', 'wuthering_heights', 'pride_and_prejudice', 'christmas_carol']:
fig, names, _ = character_figure(book_id)
display_plotly(fig)
Who dominates the last third?#
rows = []
for book in BOOKS:
df = character_mentions(PAGES, book['book_id'])
names = [c for c in df.columns if c not in PAGES.columns]
last = df.loc[df['progress'] >= 2 / 3, names].sum()
first = df.loc[df['progress'] < 1 / 3, names].sum()
if last.empty:
continue
rows.append({
'title': book['title'],
'opening lead': first.idxmax() if first.sum() else '—',
'closing lead': last.idxmax() if last.sum() else '—',
'closing mentions': int(last.max()) if last.sum() else 0,
})
pd.DataFrame(rows)
| title | opening lead | closing lead | closing mentions | |
|---|---|---|---|---|
| 0 | Dracula | Lucy | Van Helsing | 264 |
| 1 | Wuthering Heights | Heathcliff | Catherine | 207 |
| 2 | The Time Machine | Time Traveller | Morlocks | 32 |
| 3 | Pride and Prejudice | Darcy | Elizabeth | 281 |
| 4 | Frankenstein | Clerval | Elizabeth | 36 |
| 5 | Alice's Adventures in Wonderland | Alice | Alice | 131 |
| 6 | A Christmas Carol | Scrooge | Scrooge | 103 |
| 7 | The Picture of Dorian Gray | Lord Henry | Dorian | 217 |
What the occupancy charts should show#
Dracula: Jonathan’s journal opens the book; Mina, Van Helsing, and the Count share the hunt in the close.
Wuthering Heights: Heathcliff is a constant; the second-generation names (Hareton, the younger Cathy) rise later.
Pride and Prejudice: Elizabeth is the baseline; Darcy’s mentions should climb after the first proposal / letter stretch.
A Christmas Carol: Scrooge is everywhere; the Ghosts arrive in sequence, Tiny Tim clusters in the Cratchit scenes and the end.
Alias lists are still a compromise. Bob will catch Bob Cratchit and the occasional unrelated bob; creature in Frankenstein is the point of the book and also a generic noun. Read the lines as stage lighting, not as a census.
Navigation: ← Previous: Word Clouds | Next: Project Overview →