如何用Python和R对《权力的游戏》故事情节做情绪分析?

百家 作者:AI100 2017-09-13 10:55:52

·


本文授权转自王树义的简书



想知道一部没看过的影视剧能否符合自己口味,却又怕被剧透?没关系,我们可以用情绪分析来了解故事情节是否足够跌宕起伏。本文一步步教你如何用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"。


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


https://genius.com/Game-of-thrones-the-rains-of-castamere-annotated



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


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


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


Python


我们需要用到Jupyter Notebook,请安装Anaconda套装。具体的安装方法请参考《 如何用Python做词云 》一文(http://www.jianshu.com/p/e4b24a734ccc)


R


到这个网址(https://cran.r-project.org/mirrors.html)下载R基础安装包。你会看到R的下载位置有很多。



我建议你选择中国的镜像,这样连接速度更快。清华大学的镜像(https://mirrors.tuna.tsinghua.edu.cn/CRAN/)就不错。




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


安装了基础包之后,我们继续安装集成开发环境RStudio。下载地址为这里(https://www.rstudio.com/products/rstudio/download/#download)



还是依据你的操作系统情况,选择对应的安装包。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

##            

##  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

##            

##  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

##        

##  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

##      

##  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

##      

##  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

##          

##  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

##      

##  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绘制多维度切面图形。


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


讨论


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


如果你对我的文章感兴趣,欢迎点赞,并且关注我的简书文集《玉树芝兰》。


如果本文可能对你身边的亲友有帮助,也欢迎你把本文通过微博或朋友圈分享给他们。让他们一起参与到我们的讨论中来。


延伸阅读

如何用Python做词云?

http://www.jianshu.com/p/e4b24a734ccc

如何用Python做中文分词? 

http://www.jianshu.com/p/721190534061

如何用Python做情感分析?

http://www.jianshu.com/p/d50a14541d01

如何用Python做舆情时间序列可视化?

http://www.jianshu.com/p/4ea083874df4

贷还是不贷:如何用Python和机器学习帮你决策?

http://www.jianshu.com/p/67a71e366516

如何用Python从海量文本抽取主题?

http://www.jianshu.com/p/fdde9fc03f94


原文地址

http://www.jianshu.com/p/0c782715e58a





精选福利


关注AI科技大本营,进入公众号,回复对应关键词查看分类专题;回复“入群”,加入AI科技大本营学习群。

回复“深度学习”,一文囊括30篇深度学习精华文章。

回复“机器学习”,一文推荐30篇机器学习优质文章。

回复“访谈”,查看吴喜之、周志华、杨强、蚂蚁金服漆远、今日头条李磊的独家访谈实录。

回复“资源”,一文梳理机器学习,深度学习,神经网络等各方面的资源。

回复“视频”,5分钟的视频带你轻松入门人工智能。

回复“程序员”,带你了解别人家的程序员如何学好AI。

回复“数据”,帮你弄清楚人工智能与数据科学之前的关系。

回复“课程”,跟我一起免费学习:谷歌大脑深度学习&Fast.ai最实战深度学习&David Silver深度强化学习。



 点击阅读原文,查看AI科技大本营招募计划

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接