阅读 226

如何用Python和R对故事情节做情绪分析?

想知道一部没看过的影视剧能否符合自己口味,却又怕被剧透?没关系,我们可以用情绪分析来了解故事情节是否足够跌宕起伏。本文一步步教你如何用Python和R轻松愉快完成文本情绪分析。一起来试试吧。

烦恼

追剧是个令人苦恼的事情。

就拿刚刚播完第7季的《权力的游戏》来说,每周等的时候那叫一个煎熬,就盼着周一能提早到来。

可是最后一集播完,你紧张、兴奋、激动和过瘾之后呢?是不是又觉得很失落?

因为——下面我该看什么剧啊?

现在的影视作品,不是太少,而是太多。如果你有选择困难症,更会有生不逢时的感觉。

Netflix, Amazon和豆瓣等推荐引擎可以给你推荐影视作品。但是它们的推荐,只是把观众划分成了许多个圈子。你的数据,如果足够真实准确的话,可能刚好和某一个圈子的特性比较接近,于是就给你推荐这个圈子更喜欢的作品。

但是这不一定靠谱。有可能你的观影和评价信息分散在不同的平台上。不完整、不准确的观影数据,会导致推荐的效果大打折扣。

即便有了推荐的影视剧,它是否符合你的口味呢?毕竟看剧也是有机会成本的。放着《绝命毒师》不看,去看了一部烂剧,你的生命中的数十小时就这样被浪费了。

可除了从头到尾看一遍,又如何能验证一部剧是否是自己喜欢的呢?

你可能想到去评论区看剧评。那可是个危险区域,因为随时都有被剧透的风险。

你觉得还是利用社交媒体吧,在万能的朋友圈问问好友。有的好友确实很热心,但有的时候,也许会过于热心。

例如下面这位(图片来自于网络):

你可能抓狂了,觉得这是个不可能完成的任务,就如同英谚所云:

You can't have your cake and eat it too.

真的是这样吗?不一定。在这个大数据泛滥,数据分析工具并不稀缺的时代,你完全可以利用技术帮自己选择优秀的影视作品。

故事情节的文本,你可以到互联网上找剧本,或者是字幕。当然,不是让你把剧本从头读到尾,那样还不如直接看剧呢。你需要用技术来对文本进行分析。

情绪

我们提到的这个技术,叫做情绪分析(emotional analysis)。它和情感分析(sentiment analysis)有相似之处。都是通过对内容的自动化分析,来获得结果。

情感分析的结果一般分为正向(positive)和负向(negative),而情绪分析包含的种类就比较多了。

加拿大国家研究委员会(National Research Council of Canada)官方发布的情绪词典包含了8种情绪,分别为:

  1. 愤怒(anger)
  2. 期待(anticipation)
  3. 厌恶(disgust)
  4. 恐惧(fear)
  5. 喜悦(joy)
  6. 悲伤(sadness)
  7. 惊讶(surprise)
  8. 信任(trust)

有了这些情绪的标记,你可以轻松地对一段文本的情绪变化进行分析。

这时候,你可以回忆起中学语文老师讲作文时说过的那句话:

文如看山不喜平。

故事情节会伴随着各种情绪的波动。通过分析这些情绪的起伏,我们可以看出故事的基调是否符合自己的口味,情节是否紧凑等。这样,你可以根据自己的偏好,甚至是当前的心境,来选择合适的作品观看了。

我们需要用到Python和R。这两种语言在目前数据科学领域里最受欢迎。Python的优势在于通用,而R的优势在于统计学家组成的社区。这些统计学家真是高产,也很酷,经常制造出令人惊艳的分析包。

咱们这里就用Python来做数据清理,然后用R做情绪分析,并且把结果可视化输出。

准备

数据

我们首先需要找到的是来源数据。作为例子,我们选择了《权利的游戏》第三季的第9集,名字叫做"The Rains of Castamere"。

你可以到这个网址下载这一集的剧本。

你只需要全选页面拷贝,然后打开一个文本编辑器,把内容粘贴进去。好了,现在你就有可供分析的文本了。

请建立一个工作目录。后面的操作都在这个目录里进行。例如我的工作目录是~/Downloads/python-r-emotion

把刚刚获得的文本文件放到这个目录中。

Python

我们需要用到Jupyter Notebook,请安装Anaconda套装。具体的安装方法请参考《 如何用Python做词云 》一文。

R

这个网址下载R基础安装包。你会看到R的下载位置有很多。

