如何用Python和R對故事情節做情緒分析?

王樹義發表於2018-06-28

想知道一部沒看過的影視劇能否符合自己口味,卻又怕被劇透?沒關係,我們可以用情緒分析來了解故事情節是否足夠跌宕起伏。本文一步步教你如何用Python和R輕鬆愉快完成文字情緒分析。一起來試試吧。

如何用Python和R對故事情節做情緒分析?

煩惱

追劇是個令人苦惱的事情。

就拿剛剛播完第7季的《權力的遊戲》來說,每週等的時候那叫一個煎熬,就盼著週一能提早到來。

可是最後一集播完,你緊張、興奮、激動和過癮之後呢?是不是又覺得很失落?

因為——下面我該看什麼劇啊?

現在的影視作品,不是太少,而是太多。如果你有選擇困難症,更會有生不逢時的感覺。

Netflix, Amazon和豆瓣等推薦引擎可以給你推薦影視作品。但是它們的推薦,只是把觀眾劃分成了許多個圈子。你的資料,如果足夠真實準確的話,可能剛好和某一個圈子的特性比較接近,於是就給你推薦這個圈子更喜歡的作品。

但是這不一定靠譜。有可能你的觀影和評價資訊分散在不同的平臺上。不完整、不準確的觀影資料,會導致推薦的效果大打折扣。

即便有了推薦的影視劇,它是否符合你的口味呢?畢竟看劇也是有機會成本的。放著《絕命毒師》不看,去看了一部爛劇,你的生命中的數十小時就這樣被浪費了。

可除了從頭到尾看一遍,又如何能驗證一部劇是否是自己喜歡的呢?

你可能想到去評論區看劇評。那可是個危險區域,因為隨時都有被劇透的風險。

你覺得還是利用社交媒體吧,在萬能的朋友圈問問好友。有的好友確實很熱心,但有的時候,也許會過於熱心。

例如下面這位(圖片來自於網路):

如何用Python和R對故事情節做情緒分析?

你可能抓狂了,覺得這是個不可能完成的任務,就如同英諺所云:

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"。

你可以到這個網址下載這一集的劇本。

如何用Python和R對故事情節做情緒分析?

你只需要全選頁面拷貝,然後開啟一個文字編輯器,把內容貼上進去。好了,現在你就有可供分析的文字了。

請建立一個工作目錄。後面的操作都在這個目錄裡進行。例如我的工作目錄是~/Downloads/python-r-emotion

把剛剛獲得的文字檔案放到這個目錄中。

Python

我們需要用到Jupyter Notebook,請安裝Anaconda套裝。具體的安裝方法請參考《 如何用Python做詞雲 》一文。

R

這個網址下載R基礎安裝包。你會看到R的下載位置有很多。

如何用Python和R對故事情節做情緒分析?

我建議你選擇中國的映象,這樣連線速度更快。清華大學的映象就不錯。

如何用Python和R對故事情節做情緒分析?

請根據你的作業系統平臺選擇其中對應的版本下載。我選擇的是macOS版本,下載得到pkg檔案。雙擊就可以安裝。

安裝了基礎包之後,我們繼續安裝整合開發環境RStudio。下載地址為這裡

如何用Python和R對故事情節做情緒分析?

還是依據你的作業系統情況,選擇對應的安裝包。macOS安裝包為dmg檔案。雙擊開啟後,把其中的RStudio.app圖示拖動到Applications資料夾中,安裝就完成了。

好了,現在你就有了R的執行環境了。

清理

我們首先需要清理文字資料,完成以下這兩個任務:

  1. 把與劇情正文無關的內容去除;
  2. 將資料轉換成R可以直接做情緒分析的結構化資料格式。

到你的系統“終端”(macOS, Linux)或者“命令提示符”(Windows)下,進入我們的工作目錄,執行以下命令。

jupyter notebook
複製程式碼

這時候工作目錄下還只有那個文字檔案。

如何用Python和R對故事情節做情緒分析?

我們開啟看看內容。

如何用Python和R對故事情節做情緒分析?

往下翻頁,我們找到了劇本正文正式開始的標記Opening Credits

如何用Python和R對故事情節做情緒分析?

翻到文字的結尾,我們可以看到劇本結束的標記End Credits

我們回到主頁面下,新建一個Python的Notebook。點選右方的New按鈕,選擇Python 2。

如何用Python和R對故事情節做情緒分析?

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

import pandas as pd
import re
複製程式碼

然後讀取當前目錄下的文字檔案。

with open("s03e09.txt") as f:
    data = f.read()
複製程式碼

看看內容:

print(data)
複製程式碼

結果如下:

如何用Python和R對故事情節做情緒分析?

資料正確讀入。下面我們依照剛才瀏覽中發現的標記把正文以外的文字內容去掉。

先去掉開頭的非劇本正文內容。

data = data.split('Opening Credits]')[1]
複製程式碼

再次列印,可以看見現在從正文開頭了。

print(data)
複製程式碼

如何用Python和R對故事情節做情緒分析?

下面我們同樣處理結尾部分。

data = data.split('[End Credits')[0]
複製程式碼

列印出來試試看。

print(data)
複製程式碼

拖動到尾部。

如何用Python和R對故事情節做情緒分析?

移除了開頭和結尾的多餘內容後,我們來移除空行。這裡我們需要用到正規表示式。

regex = r"^$\n"
subst = ""
data = re.sub(regex, subst, data, 0, re.MULTILINE)
複製程式碼

