"""
DepecheMood
-----------
DepecheMood is a high-quality and high-coverage emotion lexicon for English and Italian
text, mapping individual terms to their emotional valences. These word-emotion weights
are inferred from crowd-sourced datasets of emotionally tagged news articles
(rappler.com for English, corriere.it for Italian).
English terms are assigned weights to eight emotions:
- AFRAID
- AMUSED
- ANGRY
- ANNOYED
- DONT_CARE
- HAPPY
- INSPIRED
- SAD
Italian terms are assigned weights to five emotions:
- DIVERTITO (~amused)
- INDIGNATO (~annoyed)
- PREOCCUPATO (~afraid)
- SODDISFATTO (~happy)
- TRISTE (~sad)
"""
import collections
import csv
import io
import statistics
from spacy.parts_of_speech import ADJ, ADV, NOUN, VERB
from spacy.tokens import Doc, Span, Token
from .. import constants
from .. import io as tio
from .. import utils
from .base import Resource
NAME = "depeche_mood"
META = {
"site_url": "http://www.depechemood.eu",
"publication_url": "https://arxiv.org/abs/1810.03660",
"description": "A simple tool to analyze the emotions evoked by a text.",
}
DOWNLOAD_URL = "https://github.com/marcoguerini/DepecheMood/releases/download/v2.0/DepecheMood_v2.0.zip"
[docs]class DepecheMood(Resource):
"""
Interface to DepecheMood, an emotion lexicon for English and Italian text.
Download the data (one time only!), and save its contents to disk::
>>> import textacy.resources
>>> rs = textacy.resources.DepecheMood(lang="en", word_rep="lemmapos")
>>> rs.download()
>>> rs.info
{'name': 'depeche_mood',
'site_url': 'http://www.depechemood.eu',
'publication_url': 'https://arxiv.org/abs/1810.03660',
'description': 'A simple tool to analyze the emotions evoked by a text.'}
Access emotional valences for individual terms::
>>> rs.get_emotional_valence("disease#n")
{'AFRAID': 0.37093526222120465,
'AMUSED': 0.06953745082761113,
'ANGRY': 0.06979683067736414,
'ANNOYED': 0.06465401081252636,
'DONT_CARE': 0.07080580707440012,
'HAPPY': 0.07537324330608403,
'INSPIRED': 0.13394731320662606,
'SAD': 0.14495008187418348}
>>> rs.get_emotional_valence("heal#v")
{'AFRAID': 0.060450319886187334,
'AMUSED': 0.09284046387491741,
'ANGRY': 0.06207816933776029,
'ANNOYED': 0.10027622719958346,
'DONT_CARE': 0.11259594401785,
'HAPPY': 0.09946106491457314,
'INSPIRED': 0.37794768332634626,
'SAD': 0.09435012744278205}
When passing multiple terms in the form of a List[str] or ``Span`` or ``Doc``,
emotion weights are averaged over all terms for which weights are available::
>>> rs.get_emotional_valence(["disease#n", "heal#v"])
{'AFRAID': 0.215692791053696,
'AMUSED': 0.08118895735126427,
'ANGRY': 0.06593750000756221,
'ANNOYED': 0.08246511900605491,
'DONT_CARE': 0.09170087554612506,
'HAPPY': 0.08741715411032858,
'INSPIRED': 0.25594749826648616,
'SAD': 0.11965010465848278}
>>> text = "The acting was sweet and amazing, but the plot was dumb and terrible."
>>> doc = textacy.make_spacy_doc(text, lang="en")
>>> rs.get_emotional_valence(doc)
{'AFRAID': 0.05272350876803627,
'AMUSED': 0.13725054992595098,
'ANGRY': 0.15787016147081184,
'ANNOYED': 0.1398733360688608,
'DONT_CARE': 0.14356943460620503,
'HAPPY': 0.11923217912716871,
'INSPIRED': 0.17880214720077342,
'SAD': 0.07067868283219296}
>>> rs.get_emotional_valence(doc[0:6]) # the acting was sweet and amazing
{'AFRAID': 0.039790959333750785,
'AMUSED': 0.1346884072825313,
'ANGRY': 0.1373596223131593,
'ANNOYED': 0.11391999698695347,
'DONT_CARE': 0.1574819173485831,
'HAPPY': 0.1552521762333925,
'INSPIRED': 0.21232264216449326,
'SAD': 0.049184278337136296}
For good measure, here's how Italian w/o POS-tagged words looks::
>>> rs = textacy.resources.DepecheMood(lang="it", word_rep="lemma")
>>> rs.get_emotional_valence("amore")
{'INDIGNATO': 0.11451408951814121,
'PREOCCUPATO': 0.1323655108545536,
'TRISTE': 0.18249663560400609,
'DIVERTITO': 0.33558928569110086,
'SODDISFATTO': 0.23503447833219815}
Args:
data_dir (str or :class:`pathlib.Path`): Path to directory on disk
under which resource data is stored, i.e. ``/path/to/data_dir/depeche_mood``.
lang ({"en", "it"}): Standard two-letter code for the language of terms
for which emotional valences are to be retrieved.
word_rep ({"token", "lemma", "lemmapos"}): Level of text processing used
in computing terms' emotion weights. "token" => tokenization only;
"lemma" => tokenization and lemmatization; "lemmapos" => tokenization,
lemmatization, and part-of-speech tagging.
min_freq (int): Minimum number of times that a given term must have appeared
in the source dataset for it to be included in the emotion weights dict.
This can be used to remove noisy terms at the expense of reducing coverage.
Researchers observed peak performance at 10, but anywhere between
1 and 20 is reasonable.
"""
_lang_map = {"en": "english", "it": "italian"}
_pos_map = {NOUN: "n", VERB: "v", ADJ: "a", ADV: "r"}
_word_reps = ("token", "lemma", "lemmapos")
def __init__(
self,
data_dir=constants.DEFAULT_DATA_DIR.joinpath(NAME),
lang="en",
word_rep="lemmapos",
min_freq=3,
):
super().__init__(NAME, meta=META)
if lang not in self._lang_map:
raise ValueError(
"lang='{}' is invalid; valid options are {}".format(
lang, sorted(self._lang_map.keys())
)
)
if word_rep not in self._word_reps:
raise ValueError(
"word_rep='{}' is invalid; valid options are {}".format(
word_rep, self._word_reps
)
)
self.lang = lang
self.word_rep = word_rep
self.min_freq = min_freq
self.data_dir = utils.to_path(data_dir).resolve()
self._filepath = self.data_dir.joinpath(
"DepecheMood++",
"DepecheMood_{lang}_{word_rep}_full.tsv".format(
lang=self._lang_map[lang], word_rep=word_rep
),
)
self._weights = None
@property
def filepath(self):
"""
str: Full path on disk for the DepecheMood tsv file
corresponding to the ``lang`` and ``word_rep``.
"""
if self._filepath.is_file():
return str(self._filepath)
else:
return None
@property
def weights(self):
"""
Dict[str, Dict[str, float]]: Mapping of term string (or term#POS,
if :attr:`DepecheMood.word_rep` is "lemmapos") to the terms' normalized weights
on a fixed set of affective dimensions (aka "emotions").
"""
if not self._weights:
if not self.filepath:
raise OSError(
"resource file {} not found;\n"
"has the data been downloaded yet?".format(self._filepath)
)
with io.open(self.filepath, mode="rt", encoding="utf-8") as csvfile:
csv_reader = csv.reader(csvfile, delimiter="\t")
rows = list(csv_reader)
cols = rows[0]
self._weights = {
row[0]: {col: float(val) for col, val in zip(cols[1:-1], row[1:-1])}
for row in rows[1:]
if int(row[-1]) >= self.min_freq
}
return self._weights
[docs] def download(self, *, force=False):
"""
Download resource data as a zip archive file, then save it to disk
and extract its contents under the ``data_dir`` directory.
Args:
force (bool): If True, download the resource, even if it already
exists on disk under ``data_dir``.
"""
filepath = tio.download_file(
DOWNLOAD_URL, filename=None, dirpath=self.data_dir, force=force,
)
if filepath:
tio.unpack_archive(filepath, extract_dir=None)
[docs] def get_emotional_valence(self, terms):
"""
Get average emotional valence over all terms in ``terms`` for which
emotion weights are available.
Args:
terms (str or Sequence[str], ``Token`` or Sequence[``Token``]):
One or more terms over which to average emotional valences.
Note that only nouns, adjectives, adverbs, and verbs are included.
.. note:: If the resource was initialized with ``word_rep="lemmapos"``,
then string terms must have matching parts-of-speech appended to them
like TERM#POS. Only "n" => noun, "v" => verb, "a" => adjective, and
"r" => adverb are included in the data.
Returns:
Dict[str, float]: Mapping of emotion to average weight.
"""
if isinstance(terms, (Token, str)):
return self._get_term_emotional_valence(terms)
elif isinstance(terms, (Span, Doc, collections.abc.Sequence)):
return self._get_terms_emotional_valence(terms)
else:
raise TypeError(
"`terms` must be of type {}, not {}".format(
{Token, Span, Doc, str, collections.abc.Sequence}, type(terms)
)
)
def _get_term_emotional_valence(self, term):
"""
Args:
term (str or :class:`spacy.tokens.Token`)
Returns:
Dict[str, float]
"""
try:
if isinstance(term, str):
return self.weights[term]
elif isinstance(term, Token):
if self.word_rep == "lemmapos":
return self.weights[
"{}#{}".format(term.lemma_, self._pos_map[term.pos])
]
elif self.word_rep == "lemma":
return self.weights[term.lemma_]
else: # word_rep == "token"
return self.weights[term.text]
else:
raise TypeError(
"`term` must be of type {}, not {}".format({str, Token}, type(term))
)
except KeyError:
return {}
def _get_terms_emotional_valence(self, terms):
"""
Args:
term (Sequence[str] or Sequence[:class:`spacy.tokens.Token`])
Returns:
Dict[str, float]
"""
all_emo_weights = collections.defaultdict(list)
for term in terms:
emo_weights = self._get_term_emotional_valence(term)
for emo, weight in emo_weights.items():
all_emo_weights[emo].append(weight)
return {
emo: statistics.mean(weights) for emo, weights in all_emo_weights.items()
}