我建议你选择中国的镜像,这样连接速度更快。清华大学的镜像就不错。

请根据你的操作系统平台选择其中对应的版本下载。我选择的是macOS版本,下载得到pkg文件。双击就可以安装。

安装了基础包之后,我们继续安装集成开发环境RStudio。下载地址为这里

还是依据你的操作系统情况,选择对应的安装包。macOS安装包为dmg文件。双击打开后,把其中的RStudio.app图标拖动到Applications文件夹中,安装就完成了。

好了,现在你就有了R的运行环境了。

清理

我们首先需要清理文本数据,完成以下这两个任务:

  1. 把与剧情正文无关的内容去除;
  2. 将数据转换成R可以直接做情绪分析的结构化数据格式。

到你的系统“终端”(macOS, Linux)或者“命令提示符”(Windows)下,进入我们的工作目录,执行以下命令。

jupyter notebook
复制代码

这时候工作目录下还只有那个文本文件。

我们打开看看内容。

往下翻页,我们找到了剧本正文正式开始的标记Opening Credits

翻到文本的结尾,我们可以看到剧本结束的标记End Credits

我们回到主页面下,新建一个Python的Notebook。点击右方的New按钮,选择Python 2。

有了全新的Notebook后,我们首先引入需要用到的包。

import pandas as pd
import re
复制代码

然后读取当前目录下的文本文件。

with open("s03e09.txt") as f:
    data = f.read()
复制代码

看看内容:

print(data)
复制代码

结果如下:

数据正确读入。下面我们依照刚才浏览中发现的标记把正文以外的文本内容去掉。

先去掉开头的非剧本正文内容。

data = data.split('Opening Credits]')[1]
复制代码

再次打印,可以看见现在从正文开头了。

print(data)
复制代码

下面我们同样处理结尾部分。

data = data.split('[End Credits')[0]
复制代码

打印出来试试看。

print(data)
复制代码

拖动到尾部。

移除了开头和结尾的多余内容后,我们来移除空行。这里我们需要用到正则表达式。

regex = r"^$\n"
subst = ""
data = re.sub(regex, subst, data, 0, re.MULTILINE)
复制代码

然后我们再次打印。

print(data)
复制代码

空行都已经成功挪走了。可是我们注意到还有一些分割线组成的行,也需要去除掉。

regex = r"^-+$\n"
subst = ""
data = re.sub(regex, subst, data, 0, re.MULTILINE)
复制代码

至此,清理工作已经完成了。下面我们把文本整理成数据框,每一行分别加上行号。

利用换行符把原本完整的文本分割成行。

lines = data.split('\n')
复制代码

然后给每一行加上行号。

myrows = []
num = 1
for line in lines:
    myrows.append([num, line])
    num = num + 1
复制代码

我们看看前三行的行号是否已经正常添加。

myrows[:3]
复制代码

一切正常,下面我们把目前的数组转换成数据框。如果你对数据框的概念不太熟悉,请参考《贷还是不贷:如何用Python和机器学习帮你决策?》一文。

df = pd.DataFrame(myrows)
复制代码

我们来看看执行结果:

df.head()
复制代码

数据是正确的,不过表头不对。我们给表头重新命名。

df.columns = ['line', 'text']
复制代码

再来看看:

df.head()
复制代码

好了,既然数据框已经做好了。下面我们把它转换成为csv格式,以便于R来读取和处理。

df.to_csv('data.csv', index=False)
复制代码

我们打开data.csv文件,可以看到数据如下:

数据清理和准备工作结束,下面我们用R进行分析。

分析

RStudio可以提供一个交互环境,帮我们执行R命令并即时反馈结果。

打开RStudio之后,选择File->New,然后从以下界面中选择 R Notebook。

然后,我们就有了一个R Notebook的模板。模板附带一些基础使用说明。

我们尝试点击编辑区域(左侧)代码部分(灰色)的运行按钮。

立即就可以看到绘图的结果了。

另外我们还可以点击菜单栏上的Preview按钮,来看整个儿代码的运行结果。

RStudio为我们生成了HTML文件,我们的文字说明、代码和运行结果图文并茂呈现出来。

好了,熟悉了环境后,我们该实际操作运行自己的代码了。咱们把左侧编辑区的开头说明区保留,把全部正文删除,并且把文件名改成有意义的名字,例如emotional-analysis

这样就清爽多了。

下面我们读入数据。

setwd("~/Downloads/python-r-emotion/")
script <- read.csv("data.csv", stringsAsFactors=FALSE)
复制代码

