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

本項目基於Kaggle電影影評數據集,通過這個系列,你將學到如何進行數據探索性分析(EDA),學會使用數據分析利器pandas,會用繪圖包pyecharts,以及EDA時可能遇到的各種實際問題及一些處理技巧。

通過這個小項目,大家將會掌握pandas主要常用函數的使用技巧,matplotlib繪製直方圖,和pyecharts使用邏輯,具體以下13個知識點:

1 創建DataFrame,轉換長數據為寬數據;2 導入數據;3 處理組合值;4索引列;5 連接兩個表;6 按列篩選;

7 按照字段分組;8 按照字段排序;9 分組後使用聚合函數;10 繪製頻率分佈直方圖繪製;11 最小抽樣量的計算方法;12 數據去重;13 結果分析

注意:這些知識點不是散落的,而是通過求出喜劇電影排行榜,這一個目標主線把它們串聯起來。

本項目需要導入的包:

<code>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/<code>

1 創建DataFrame

pandas中一個dataFrame實例:

<code>Out[89]:
a val
0 apple1 1.0
1 apple2 2.0
2 apple3 3.0
3 apple4 4.0
4 apple5 5.0/<code>

我們的目標是變為如下結構:

<code>a  apple1  apple2  apple3  apple4  apple5
0 1.0 2.0 3.0 4.0 5.0/<code>

乍看可使用pivot,但很難一步到位。

所以另闢蹊徑,提供一種簡單且好理解的方法:

<code>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/<code>

以上方法是重新創建一個DataFrame,直接把df.a所有可能取值作為新dataframe的列,index調整為[0],注意類型必須是數組類型(array-like 或者 Index),兩個軸確定後,data填充數據域。

<code>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}/<code>

2 導入數據

數據來自kaggle,共包括三個文件:

  1. movies.dat
  2. ratings.dat
  3. users.dat

movies.dat包括三個字段:['Movie ID', 'Movie Title', 'Genre']

使用pandas導入此文件:

<code>import pandas as pd

movies = pd.read_csv('./data/movietweetings/movies.dat', delimiter='::', engine='python', header=None, names = ['Movie ID', 'Movie Title', 'Genre'])/<code>

導入後,顯示前5行:

<code>   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/<code>

次導入其他兩個數據文件

users.dat:

<code>users = pd.read_csv('./data/movietweetings/users.dat', delimiter='::', engine='python', header=None, names = ['User ID', 'Twitter ID'])
print(users.head())/<code>

結果:

<code>   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/<code>

rating.data:

<code>ratings = pd.read_csv('./data/movietweetings/ratings.dat', delimiter='::', engine='python', header=None, names = ['User ID', 'Movie ID', 'Rating', 'Rating Timestamp'])
print(ratings.head())/<code>

結果:

<code>   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/<code>

read_csv 使用說明

說明,本次導入dat文件使用pandas.read_csv函數。

第一個位置參數./data/movietweetings/ratings.dat 表示文件的相對路徑

第二個關鍵字參數:delimiter='::',表示文件分隔符使用::

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

導入後dataframe的列名使用names關鍵字設置,這個參數大家可以記住,比較有用。

Kaggle電影數據集第一節,我們使用數據處理利器 pandas, 函數read_csv 導入給定的三個數據文件。

<code>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'])/<code>

用到的read_csv,某些重要的參數,如何使用在上一節也有所提到。下面開始數據探索分析(EDA)

找出得分前10喜劇(comedy)

3 處理組合值

表movies字段Genre表示電影的類型,可能有多個值,分隔符為|,取值也可能為None.

針對這類字段取值,可使用Pandas中Series提供的str做一步轉化,注意它是向量級的,下一步,如Python原生的str類似,使用contains判斷是否含有comedy字符串:

<code>mask = movies.Genre.str.contains('comedy',case=False,na=False)/<code>

注意使用的兩個參數:case, na

case為 False,表示對大小寫不敏感;na Genre列某個單元格為NaN時,我們使用的充填值,此處填充為False

返回的mask是一維的Series,結構與 movies.Genre相同,取值為True 或 False.

觀察結果:

<code>0    False
1 False

2 False
3 False
4 False
5 True
6 True
7 False
8 False
9 False
Name: Genre, dtype: bool
/<code>

4 訪問某列

得到掩碼mask後,pandas非常方便地能提取出目標記錄:

<code>comedy = movies[mask]
comdey_ids = comedy['Movie ID']
/<code>