然後我們再次列印。

print(data)
複製程式碼

如何用Python和R對故事情節做情緒分析?

空行都已經成功挪走了。可是我們注意到還有一些分割線組成的行,也需要去除掉。

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和R對故事情節做情緒分析?

一切正常,下面我們把目前的陣列轉換成資料框。如果你對資料框的概念不太熟悉,請參考《貸還是不貸:如何用Python和機器學習幫你決策?》一文。

df = pd.DataFrame(myrows)
複製程式碼

我們來看看執行結果:

df.head()
複製程式碼

如何用Python和R對故事情節做情緒分析?

資料是正確的,不過表頭不對。我們給表頭重新命名。

df.columns = ['line', 'text']
複製程式碼

再來看看:

df.head()
複製程式碼

如何用Python和R對故事情節做情緒分析?

好了,既然資料框已經做好了。下面我們把它轉換成為csv格式,以便於R來讀取和處理。

df.to_csv('data.csv', index=False)
複製程式碼

我們開啟data.csv檔案,可以看到資料如下:

如何用Python和R對故事情節做情緒分析?

資料清理和準備工作結束,下面我們用R進行分析。

分析

RStudio可以提供一個互動環境,幫我們執行R命令並即時反饋結果。

開啟RStudio之後,選擇File->New,然後從以下介面中選擇 R Notebook。

如何用Python和R對故事情節做情緒分析?

然後,我們就有了一個R Notebook的模板。模板附帶一些基礎使用說明。

如何用Python和R對故事情節做情緒分析?

我們嘗試點選編輯區域(左側)程式碼部分(灰色)的執行按鈕。

如何用Python和R對故事情節做情緒分析?

立即就可以看到繪圖的結果了。

另外我們還可以點選選單欄上的Preview按鈕,來看整個兒程式碼的執行結果。

如何用Python和R對故事情節做情緒分析?

RStudio為我們生成了HTML檔案,我們的文字說明、程式碼和執行結果圖文並茂呈現出來。

好了,熟悉了環境後,我們該實際操作執行自己的程式碼了。我們們把左側編輯區的開頭說明區保留,把全部正文刪除,並且把檔名改成有意義的名字,例如emotional-analysis

如何用Python和R對故事情節做情緒分析?

這樣就清爽多了。

下面我們讀入資料。

setwd("~/Downloads/python-r-emotion/")
script <- read.csv("data.csv", stringsAsFactors=FALSE)
複製程式碼

讀入的時候一定要注意設定stringsAsFactors=FALSE,不然R在讀取字串資料的時候,會預設轉換為level,後面的分析就做不成了。讀取之後,在右側的資料區域你可以看到script這個變數,雙擊它,可以看到內容。

如何用Python和R對故事情節做情緒分析?

資料有了,下面我們需要準備分析用的包。這裡我們需要用到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"
複製程式碼

如何用Python和R對故事情節做情緒分析?

結果是豐富多彩的,可惜看不大清楚。為了區別不同情緒,我們呼叫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"
複製程式碼

如何用Python和R對故事情節做情緒分析?

嗯,這張圖看著就舒服多了。

不過這張圖也會給我們造成一些疑惑。按照道理來說,每一段落的內容裡,包含單詞數量大致相當。結尾部分情感分析結果裡面,正向和負向幾乎同時上升,這就讓人很不解。是這裡的幾行太長了,還是出了什麼其他的問題呢?

資料分析的關鍵,就是在這種令人疑惑的地方深挖進去。

我們不妨來看看,出現最多的正向和負向情感詞都有哪些。

先來看看正向的。我們這次不是按照行號,而是按照詞頻來排序。

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和R對故事情節做情緒分析?

這幅圖一下子變得清晰,也值得琢磨。

在這一集的結尾,多種情緒混雜交織——歡快的氣氛陡然下降,期待與信任在波動,厭惡在不斷上漲,恐懼與悲傷陡然上升,憤怒突破天際,交雜著數次的驚訝……

你可能會納悶兒,情緒怎麼可能這麼複雜?是不是分析又出問題了?

還真不是,這一集的故事,有個另外的名字,叫做《紅色婚禮》。

收穫

通過本文的學習,希望你已初步掌握瞭如下技能:

  1. 如何用Python對網路摘取的文字做處理,從中找出正文,並且去掉空行等內容;
  2. 如何用資料框對資料進行儲存、表示與格式轉換,在Python和R中交換資料;
  3. 如何安裝和使用RStudio環境,用R Notebook做互動式程式設計;
  4. 如何利用tidytext方式來處理情感分析與情緒分析;
  5. 如何設定自己的停用詞表;
  6. 如何用ggplot繪製多維度切面圖形。

掌握了這些內容後,你是否覺得用這麼強大的工具分析個劇本找影視作品,有些大炮轟蚊子的感覺?

討論

除了本文介紹的方法之外,你還知道哪些方便的情緒分析工具與方法?在尋找新劇方面,你有什麼獨家心得體悟?有了情緒分析這個利器,你還可以處理哪些有趣的問題?歡迎留言,記錄下你的思考,分享給大家。我們一起交流討論。

喜歡請點贊。還可以微信關注和置頂我的公眾號“玉樹芝蘭”(nkwangshuyi)

如果你對資料科學感興趣,不妨閱讀我的系列教程索引貼《如何高效入門資料科學?》,裡面還有更多的有趣問題及解法。

相關文章