读入的时候一定要注意设置stringsAsFactors=FALSE,不然R在读取字符串数据的时候,会默认转换为level,后面的分析就做不成了。读取之后,在右侧的数据区域你可以看到script这个变量,双击它,可以看到内容。

数据有了,下面我们需要准备分析用的包。这里我们需要用到4个包,请执行以下语句安装。

install.packages("dplyr")
install.packages("tidytext")
install.packages("tidyr")
install.packages("ggplot2")
复制代码

注意安装新软件包这种操作只需要执行一次。可是我们每次预览结果的时候,文件里所有语句都会被执行一遍。为了避免安装命令被反复执行。当安装结束后,请你删除或者注释掉上面几条语句。

安装了包,并不意味着就可以直接用其中的函数了。使用之前,你需要执行library语句调用这些包。

library(dplyr)
library(tidytext)
library(tidyr)
library(ggplot2)
复制代码

好了,万事俱备。我们需要把一句句的文本拆成单词,这样才能和情绪词典里的单词做匹配,从而分析单词的情绪属性。

在R里面,可以采用Tidy Text方式来做。执行的语句是unnest_token,我们把原先的句子拆分成为单词。

tidy_script <- script %>%
  unnest_tokens(word, text)
head(tidy_script)
复制代码
##     line     word
## 1      1    first
## 1.1    1    scene
## 1.2    1    shows
## 1.3    1      the
## 1.4    1 location
## 1.5    1       of
复制代码

这里原先的行号依然被保留。我们可以看到每一个词来自于哪一行,这有利于下面我们对行甚至段落单位进行分析。

我们调用加拿大国家研究委员会发布的情绪词典。这个词典在tidytext包里面内置了,就叫做nrc

tidy_script %>%
  inner_join(get_sentiments("nrc")) %>%
  arrange(line) %>%
  head(10)
复制代码

我们只显示前10行的内容:

## Joining, by = "word"

##    line         word    sentiment
## 1     1         rock     positive
## 2     1    ancestral        trust
## 3     1        giant         fear
## 4     1 representing anticipation
## 5     1        stark     negative
## 6     1        stark        trust
## 7     1        stark     negative
## 8     1        stark        trust
## 9     4    dangerous         fear
## 10    4    dangerous     negative
复制代码

可以看到,有的词对应某一种情绪属性,有的词同时对应多种情绪属性。注意nrc包里面不仅有情绪,而且还有情感(正向和负向)。

我们对单词的情绪已经清楚了。下面我们来综合判断每一行的不同情感分别含有几个词。

tidy_script %>%
  inner_join(get_sentiments("nrc")) %>%
  count(line, sentiment) %>%
  arrange(line) %>%
  head(10)
复制代码

还是只显示结果的前10行。

## Joining, by = "word"

## # A tibble: 10 x 3
##     line    sentiment     n
##    <int>        <chr> <int>
##  1     1 anticipation     1
##  2     1         fear     1
##  3     1     negative     2
##  4     1     positive     1
##  5     1        trust     3
##  6     4         fear     1
##  7     4     negative     1
##  8     5     positive     1
##  9     5        trust     1
## 10     6     positive     1
复制代码

以第1行为例,包含“期待”的词有1个,包含“恐惧”的有1个,包含“信任”的有3个。

如果我们以1行为单位分析情感变化,粒度过细。鉴于整个剧本包含了几百行文字,我们以5行作为一个基础单位,来进行分析。

这里我们使用index来把原先的行号处理一下,分成段落。%/%代表整除符号,这样0-4行就成为了第一段落,5-9行成为第二段落,以此类推。

tidy_script %>%
  inner_join(get_sentiments("nrc")) %>%
  count(line, sentiment) %>%
  mutate(index = line %/% 5) %>%
  arrange(index) %>%
  head(10)
复制代码
## Joining, by = "word"

## # A tibble: 10 x 4
##     line    sentiment     n index
##    <int>        <chr> <int> <dbl>
##  1     1 anticipation     1     0
##  2     1         fear     1     0
##  3     1     negative     2     0
##  4     1     positive     1     0
##  5     1        trust     3     0
##  6     4         fear     1     0
##  7     4     negative     1     0
##  8     5     positive     1     1
##  9     5        trust     1     1
## 10     6     positive     1     1
复制代码

可以看出,第一段包含的情感还真是很丰富。

只是如果让我们把结果表格从头读到尾,那也真够难受的。我们还是用可视化的方法,把图绘制出来吧。

