如何用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种情绪,分别为:
愤怒(anger)
期待(anticipation)
厌恶(disgust)
恐惧(fear)
喜悦(joy)
悲伤(sadness)
惊讶(surprise)
信任(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的运行环境了。
清理
我们首先需要清理文本数据,完成以下这两个任务:
把与剧情正文无关的内容去除;
将数据转换成R可以直接做情绪分析的结构化数据格式。
到你的系统“终端”(macOS, Linux)或者“命令提示符”(Windows)下,进入我们的工作目录,执行以下命令。
jupyter notebook
这时候工作目录下还只有那个文本文件。
我们打开看看内容。
往下翻页,我们找到了剧本正文正式开始的标记Opening Credits
。
翻到文本的结尾,我们可以看到剧本结束的标记End Credits
。
我们回到主页面下,新建一个Python的Notebook。点击右方的New按钮,选择Python 2。
有了全新的Notebook后,我们首先引入需要用到的包。
import pandas as pdimport 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 = 1for 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"
这幅图一下子变得清晰,也值得琢磨。
在这一集的结尾,多种情绪混杂交织——欢快的气氛陡然下降,期待与信任在波动,厌恶在不断上涨,恐惧与悲伤陡然上升,愤怒突破天际,交杂着数次的惊讶……
你可能会纳闷儿,情绪怎么可能这么复杂?是不是分析又出问题了?
还真不是,这一集的故事,有个另外的名字,叫做《红色婚礼》。
收获
通过本文的学习,希望你已初步掌握了如下技能:
如何用Python对网络摘取的文本做处理,从中找出正文,并且去掉空行等内容;
如何用数据框对数据进行存储、表示与格式转换,在Python和R中交换数据;
如何安装和使用RStudio环境,用R Notebook做交互式编程;
如何利用tidytext方式来处理情感分析与情绪分析;
如何设置自己的停用词表;
如何用ggplot绘制多维度切面图形。
掌握了这些内容后,你是否觉得用这么强大的工具分析个剧本找影视作品,有些大炮轰蚊子的感觉?
讨论
除了本文介绍的方法之外,你还知道哪些方便的情绪分析工具与方法?在寻找新剧方面,你有什么独家心得体悟?有了情绪分析这个利器,你还可以处理哪些有趣的问题?欢迎留言,记录下你的思考,分享给大家。我们一起交流讨论。
如果你对我的文章感兴趣,欢迎点赞,并且微信关注和置顶我的公众号“玉树芝兰”(nkwangshuyi)。
如果本文可能对你身边的亲友有帮助,也欢迎你把本文通过微博或朋友圈分享给他们。让他们一起参与到我们的讨论中来。
如果喜欢我的文章,请微信扫描下方二维码,关注并置顶我的公众号“玉树芝兰”。
如果你希望支持我继续输出更多的优质内容,欢迎长按并识别下面的小程序码,赞赏本文。感谢你的支持!
欢迎微信扫码加入我的“知识星球”圈子。第一时间分享给你我的发现和思考。