以上,在pandas中被最頻率使用,不再解釋。看結果comedy_ids.head():

<code>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
/<code>

1-4介紹數據讀入,處理組合值,索引數據等, pandas中使用較多的函數,基於Kaggle真實電影影評數據集,最後得到所有喜劇 ID:

<code>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
/<code>

下面繼續數據探索之旅~

5 連接兩個表

拿到所有喜劇的ID後,要想找出其中平均得分最高的前10喜劇,需要關聯另一張表:ratings:

再回顧下ratings表結構:

<code>   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
/<code>

pandas 中使用join關聯兩張表,連接字段是Movie ID,如果順其自然這麼使用join:

<code>combine = ratings.join(comedy, on='Movie ID', rsuffix='2')
/<code>

左右滑動,查看完整代碼

大家可驗證這種寫法,仔細一看,會發現結果非常詭異。

究其原因,這是pandas join函數使用的一個算是坑點,它在官檔中介紹,連接右表時,此處右表是comedy,它的index要求是連接字段,也就是 Movie ID.

左表的index不要求,但是要在參數 on中給定。

以上是要注意的一點

修改為:

<code>combine = ratings.join(comedy.set_index('Movie ID'), on='Movie ID')
print(combine.head(10))
/<code>

以上是OK的寫法

觀察結果:

<code>   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
/<code>

Genre列為NaN表明,這不是喜劇。需要篩選出此列不為NaN 的記錄。

6 按列篩選

pandas最方便的地方,就是向量化運算,儘可能減少了for循環的嵌套。

按列篩選這種常見需求,自然可以輕鬆應對。

為了照顧初次接觸 pandas 的朋友,分兩步去寫:

<code>mask = pd.notnull(combine['Genre'])
/<code>

結果是一列只含True 或 False的值

<code>result = combine[mask]
print(result.head())
/<code>

結果中,Genre字段中至少含有一個Comedy字符串,表明驗證了我們以上操作是OK的。

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

/<code>

截止目前已經求出所有喜劇電影result,前5行如下,Genre中都含有Comedy字符串:

<code>    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/<code>

7 按照Movie ID 分組

result中會有很多觀眾對同一部電影的打分,所以要求得分前10的喜劇,先按照Movie ID分組,然後求出平均值:

<code>score_as_movie = result.groupby('Movie ID').mean()/<code>

前5行顯示如下:

<code>               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/<code>

8 按照電影得分排序

<code>score_as_movie.sort_values(by='Rating', ascending = False,inplace=True)
score_as_movie/<code>

前5行顯示如下:

<code>\tUser ID\tRating\tRating Timestamp
Movie ID\t\t\t
7134690\t30110.0\t10.0\t1.524974e+09
416889\t1319.0\t10.0\t1.543320e+09
57840\t23589.0\t10.0\t1.396802e+09
5693562\t50266.0\t10.0\t1.511024e+09
5074\t43803.0\t10.0\t1.428352e+09/<code>

都是滿分?這有點奇怪,會不會這些電影都只有幾個人評分,甚至只有1個?評分樣本個數太少,顯然最終的平均分數不具有太強的說服力。

所以,下面要進行每部電影的評分人數統計

9 分組後使用聚合函數

根據Movie ID分組後,使用count函數統計每組個數,只保留count列,最後得到watchs2:

<code>watchs = result.groupby('Movie ID').agg(['count'])
watchs2 = watchs['Rating']['count']/<code>

打印前20行:

<code>print(watchs2.head(20))/<code>

結果:

<code>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/<code>

果然,竟然有這麼多電影的評論數只有1次!樣本個數太少,評論的平均值也就沒有什麼說服力。

查看watchs2一些重要統計量:

<code>watchs2.describe()/<code>

結果:

<code>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/<code>

共有10740部喜劇電影被評分,平均打分次數20次,標準差86,75%的電影樣本打分次數小於7次,最小1次,最多1843次。

10 頻率分佈直方圖

繪製評論數的頻率分佈直方圖,便於更直觀的觀察電影被評論的分佈情況。上面分析到,75%的電影打分次數小於7次,所以繪製打分次數小於20次的直方圖:

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

<code>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])/<code>

histn元祖表示個數和對應的被分割的區間,查看histn[0]:

<code>array([4383., 1507.,  787.,  541.,  356.,  279.,  209.,  163.,  158.,
118., 114., 90., 104., 81., 80., 73., 62., 65.,
52.])/<code>
<code>sum(histn[0]) # 9222/<code>

