記一次美妙的數據分析之旅~

  • 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,共包括三個文件:

  1. movies.dat
  2. ratings.dat
  3. 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='::',表示文件分隔符使用::

後面幾個關鍵字參數分別代表使用的引擎,文件沒有表頭,所以headerNone;

導入後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

有一個細節容易忽視,因為上面連接的ratingsMovie 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()