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 →