資料分析師的尋常一日的簡短分析:IMDB dataset¶
今天要使用 python pandas 函式讀取的是 IMDB 提供的 dataset。 在這個 dataset 中,總共有七個檔案,都用 gzip 壓縮,而分別是:
檔案名稱 | 簡短中文註解 | 是否使用 |
---|---|---|
title.akas.tsv.gz | 影片的語言和地區等較為國際性的資料 | No |
title.basics.tsv.gz | 影片的基本資料,如時間和發行年代 | Yes |
title.crew.tsv.gz | 包括電影與人員有關,主要是導演和編劇的資料 | Yes |
title.episode.tsv.gz | 和電視影集相關的資訊 | No |
title.principals.tsv.gz | 包括影片較為詳細的主要工作人員資料 | Mo |
title.ratings.tsv.gz | 包括觀眾對於影片的評價資料 | No |
name.basics.tsv.gz | 包括電影工作人員包括導演等的相關資料 | Yes |
如表格所示,今天我們只會用到三個檔案:title.basics.tsv, title.crew.tsv 和 name.basics.tsv。
今天的分析,是想知道《從前,有個好萊塢 ..》的導演昆丁塔倫提諾,是否有拍片長度變長的趨勢。這個問題來自於光與影的臉書貼文,其中提到一個關鍵是,昆丁塔倫提諾在 2010 年失去了長期合作的剪輯師後,導演的電影皆呈現冗長的傾向。
分析的流程是:先讀入檔案,找出昆丁塔倫提諾的名字代碼(name.basics.tsv),對應到角色為導演的電影(title.crew.tsv),取出電影的官方公佈時間長(title.basics.tsv),繪製昆丁塔倫提諾導演在執導期間的所有片長時間。
import pandas as pd
import csv
basics = pd.read_csv('../input/title.basics.tsv', index_col='tconst',
delimiter ='\t', na_values=['\\N', '\\\\N'])
basics.dtypes
檢查 title.basics 的表格時,發現 runtimeMinutes 的資料型態為 object,這顯然不是我們希望的結果,我們得去找出問題出在哪裡。第一個除錯的地方可能是希望把未能成功轉成數值型態的值給列印出來。
cond = basics['runtimeMinutes'].str.contains('[a-zA-Z]+', na=False)
w = pd.read_csv('../input/title.basics.tsv', delimiter ='\t')
cond = cond.reset_index()
w[cond['runtimeMinutes']]
!sed -n '1102408, 1102408'p ../input/title.basics.tsv
藉由列印出有問題的列,可以發現原檔案中有使用雙引號,但其數量是不對稱的,也就是說用在 primaryTitle 欄位的雙引號,包住了分隔符號,也就是 \t 造成原本分配給 primaryTitle 和 originalTitle 兩個欄位的值,在不當使用雙引號後,全都分配給了 primaryTitle ,而造成了解析後的欄位減少! 看到這裡,有些新手可能會覺得恐慌,難道我們要改寫 pandas 嗎?但事實上,解決這個問題非常簡單,如果你熟悉 csv 檔的話!這時,就讓我們垂降至架構 pandas.read_csv 底層的函示,也是 python standard library csv 模組。我們可以看到,在 csv 模組 中針對 quoting (引號)定義了一些在寫入時,如何處理引號的部分。 他們包括了:
- csv.QUOTE_ALL: 不管每一個欄位裝什麼東西,一律都用引用符號(可以是單引號或是雙引號)來包住。
- csv.QUOTE_MINIMAL: 只要該欄位有特別的字元,如分隔符號或甚至引號到分行符號,即使用引用符號來包住。
- csv.QUOTE_NONNUMERIC:同上,但使用引號的標準是當欄位包括文字時,或非數字的字元,且將未能使用引號的欄位,轉成浮點數型態。
- csv.QUOTE_NONE:不使用引用
看起來 csv.QUOTE_NONE 比較像我們的需求,所以我們將這個參數帶入 pandas.read_csv 中
basics = pd.read_csv('../input/title.basics.tsv', index_col='tconst',
delimiter ='\t', na_values=['\\N', '\\\\N'], quoting=csv.QUOTE_NONE)
basics.dtypes
很好,我們順利的將 runtimeMinutes 轉成了 float64。但,大家可能覺得很奇怪,包括 startYear,endYear 為何是 float 型態。這一切都是萬惡的 np.nan 主要型態是 float,而且沒有 integer 的對應版本。大家可以看以下的表格,了解 missing values 在不同的資料型態被表示的方式。
Type | Represents missing values | |
---|---|---|
floating : float64, float32, float16 | np.nan | |
integer : int64, int32, int8, uint64,uint32, uint8 | ||
boolean | ||
datetime64[ns] | NaT | |
timedelta64[ns] | NaT | |
categorical : see the section below | ||
object : strings | np.nan |
但為了節省記憶體空間,我們把這些資料型態從 64 位元壓縮到 32 位元,畢竟他們原本是正整數。
for col in basics.columns:
if str(basics[col].dtype).endswith('64'):
from_dtype = str(basics[col].dtype)[:-2]
basics[col] = basics[col].astype('%s32'%from_dtype)
basics.dtypes
crew = pd.read_csv('../input/title.crew.tsv', delimiter ='\t', na_values=['\\N', '\\\\N'], index_col='tconst')
接著我們來讀進其他檔案。這些檔案和主要檔案的關係如下圖:
crew.head()
name = pd.read_csv('../input/name.basics.tsv', delimiter ='\t', na_values=['\\N', '\\\\N'],
index_col='nconst')
name.head()
from pandas.io.json import build_table_schema
build_table_schema(crew)
我們利用 pandas 的 str accessor 來進行對每一列的字串型態資料來進行比對,這個版本會呼叫 python 標準函式庫 re 模組的 match,也就是會做 c library 呼叫。不過,先確認我們的電影人員名單上只有一個 'Quentin Tarantino',我們可以用 sum 這個 aggregation,確定這個世界上只有一個 Quentin Tarantino(昆丁塔倫提諾)。
assert(name.primaryName.str.match('Quentin Tarantino').sum()==1)
太好了!並沒有複製人出現!接著我們需要取出昆丁導演的名字代碼,要取出代碼就要使用由 str.match 傳為的真假值。我們知道只有一列的名稱為真,所以我們可以使用 pandas.Series 或是 pandas.DataFrame 的 where 方法,建構出與輸入大小相等,型態相同的資料結構。那就是,若輸入為 pandas.Series 則回傳 Series 或 pandas.DataFrame 則回傳 DataFrame。where 方法對於假值(回傳 False)預設值為 np.nan,但 cllient code 也可以指定要回傳哪一個值。
name.where(name.primaryName.str.match('Quentin Tarantino', na=False)).head()
build_table_schema(name)
然而,where 方法的輸出,比較適合將已 apply 過濾的條件式做數值運算,如 sum 等函式,具有處理 np.nan 的能力。但這個能力,在我們的目的並沒有任何施展的空間,因為我們只單純的想把真值對應到的列取出,此時我們就可以用 boolean index,用法如下:
qt = name[name.primaryName.str.match('Quentin Tarantino', na=False)]
qt.head()
在這裡我們再次應用 str.find 的方法,來找出所有的導演列表中,包含昆丁導演的名字代碼。一樣我們也取出由昆丁導演執導的電影。
qt_result = crew[crew['directors'].str.find(qt.index[0]) >= 0]
qt_result.head()
根據我們的 table schema,我們現在要找出所有昆丁導演執導的片。但在這裡值得一提的字串匹配都需要昂貴的計算量,但,我們可以將這些字串 index 起來,為了能快速提取符合 index 的值,多數成為 index 的 raw 值都必須經過前處理,如使用雜湊表(hash)產生一個獨特的數值,而非字串。 想當然爾,比較雜湊表產生的獨特數值要比字串容易多了。
其次,因為 join table 是一個相當昂貴的運算,所以我們希望用於該運算的資料是愈精簡愈好,所以 basics table 中就先限制只屬於 movie 的類別,隨後再以 pandas.merge 來做 join table 的部分。
%%timeit
movie_types = basics[basics['titleType'] == 'movie']
qt_movies = pd.merge(qt_result.reset_index(), movie_types.reset_index(), on='tconst')
%%timeit
movie_types = basics[basics['titleType'] == 'movie']
qt_movies = pd.merge(qt_result, movie_types, left_index=True, right_index=True)
movie_types = basics[basics['titleType'] == 'movie']
qt_movies = pd.merge(qt_result.reset_index(), movie_types.reset_index(), on='tconst')
qt_movies.head()
qt_movies = qt_movies.sort_values(by='startYear')
#qt_movies = qt_movies.reset_index()
qt_movies.tail()
qt_movies[~qt_movies['runtimeMinutes'].isnull()].tail()
qt_movies = qt_movies[~qt_movies['runtimeMinutes'].isnull()]
%matplotlib inline
import matplotlib.pyplot as plt
year_start = qt_movies['startYear'].min()
year_end = qt_movies['startYear'].max()
movies_qt_subset = movie_types[(movie_types['startYear'] >= year_start) & (movie_types['startYear'] <= year_end)]
gt2010_mean = qt_movies.loc[qt_movies['startYear'] >= 2010, 'runtimeMinutes'].mean()
lt2010_mean = qt_movies.loc[qt_movies['startYear'] < 2010, 'runtimeMinutes'].mean()
movies_qt_subset.head()
group_by_year = movies_qt_subset[['startYear', 'runtimeMinutes']].sort_index().groupby('startYear')
average_run = group_by_year.mean()
std_run = group_by_year.std()
import numpy as np
fig = plt.gcf()
fig.set_figheight(5)
fig.set_figwidth(15)
plt.plot(qt_movies['startYear'], qt_movies['runtimeMinutes'], 'x-', label='Quentin Tarantino')
plt.plot(average_run.index.values, average_run['runtimeMinutes'], label='Background', color='orange')
plt.xlabel("Year")
plt.ylabel("Minutes")
lower_run = np.clip(average_run['runtimeMinutes'] - std_run['runtimeMinutes'], np.inf, 0)
# TODO: adding movie titles
plt.fill(std_run.index.values, lower_run,
average_run['runtimeMinutes'] + std_run['runtimeMinutes'], alpha=0.5, color='orange')
for i in range(0, len(qt_movies)):
if i % 2 == 0 and i != 10 and i != 14:
plt.text(qt_movies.loc[i, 'startYear'], qt_movies.loc[i, 'runtimeMinutes'] + 10,
qt_movies.loc[i, 'primaryTitle'], rotation=45, ha="left", va="bottom")
elif i != 11 and i != 14:
plt.text(qt_movies.loc[i, 'startYear'], qt_movies.loc[i, 'runtimeMinutes'] - 10,
qt_movies.loc[i, 'primaryTitle'], rotation=-45, ha="left", va="top")
plt.text(qt_movies.loc[10, 'startYear'], qt_movies.loc[10, 'runtimeMinutes'] - 10,
qt_movies.loc[10, 'primaryTitle'], rotation=-45, ha="left", va="top")
plt.text(qt_movies.loc[11, 'startYear'], qt_movies.loc[11, 'runtimeMinutes'] + 10,
qt_movies.loc[11, 'primaryTitle'], rotation=30, ha="left", va="bottom")
plt.text(qt_movies.loc[14, 'startYear'] - 0.5, qt_movies.loc[14, 'runtimeMinutes'] + 10,
"Once Upon a Time in...\n Hollywood", rotation=55, ha="left", va="bottom", wrap=True)
max_time = max(average_run['runtimeMinutes'] + std_run['runtimeMinutes'])
plt.plot((2010, 2010), (0, max_time), '--', color='r')
plt.text(2011, max_time + 100, '>=2010 mean runtimeMinutes={:.2f}'.format(gt2010_mean),
ha="left", fontsize='x-large')
plt.text(2009, max_time + 100, '<2010 mean runtimeMinutes={:.2f}'.format(lt2010_mean),
ha="right", fontsize='x-large')
plt.legend(loc="best");
今天的尋常一日的教學就到這裡了!讓讀者帶回家並放在枕頭下,好在睡夢中也溫習的訊息有:
- 不要過度依賴 pandas.read_csv 的預設值,藉由檢視 pandas 讀進的資料型態,了解是否有解析錯誤。
- 在做 table join 時,若 join type 為字串時,盡量使之成為 index,會大幅減少運算時間。
最後,希望各位讀者都有收穫。