記一次美妙的數據分析之旅~
- 2020 年 2 月 21 日
- 筆記
本項目基於Kaggle電影影評數據集,通過這個系列,你將學到如何進行數據探索性分析(EDA),學會使用數據分析利器pandas
,會用繪圖包pyecharts
,以及EDA時可能遇到的各種實際問題及一些處理技巧。
通過這個小項目,大家將會掌握pandas
主要常用函數的使用技巧,matplotlib
繪製直方圖,和pyecharts
使用邏輯,具體以下13個知識點:
1 創建DataFrame
,轉換長數據為寬數據;2 導入數據
;3 處理組合值
;4 索引列
;5 連接兩個表
;6 按列篩選
;
7 按照欄位分組
;8 按照欄位排序
;9 分組後使用聚合函數
;10 繪製頻率分布直方圖
繪製;11 最小抽樣量
的計算方法;12 數據去重
;13 結果分析
注意:這些知識點不是散落的
,而是通過求出喜劇電影排行榜,這一個目標主線
把它們串聯起來。
本項目需要導入的包:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from pyecharts.charts import Bar,Grid,Line import pyecharts.options as opts from pyecharts.globals import ThemeType
1 創建DataFrame
pandas中一個dataFrame實例:
Out[89]: a val 0 apple1 1.0 1 apple2 2.0 2 apple3 3.0 3 apple4 4.0 4 apple5 5.0
我們的目標是變為如下結構:
a apple1 apple2 apple3 apple4 apple5 0 1.0 2.0 3.0 4.0 5.0
乍看可使用pivot
,但很難一步到位。
所以另闢蹊徑,提供一種簡單且好理解的方法:
In [113]: pd.DataFrame(index=[0],columns=df.a,data=dict(zip(df.a,df.val))) Out[113]: a apple1 apple2 apple3 apple4 apple5 0 1.0 2.0 3.0 4.0 5.0
以上方法是重新創建一個DataFrame,直接把df.a
所有可能取值作為新dataframe的列,index調整為[0]
,注意類型必須是數組類型(array-like 或者 Index),兩個軸確定後,data
填充數據域。
In [116]: dict(zip(df.a,df.val)) Out[116]: {'apple1': 1.0, 'apple2': 2.0, 'apple3': 3.0, 'apple4': 4.0, 'apple5': 5.0}
2 導入數據
數據來自kaggle,共包括三個文件:
- movies.dat
- ratings.dat
- users.dat
movies.dat
包括三個欄位:['Movie ID', 'Movie Title', 'Genre']
使用pandas導入此文件:
import pandas as pd movies = pd.read_csv('./data/movietweetings/movies.dat', delimiter='::', engine='python', header=None, names = ['Movie ID', 'Movie Title', 'Genre'])
導入後,顯示前5行:
Movie ID Movie Title 0 8 Edison Kinetoscopic Record of a Sneeze (1894) 1 10 La sortie des usines Lumi貓re (1895) 2 12 The Arrival of a Train (1896) 3 25 The Oxford and Cambridge University Boat Race ... 4 91 Le manoir du diable (1896) 5 131 Une nuit terrible (1896) 6 417 Le voyage dans la lune (1902) 7 439 The Great Train Robbery (1903) 8 443 Hiawatha, the Messiah of the Ojibway (1903) 9 628 The Adventures of Dollie (1908) Genre 0 Documentary|Short 1 Documentary|Short 2 Documentary|Short 3 NaN 4 Short|Horror 5 Short|Comedy|Horror 6 Short|Action|Adventure|Comedy|Fantasy|Sci-Fi 7 Short|Action|Crime|Western 8 NaN 9 Action|Short
次導入其他兩個數據文件
users.dat
:
users = pd.read_csv('./data/movietweetings/users.dat', delimiter='::', engine='python', header=None, names = ['User ID', 'Twitter ID']) print(users.head())
結果:
User ID Twitter ID 0 1 397291295 1 2 40501255 2 3 417333257 3 4 138805259 4 5 2452094989 5 6 391774225 6 7 47317010 7 8 84541461 8 9 2445803544 9 10 995885060
rating.data
:
ratings = pd.read_csv('./data/movietweetings/ratings.dat', delimiter='::', engine='python', header=None, names = ['User ID', 'Movie ID', 'Rating', 'Rating Timestamp']) print(ratings.head())
結果:
User ID Movie ID Rating Rating Timestamp 0 1 111161 10 1373234211 1 1 117060 7 1373415231 2 1 120755 6 1373424360 3 1 317919 6 1373495763 4 1 454876 10 1373621125 5 1 790724 8 1374641320 6 1 882977 8 1372898763 7 1 1229238 9 1373506523 8 1 1288558 5 1373154354 9 1 1300854 8 1377165712
read_csv 使用說明
說明,本次導入dat
文件使用pandas.read_csv
函數。
第一個位置參數./data/movietweetings/ratings.dat
表示文件的相對路徑
第二個關鍵字參數:delimiter='::'
,表示文件分隔符使用::
後面幾個關鍵字參數分別代表使用的引擎,文件沒有表頭,所以header
為None;
導入後dataframe的列名使用names
關鍵字設置,這個參數大家可以記住,比較有用。
Kaggle電影數據集第一節,我們使用數據處理利器 pandas
, 函數read_csv
導入給定的三個數據文件。
import pandas as pd movies = pd.read_csv('./data/movietweetings/movies.dat', delimiter='::', engine='python', header=None, names = ['Movie ID', 'Movie Title', 'Genre']) users = pd.read_csv('./data/movietweetings/users.dat', delimiter='::', engine='python', header=None, names = ['User ID', 'Twitter ID']) ratings = pd.read_csv('./data/movietweetings/ratings.dat', delimiter='::', engine='python', header=None, names = ['User ID', 'Movie ID', 'Rating', 'Rating Timestamp'])
用到的read_csv
,某些重要的參數,如何使用在上一節也有所提到。下面開始數據探索分析(EDA)
找出得分前10喜劇(comedy)
3 處理組合值
表movies
欄位Genre
表示電影的類型,可能有多個值,分隔符為|
,取值也可能為None
.
針對這類欄位取值,可使用Pandas中Series提供的str
做一步轉化,注意它是向量級的,下一步,如Python原生的str
類似,使用contains
判斷是否含有comedy
字元串:
mask = movies.Genre.str.contains('comedy',case=False,na=False)
注意使用的兩個參數:case
, na
case為 False,表示對大小寫不敏感;na Genre列某個單元格為NaN
時,我們使用的充填值,此處填充為False
返回的mask
是一維的Series
,結構與 movies.Genre相同,取值為True 或 False.
觀察結果:
0 False 1 False 2 False 3 False 4 False 5 True 6 True 7 False 8 False 9 False Name: Genre, dtype: bool
4 訪問某列
得到掩碼mask後,pandas非常方便地能提取出目標記錄:
comedy = movies[mask] comdey_ids = comedy['Movie ID']
以上,在pandas中被最頻率使用,不再解釋。看結果comedy_ids.head()
:
5 131 6 417 15 2354 18 3863 19 4099 20 4100 21 4101 22 4210 23 4395 25 4518 Name: Movie ID, dtype: int64
1-4介紹數據讀入
,處理組合值
,索引數據
等, pandas中使用較多的函數,基於Kaggle真實電影影評數據集,最後得到所有喜劇 ID
:
5 131 6 417 15 2354 18 3863 19 4099 20 4100 21 4101 22 4210 23 4395 25 4518 Name: Movie ID, dtype: int64
下面繼續數據探索之旅~
5 連接兩個表
拿到所有喜劇的ID後,要想找出其中平均得分最高的前10喜劇,需要關聯另一張表:ratings
:
再回顧下ratings表結構:
User ID Movie ID Rating Rating Timestamp 0 1 111161 10 1373234211 1 1 117060 7 1373415231 2 1 120755 6 1373424360 3 1 317919 6 1373495763 4 1 454876 10 1373621125 5 1 790724 8 1374641320 6 1 882977 8 1372898763 7 1 1229238 9 1373506523 8 1 1288558 5 1373154354 9 1 1300854 8 1377165712
pandas 中使用join
關聯兩張表,連接欄位是Movie ID
,如果順其自然這麼使用join
:
combine = ratings.join(comedy, on='Movie ID', rsuffix='2')
左右滑動,查看完整程式碼
大家可驗證這種寫法,仔細一看,會發現結果非常詭異。
究其原因,這是pandas join函數使用的一個算是坑點,它在官檔中介紹,連接右表時,此處右表是comedy
,它的index
要求是連接欄位,也就是 Movie ID
.
左表的index不要求,但是要在參數 on
中給定。
以上是要注意的一點
修改為:
combine = ratings.join(comedy.set_index('Movie ID'), on='Movie ID') print(combine.head(10))
以上是OK的寫法
觀察結果:
User ID Movie ID Rating Rating Timestamp Movie Title Genre 0 1 111161 10 1373234211 NaN NaN 1 1 117060 7 1373415231 NaN NaN 2 1 120755 6 1373424360 NaN NaN 3 1 317919 6 1373495763 NaN NaN 4 1 454876 10 1373621125 NaN NaN 5 1 790724 8 1374641320 NaN NaN 6 1 882977 8 1372898763 NaN NaN 7 1 1229238 9 1373506523 NaN NaN 8 1 1288558 5 1373154354 NaN NaN 9 1 1300854 8 1377165712 NaN NaN
Genre列為NaN
表明,這不是喜劇。需要篩選出此列不為NaN
的記錄。
6 按列篩選
pandas最方便的地方,就是向量化運算,儘可能減少了for循環的嵌套。
按列篩選這種常見需求,自然可以輕鬆應對。
為了照顧初次接觸 pandas 的朋友,分兩步去寫:
mask = pd.notnull(combine['Genre'])
結果是一列只含True 或 False
的值
result = combine[mask] print(result.head())
結果中,Genre欄位中至少含有一個Comedy字元串,表明驗證了我們以上操作是OK的。
User ID Movie ID Rating Rating Timestamp Movie Title 12 1 1588173 9 1372821281 Warm Bodies (2013) 13 1 1711425 3 1372604878 21 & Over (2013) 14 1 2024432 8 1372703553 Identity Thief (2013) 17 1 2101441 1 1372633473 Spring Breakers (2012) 28 2 1431045 7 1457733508 Deadpool (2016) Genre 12 Comedy|Horror|Romance 13 Comedy 14 Adventure|Comedy|Crime|Drama 17 Comedy|Crime|Drama 28 Action|Adventure|Comedy|Sci-Fi
截止目前已經求出所有喜劇電影result
,前5行如下,Genre中都含有Comedy
字元串:
User ID Movie ID Rating Rating Timestamp Movie Title 12 1 1588173 9 1372821281 Warm Bodies (2013) 13 1 1711425 3 1372604878 21 & Over (2013) 14 1 2024432 8 1372703553 Identity Thief (2013) 17 1 2101441 1 1372633473 Spring Breakers (2012) 28 2 1431045 7 1457733508 Deadpool (2016) Genre 12 Comedy|Horror|Romance 13 Comedy 14 Adventure|Comedy|Crime|Drama 17 Comedy|Crime|Drama 28 Action|Adventure|Comedy|Sci-Fi
7 按照Movie ID 分組
result中會有很多觀眾對同一部電影的打分,所以要求得分前10的喜劇,先按照Movie ID
分組,然後求出平均值:
score_as_movie = result.groupby('Movie ID').mean()
前5行顯示如下:
User ID Rating Rating Timestamp Movie ID 131 34861.000000 7.0 1.540639e+09 417 34121.409091 8.5 1.458680e+09 2354 6264.000000 8.0 1.456343e+09 3863 43803.000000 10.0 1.430439e+09 4099 25084.500000 7.0 1.450323e+09
8 按照電影得分排序
score_as_movie.sort_values(by='Rating', ascending = False,inplace=True) score_as_movie
前5行顯示如下:
User ID Rating Rating Timestamp Movie ID 7134690 30110.0 10.0 1.524974e+09 416889 1319.0 10.0 1.543320e+09 57840 23589.0 10.0 1.396802e+09 5693562 50266.0 10.0 1.511024e+09 5074 43803.0 10.0 1.428352e+09
都是滿分?這有點奇怪,會不會這些電影都只有幾個人評分,甚至只有1個?評分樣本個數太少,顯然最終的平均分數不具有太強的說服力。
所以,下面要進行每部電影的評分人數統計
9 分組後使用聚合函數
根據Movie ID
分組後,使用count
函數統計每組個數
,只保留count列,最後得到watchs2
:
watchs = result.groupby('Movie ID').agg(['count']) watchs2 = watchs['Rating']['count']
列印前20行:
print(watchs2.head(20))
結果:
Movie ID 131 1 417 22 2354 1 3863 1 4099 2 4100 1 4101 1 4210 1 4395 1 4518 1 4546 2 4936 2 5074 1 5571 1 6177 1 6414 3 6684 1 6689 1 7145 1 7162 2 Name: count, dtype: int64
果然,竟然有這麼多電影的評論數只有1次!樣本個數太少,評論的平均值也就沒有什麼說服力。
查看watchs2
一些重要統計量:
watchs2.describe()
結果:
count 10740.000000 mean 20.192086 std 86.251411 min 1.000000 25% 1.000000 50% 2.000000 75% 7.000000 max 1843.000000 Name: count, dtype: float64
共有10740部喜劇電影被評分,平均打分次數20次,標準差86,75%的電影樣本打分次數小於7次,最小1次,最多1843次。
10 頻率分布直方圖
繪製評論數的頻率分布直方圖,便於更直觀的觀察電影被評論的分布情況。上面分析到,75%的電影打分次數小於7次,所以繪製打分次數小於20次的直方圖:

