Emotion Lexicon#

Navigation: ← Previous: Sentiment Trajectories | Next: Term Frequency →

NRC emotions — joy, fear, sadness, anger, trust, anticipation — as a richer map than a single polarity score.

Method#

The NRC Emotion Lexicon (Mohammad & Turney) tags words with eight basic emotions. We use the nrclex package and keep six: joy, fear, sadness, anger, trust, anticipation. Each page is summarised as affect frequencies (share of emotion-bearing tokens), then smoothed with the same 8-page rolling mean as the sentiment chapter.

Binary polarity in the previous notebook can hide a book that is both joyful and fearful. Gothic fiction often is.

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
emo = add_nrc_emotions(PAGES)
mean_emo = emo.groupby('title')[list(NRC_EMOTIONS)].mean().round(3)
mean_emo
joy fear sadness anger trust anticipation
title
A Christmas Carol 0.189 0.168 0.133 0.099 0.207 0.204
Alice's Adventures in Wonderland 0.147 0.140 0.133 0.120 0.214 0.246
Dracula 0.153 0.170 0.139 0.106 0.227 0.205
Frankenstein 0.168 0.173 0.168 0.136 0.190 0.166
Pride and Prejudice 0.207 0.112 0.113 0.079 0.265 0.225
The Picture of Dorian Gray 0.176 0.145 0.183 0.115 0.217 0.165
The Time Machine 0.151 0.162 0.142 0.097 0.219 0.228
Wuthering Heights 0.151 0.168 0.165 0.135 0.204 0.177

Mean emotional palette#

heat = mean_emo.copy()
fig = px.imshow(
    heat, color_continuous_scale='YlOrRd', aspect='auto',
    labels=dict(color='Mean frequency'),
    title='Average NRC emotion share by novel',
)
fig.update_layout(template='plotly_white', height=480)
display_plotly(fig)

Joy versus fear along the plot#

Two emotions that ought to trade places in a gothic novel, and stay inverted in a comedy of manners.

long = emo.melt(
    id_vars=['title', 'book_id', 'progress'],
    value_vars=['joy_smooth', 'fear_smooth'],
    var_name='emotion', value_name='frequency',
)
long['emotion'] = long['emotion'].str.replace('_smooth', '')
long['Progress (%)'] = long['progress'] * 100
fig = px.line(
    long, x='Progress (%)', y='frequency', color='emotion', facet_col='title',
    facet_col_wrap=2, color_discrete_map={'joy': '#c9a227', 'fear': '#4a0e0e'},
)
fig.update_layout(template='plotly_white', height=900, title='Joy vs fear vs progress')
fig.update_yaxes(matches=None)
fig.for_each_annotation(lambda a: a.update(text=a.text.split('=')[-1]))
display_plotly(fig)

A Christmas Carol — five staves, six emotions#

carol = emo.loc[emo['book_id'] == 'christmas_carol']
fig = go.Figure()
palette = {
    'joy': '#c9a227', 'trust': '#2e7d32', 'anticipation': '#1565c0',
    'fear': '#4a0e0e', 'sadness': '#546e7a', 'anger': '#c62828',
}
for emo_name in NRC_EMOTIONS:
    fig.add_trace(go.Scatter(
        x=carol['progress'] * 100, y=carol[f'{emo_name}_smooth'],
        mode='lines', name=emo_name, line=dict(color=palette[emo_name], width=2),
        stackgroup='one',
    ))
fig.update_layout(
    title='A Christmas Carol — stacked NRC emotions',
    xaxis_title='Progress (%)', yaxis_title='Smoothed frequency',
    template='plotly_white', height=480,
)
display_plotly(fig)
print('Stave headings:')
print(carol.groupby('chapter_title')['page_idx'].min().sort_values())
Stave headings:
chapter_title
STAVE I: MARLEY'S GHOST                                                               0
STAVE II: THE FIRST OF THE THREE SPIRITS                                             26
STAVE III: THE SECOND OF THE THREE SPIRITS                                           51
STAVE IV: THE LAST OF THE SPIRITS THE Phantom slowly, gravely, silently, approac     84
STAVE V: THE END OF IT                                                              106
Name: page_idx, dtype: int64

Reading the lexicon#

Dracula and Frankenstein should load on fear and sadness; Pride and Prejudice on trust and joy; A Christmas Carol should shift toward joy and trust after the last ghost. NRC is still a word list — heart is tagged positively even when it is breaking — so spikes want a glance at the surrounding page, not a causal story.


Navigation: ← Previous: Sentiment Trajectories | Next: Term Frequency →