看到電影評論次數1到19次的喜劇電影9222部,共有10740部喜劇電影,大約86%的喜劇電影評論次數小於20次,有1518部電影評論數不小於20次。

我們肯定希望挑選出被評論次數儘可能多的電影,因為難免會有水軍和濫竽充數等異常評論行為。那麼,如何準確的量化最小抽樣量呢?

11 最小抽樣量

根據統計學的知識,最小抽樣量和Z值、樣本方差和樣本誤差相關,下面給出具體的求解最小樣本量的計算方法。

採用如下計算公式:

此處, 值取為95%的置信度對應的Z值也就是1.96,樣本誤差取為均值的2.5%.

根據以上公式,編寫下面代碼:

<code>n3 = result.groupby('Movie ID').agg(['count','mean','std'])
n3r = n3[n3['Rating']['count']>=20]['Rating']/<code>

只計算影評超過20次,且滿足最小樣本量的電影。計算得到的n3r前5行:

<code>\tcount\tmean\tstd
Movie ID\t\t\t
417\t22\t8.500000\t1.263027
12349\t68\t8.485294\t1.227698
15324\t20\t8.350000\t1.039990
15864\t51\t8.431373\t1.374844
17925\t44\t8.636364\t1.259216/<code>

進一步求出最小樣本量:

<code>nmin = (1.96**2*n3r['std']**2) / ( (n3r['mean']*0.025)**2 )/<code>

nmin前5行:

<code>Movie ID
417 135.712480
12349 128.671290
15324 95.349276
15864 163.434005
17925 130.668350/<code>

篩選出滿足最小抽樣量的喜劇電影:

<code>n3s = n3r[ n3r['count'] >= nmin ]/<code>

結果顯示如下,因此共有173部電影滿足最小樣本抽樣量。

<code>count\tmean\tstd
Movie ID\t\t\t
53604\t129\t8.635659\t1.230714
57012\t207\t8.449275\t1.537899
70735\t224\t8.839286\t1.190799
75686\t209\t8.095694\t1.358885
88763\t296\t8.945946\t1.026984
...\t...\t...\t...
6320628\t860\t7.966279\t1.469924

6412452\t276\t7.510870\t1.389529
6662050\t22\t10.000000\t0.000000
6966692\t907\t8.673649\t1.286455
7131622\t1102\t7.851180\t1.751500
173 rows × 3 columns/<code>

12 去重和連表

按照平均得分從大到小排序:

<code>n3s_sort = n3s.sort_values(by='mean',ascending=False)/<code>

結果:

<code>\tcount\tmean\tstd
Movie ID\t\t\t
6662050\t22\t10.000000\t0.000000
4921860\t48\t10.000000\t0.000000
5262972\t28\t10.000000\t0.000000
5512872\t353\t9.985836\t0.266123
3863552\t199\t9.010050\t1.163372
...\t...\t...\t...
1291150\t647\t6.327666\t1.785968
2557490\t546\t6.307692\t1.858434
1478839\t120\t6.200000\t0.728761
2177771\t485\t6.150515\t1.523922
1951261\t1091\t6.083410\t1.736127
173 rows × 3 columns/<code>

有一個細節容易忽視,因為上面連接的ratings表Movie ID會有重複,因為會有多個人評論同一部電影。所以再對n3s_sort去重:

<code>n3s_drops = n3s_sort.drop_duplicates(subset=['count'])/<code>

結果:

<code>\tcount\tmean\tstd
Movie ID\t\t\t
6662050\t22\t10.000000\t0.000000
4921860\t48\t10.000000\t0.000000
5262972\t28\t10.000000\t0.000000
5512872\t353\t9.985836\t0.266123
3863552\t199\t9.010050\t1.163372
...\t...\t...\t...
1291150\t647\t6.327666\t1.785968

2557490\t546\t6.307692\t1.858434
1478839\t120\t6.200000\t0.728761
2177771\t485\t6.150515\t1.523922
1951261\t1091\t6.083410\t1.736127
157 rows × 3 columns/<code>

僅靠Movie ID還是不知道哪些電影,連接movies表:

<code>ms = movies.drop_duplicates(subset=['Movie ID'])
ms = ms.set_index('Movie ID')
n3s_final = n3s_drops.join(ms,on='Movie ID')/<code>

13 結果分析

喜劇榜單前50名:

<code>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)/<code>

前10名評論數圖:

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

代碼:

<code>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()/<code>

前10名得分圖:


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

代碼:

<code>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%"))/<code>


分享到:


相關文章: