想知道一部沒看過的影視劇能否符合自己口味,卻又怕被劇透?沒關係,我們可以用情緒分析來了解故事情節是否足夠跌宕起伏。本文一步步教你如何用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 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"
複製程式碼
這幅圖一下子變得清晰,也值得琢磨。
在這一集的結尾,多種情緒混雜交織——歡快的氣氛陡然下降,期待與信任在波動,厭惡在不斷上漲,恐懼與悲傷陡然上升,憤怒突破天際,交雜著數次的驚訝……
你可能會納悶兒,情緒怎麼可能這麼複雜?是不是分析又出問題了?
還真不是,這一集的故事,有個另外的名字,叫做《紅色婚禮》。
收穫
通過本文的學習,希望你已初步掌握瞭如下技能:
- 如何用Python對網路摘取的文字做處理,從中找出正文,並且去掉空行等內容;
- 如何用資料框對資料進行儲存、表示與格式轉換,在Python和R中交換資料;
- 如何安裝和使用RStudio環境,用R Notebook做互動式程式設計;
- 如何利用tidytext方式來處理情感分析與情緒分析;
- 如何設定自己的停用詞表;
- 如何用ggplot繪製多維度切面圖形。
掌握了這些內容後,你是否覺得用這麼強大的工具分析個劇本找影視作品,有些大炮轟蚊子的感覺?
討論
除了本文介紹的方法之外,你還知道哪些方便的情緒分析工具與方法?在尋找新劇方面,你有什麼獨家心得體悟?有了情緒分析這個利器,你還可以處理哪些有趣的問題?歡迎留言,記錄下你的思考,分享給大家。我們一起交流討論。
喜歡請點贊。還可以微信關注和置頂我的公眾號“玉樹芝蘭”(nkwangshuyi)。
如果你對資料科學感興趣,不妨閱讀我的系列教程索引貼《如何高效入門資料科學?》,裡面還有更多的有趣問題及解法。