fig = plt.figure(figsize=(12,8)) histn = plt.hist(watchs2[watchs2 <=19],19,histtype='step') plt.scatter([i+1 for i in range(len(histn[0]))],histn[0])
histn
元祖表示個數和對應的被分割的區間,查看histn[0]
:
array([4383., 1507., 787., 541., 356., 279., 209., 163., 158., 118., 114., 90., 104., 81., 80., 73., 62., 65., 52.])
sum(histn[0]) # 9222
看到電影評論次數1到19次的喜劇電影9222部,共有10740部喜劇電影,大約86%
的喜劇電影評論次數小於20次
,有1518
部電影評論數不小於20次。
我們肯定希望挑選出被評論次數儘可能多的電影,因為難免會有水軍和濫竽充數等異常評論
行為。那麼,如何準確的量化最小抽樣量呢?
11 最小抽樣量
根據統計學的知識,最小抽樣量和Z值、樣本方差和樣本誤差相關,下面給出具體的求解最小樣本量的計算方法。
採用如下計算公式:
此處, 值取為95%的置信度對應的Z值也就是1.96,樣本誤差取為均值的2.5%.
根據以上公式,編寫下面程式碼:
n3 = result.groupby('Movie ID').agg(['count','mean','std']) n3r = n3[n3['Rating']['count']>=20]['Rating']
只計算影評超過20次,且滿足最小樣本量的電影。計算得到的n3r
前5行:
count mean std Movie ID 417 22 8.500000 1.263027 12349 68 8.485294 1.227698 15324 20 8.350000 1.039990 15864 51 8.431373 1.374844 17925 44 8.636364 1.259216
進一步求出最小樣本量:
nmin = (1.96**2*n3r['std']**2) / ( (n3r['mean']*0.025)**2 )
nmin
前5行:
Movie ID 417 135.712480 12349 128.671290 15324 95.349276 15864 163.434005 17925 130.668350
篩選出滿足最小抽樣量的喜劇電影:
n3s = n3r[ n3r['count'] >= nmin ]
結果顯示如下,因此共有173
部電影滿足最小樣本抽樣量。
count mean std Movie ID 53604 129 8.635659 1.230714 57012 207 8.449275 1.537899 70735 224 8.839286 1.190799 75686 209 8.095694 1.358885 88763 296 8.945946 1.026984 ... ... ... ... 6320628 860 7.966279 1.469924 6412452 276 7.510870 1.389529 6662050 22 10.000000 0.000000 6966692 907 8.673649 1.286455 7131622 1102 7.851180 1.751500 173 rows × 3 columns
12 去重和連表
按照平均得分從大到小排序:
n3s_sort = n3s.sort_values(by='mean',ascending=False)
結果:
count mean std Movie ID 6662050 22 10.000000 0.000000 4921860 48 10.000000 0.000000 5262972 28 10.000000 0.000000 5512872 353 9.985836 0.266123 3863552 199 9.010050 1.163372 ... ... ... ... 1291150 647 6.327666 1.785968 2557490 546 6.307692 1.858434 1478839 120 6.200000 0.728761 2177771 485 6.150515 1.523922 1951261 1091 6.083410 1.736127 173 rows × 3 columns
有一個細節容易忽視,因為上面連接的ratings
表Movie ID
會有重複,因為會有多個人評論同一部電影。所以再對n3s_sort
去重:
n3s_drops = n3s_sort.drop_duplicates(subset=['count'])
結果:
count mean std Movie ID 6662050 22 10.000000 0.000000 4921860 48 10.000000 0.000000 5262972 28 10.000000 0.000000 5512872 353 9.985836 0.266123 3863552 199 9.010050 1.163372 ... ... ... ... 1291150 647 6.327666 1.785968 2557490 546 6.307692 1.858434 1478839 120 6.200000 0.728761 2177771 485 6.150515 1.523922 1951261 1091 6.083410 1.736127 157 rows × 3 columns
僅靠Movie ID
還是不知道哪些電影,連接movies
表:
ms = movies.drop_duplicates(subset=['Movie ID']) ms = ms.set_index('Movie ID') n3s_final = n3s_drops.join(ms,on='Movie ID')
13 結果分析
喜劇榜單前50名:
Movie Title Five Minutes (2017) MSG 2 the Messenger (2015) Avengers: Age of Ultron Parody (2015) Be Somebody (2016) Bajrangi Bhaijaan (2015) Back to the Future (1985) La vita bella (1997) The Intouchables (2011) The Sting (1973) Coco (2017) Toy Story 3 (2010) 3 Idiots (2009) Green Book (2018) Dead Poets Society (1989) The Apartment (1960) P.K. (2014) The Truman Show (1998) Am鑼卨ie (2001) Inside Out (2015) Toy Story 4 (2019) Toy Story (1995) Finding Nemo (2003) Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb (1964) Home Alone (1990) Zootopia (2016) Up (2009) Monsters, Inc. (2001) La La Land (2016) Relatos salvajes (2014) En man som heter Ove (2015) Snatch (2000) Lock, Stock and Two Smoking Barrels (1998) How to Train Your Dragon 2 (2014) As Good as It Gets (1997) Guardians of the Galaxy (2014) The Grand Budapest Hotel (2014) Fantastic Mr. Fox (2009) Silver Linings Playbook (2012) Sing Street (2016) Deadpool (2016) Annie Hall (1977) Pride (2014) In Bruges (2008) Big Hero 6 (2014) Groundhog Day (1993) The Breakfast Club (1985) Little Miss Sunshine (2006) Deadpool 2 (2018) The Terminal (2004)
前10名評論數圖:

程式碼:
x = n3s_final['Movie Title'][:10].tolist()[::-1] y = n3s_final['count'][:10].tolist()[::-1] bar = ( Bar() .add_xaxis(x) .add_yaxis('評論數',y,category_gap='50%') .reversal_axis() .set_global_opts(title_opts=opts.TitleOpts(title="喜劇電影被評論次數"), toolbox_opts=opts.ToolboxOpts(),) ) grid = ( Grid(init_opts=opts.InitOpts(theme=ThemeType.LIGHT)) .add(bar, grid_opts=opts.GridOpts(pos_left="30%")) ) grid.render_notebook()
前10名得分圖:

程式碼:
x = n3s_final['Movie Title'][:10].tolist()[::-1] y = n3s_final['mean'][:10].round(3).tolist()[::-1] bar = ( Bar() .add_xaxis(x) .add_yaxis('平均得分',y,category_gap='50%') .reversal_axis() .set_global_opts(title_opts=opts.TitleOpts(title="喜劇電影平均得分"), xaxis_opts=opts.AxisOpts(min_=8.0,name='平均得分'), toolbox_opts=opts.ToolboxOpts(),) ) grid = ( Grid(init_opts=opts.InitOpts(theme=ThemeType.MACARONS)) .add(bar, grid_opts=opts.GridOpts(pos_left="30%")) ) grid.render_notebook()