绘图我们采用ggplot包。这个包我们在《 如何用Python做舆情时间序列可视化? 》一文中介绍过,欢迎查阅复习。

我们使用geom_col指令,让R帮我们绘制柱状图。对不同的情绪,我们用不同颜色表示出来。

tidy_script %>%
  inner_join(get_sentiments("nrc")) %>%
  count(line, sentiment) %>%
  mutate(index = line %/% 5) %>%
  ggplot(aes(x=index, y=n, color=sentiment)) %>%
  + geom_col()
复制代码
## Joining, by = "word"
复制代码

结果是丰富多彩的,可惜看不大清楚。为了区别不同情绪,我们调用facet_wrap函数,把不同情绪拆开,分别绘制。

tidy_script %>%
  inner_join(get_sentiments("nrc")) %>%
  count(line, sentiment) %>%
  mutate(index = line %/% 5) %>%
  ggplot(aes(x=index, y=n, color=sentiment)) %>%
  + geom_col() %>%
  + facet_wrap(~sentiment, ncol=3)
复制代码
## Joining, by = "word"
复制代码

嗯,这张图看着就舒服多了。

不过这张图也会给我们造成一些疑惑。按照道理来说,每一段落的内容里,包含单词数量大致相当。结尾部分情感分析结果里面,正向和负向几乎同时上升,这就让人很不解。是这里的几行太长了,还是出了什么其他的问题呢?

数据分析的关键,就是在这种令人疑惑的地方深挖进去。

我们不妨来看看,出现最多的正向和负向情感词都有哪些。

先来看看正向的。我们这次不是按照行号,而是按照词频来排序。

tidy_script %>%
  inner_join(get_sentiments("nrc")) %>%
  filter(sentiment == "positive") %>%
  count(word) %>%
  arrange(desc(n)) %>%
  head(10)
复制代码
## Joining, by = "word"

## # A tibble: 10 x 2
##        word     n
##       <chr> <int>
##  1     lord    13
##  2     good     9
##  3    guard     9
##  4 daughter     8
##  5 shoulder     7
##  6     love     6
##  7     main     6
##  8    quiet     6
##  9    bride     5
## 10     king     5
复制代码

看到这个词频,我们不禁有些失落——看来分析结果是有问题的。许多词汇都是名词,而且在《权力的游戏》故事中,这些词根本就没有明确的情感指向。例如lord这个词,剧中的lord有的正直善良,但也有很多不是什么好人;king也一样,虽然Robb和Jon是国王,但别忘了Joffrey也是国王啊。

我们再来看看负向情感词汇吧。

tidy_script %>%
  inner_join(get_sentiments("nrc")) %>%
  filter(sentiment == "negative") %>%
  count(word) %>%
  arrange(desc(n)) %>%
  head(10)
复制代码
## Joining, by = "word"

## # A tibble: 10 x 2
##       word     n
##      <chr> <int>
##  1   stark    16
##  2     pig    14
##  3    lord    13
##  4    worm    12
##  5    kill    11
##  6   black     9
##  7  dagger     8
##  8    shot     8
##  9 killing     7
## 10  afraid     4
复制代码

看了这个结果,就更令人沮丧不已了——同样的一个lord,竟然既被当成了正向,又被当成了负向词汇。词典标注者太不负责任了吧!

别着急。出现这样的情况,是因为我们做分析时少了一个重要步骤——处理停用词。对于每一个具体场景,我们都需要使用停用词表,把那些可能干扰分析结果的词扔出去。

tidytext提供了默认的停用词表。我们先拿来试试看。这里使用的语句是anti_join,就可以把停用词先去除,再进行情绪词表连接。

我们看看停用词去除后,正向情感词汇的高频词有没有变化。

tidy_script %>%
  anti_join(stop_words) %>%
  inner_join(get_sentiments("nrc")) %>%
  filter(sentiment == "positive") %>%
  count(word) %>%
  arrange(desc(n)) %>%
  head(10)
复制代码
## Joining, by = "word"
## Joining, by = "word"

## # A tibble: 10 x 2
##        word     n
##       <chr> <int>
##  1     lord    13
##  2    guard     9
##  3 daughter     8
##  4 shoulder     7
##  5     love     6
##  6     main     6
##  7    quiet     6
##  8    bride     5
##  9     king     5
## 10    music     5
复制代码

结果令人失望。看来停用词表里没有包含我们需要去除的那一堆名词。

没关系,我们自己来修订停用词表。使用R中的bind_rows语句,我们就能在基础的预置停用词表基础上,附加上我们自己的停用词。

custom_stop_words <- bind_rows(stop_words,
                               data_frame(word = c("stark", "mother", "father", "daughter", "brother", "rock", "ground", "lord", "guard", "shoulder", "king", "main", "grace", "gate", "horse", "eagle", "servent"),
                                          lexicon = c("custom")))
复制代码

我们加入了一堆名词和关系代词。因为它们和情绪之间没有必然的关联。但是名词还是保留了一些。例如“新娘”总该是和好的情感和情绪相连吧。

用了定制的停用词表后,我们来看看词频的变化。

tidy_script %>%
  anti_join(custom_stop_words) %>%
  inner_join(get_sentiments("nrc")) %>%
  filter(sentiment == "positive") %>%
  count(word) %>%
  arrange(desc(n)) %>%
  head(10)
复制代码
## Joining, by = "word"
## Joining, by = "word"

## # A tibble: 10 x 2
##           word     n
##          <chr> <int>
##  1        love     6
##  2       quiet     6
##  3       bride     5
##  4       music     5
##  5        rest     5
##  6     finally     4
##  7        food     3
##  8     forward     3
##  9        hope     3
## 10 hospitality     3
复制代码

这次好多了,起码解释情绪可以自圆其说了。我们再看看那些负向情感词汇。

tidy_script %>%
  anti_join(custom_stop_words) %>%
  inner_join(get_sentiments("nrc")) %>%
  filter(sentiment == "negative") %>%
  count(word) %>%
  arrange(desc(n)) %>%
  head(10)
复制代码
## Joining, by = "word"
## Joining, by = "word"

## # A tibble: 10 x 2
##       word     n
##      <chr> <int>
##  1     pig    14
##  2    worm    12
##  3    kill    11
##  4   black     9
##  5  dagger     8
##  6    shot     8
##  7 killing     7
##  8  afraid     4
##  9    fear     4
## 10   leave     4
复制代码

比起之前,也有很大进步。

做好了基础的修订工作,下面我们来重新作图吧。我们把停用词表加进去,并且还用filter语句把情感属性删除掉了。因为我们分析的对象是情绪(emotion),而不是情感(sentiment)。

tidy_script %>%
  anti_join(custom_stop_words) %>%
  inner_join(get_sentiments("nrc")) %>%
  filter(sentiment != "negative" & sentiment != "positive") %>%
  count(line, sentiment) %>%
  mutate(index = line %/% 5) %>%
  ggplot(aes(x=index, y=n, color=sentiment)) %>%
  + geom_col() %>%
  + facet_wrap(~sentiment, ncol=3)
复制代码
## Joining, by = "word"
## Joining, by = "word"
复制代码

这幅图一下子变得清晰,也值得琢磨。

在这一集的结尾,多种情绪混杂交织——欢快的气氛陡然下降,期待与信任在波动,厌恶在不断上涨,恐惧与悲伤陡然上升,愤怒突破天际,交杂着数次的惊讶……

你可能会纳闷儿,情绪怎么可能这么复杂?是不是分析又出问题了?

还真不是,这一集的故事,有个另外的名字,叫做《红色婚礼》。

收获

通过本文的学习,希望你已初步掌握了如下技能:

  1. 如何用Python对网络摘取的文本做处理,从中找出正文,并且去掉空行等内容;
  2. 如何用数据框对数据进行存储、表示与格式转换,在Python和R中交换数据;
  3. 如何安装和使用RStudio环境,用R Notebook做交互式编程;
  4. 如何利用tidytext方式来处理情感分析与情绪分析;
  5. 如何设置自己的停用词表;
  6. 如何用ggplot绘制多维度切面图形。

掌握了这些内容后,你是否觉得用这么强大的工具分析个剧本找影视作品,有些大炮轰蚊子的感觉?

讨论

除了本文介绍的方法之外,你还知道哪些方便的情绪分析工具与方法?在寻找新剧方面,你有什么独家心得体悟?有了情绪分析这个利器,你还可以处理哪些有趣的问题?欢迎留言,记录下你的思考,分享给大家。我们一起交流讨论。

喜欢请点赞。还可以微信关注和置顶我的公众号“玉树芝兰”(nkwangshuyi)

如果你对数据科学感兴趣,不妨阅读我的系列教程索引贴《如何高效入门数据科学?》,里面还有更多的有趣问题及解法。

关注下面的标签,发现更多相似文章
评论