見出し画像

Netflixの視聴データで人気動画コンテンツの法則を考察


はじめに

自己紹介

通信会社でDXビジネス企画に従事しながらAidemyでデータ分析を勉強中

分析に至った背景

Netflixのビジネスモデルは、DXの最も成功した事例の1つとして、もはや経典のような存在である。

それはネット上で映画やドラマが見れるようになったという単純な理由ではない。これまでAIの不可侵領域とされてたエンタメ分野に一石を投じたからである。

膨大な視聴データから「どんな内容ならウケるか」を分析し、ヒット作を連発することで、Netflixは圧倒的なエンタメコンテンツとして他の追随を許さない存在となった。つまり「面白い動画コンテンツは再現性がある」ということだ。

「我々はデータドリブンな会社である」とCEOが語ったように、まさにデータで成功した最高峰のDXの片鱗を掴みたく、今回の分析に至った。

分析の流れ

  1. データを眺め、分析の方針を立てる

  2. 様々な予測モデルを試してみる

分析環境

・言語:Pyton3
・環境:Google Colabolatory
・MacOS:13.2.1(22D68)
・チップ:Apple M1

1. データを眺め、分析の方針を立てる

1-1. データの取得

Netflixの視聴データをKaggleから取得し、pandasで読み込む。
今回はGoogle Colabolatoryを用いるため、専用のdriveをマウントする。

import pandas as pd
 #driveのモジュールをインポート 
from google.colab import drive
 #driveのマウント 
drive.mount('/content/drive')

all_df = pd.read_csv('drive/My Drive/Colab Notebooks/imdb_movies_shows.csv')

データの特徴量解説
title : 映画または番組の名前
type : 映画と番組を区別
release_year : コンテンツがリリースされた年
age_certification : 年齢適合性の評価
runtime : 映画または番組の継続時間 (分単位)
genres: ジャンル(コメディ、ドラマ、ホラーなど)
Production_countries : コンテンツが制作された国
seasons: コンテンツの範囲を示すシーズン数 (番組に適用)
imdb_id : IMDb 上の各タイトルの一意の識別子
imdb_score : コンテンツの IMDb 評価(人気と品質を反映)
imdb_votes : 投票数(視聴者のエンゲージメントと人気を示す)

1-2. データを眺めてみる

分析の方針を立てるために、データの外観を確認。

def summary(df):
    print(f'data shape: {df.shape}')
    summ = pd.DataFrame(df.dtypes, columns=['data type'])
    #nullの数 
    summ['#missing'] = df.isnull().sum().values
    #nulllの割合 
    summ['%missing'] = df.isnull().sum().values / len(df) * 100
    #uniqueの数 
    summ['#unique'] = df.nunique().values

    desc = pd.DataFrame(df.describe(include='all').transpose())
    summ['min'] = desc['min'].values
    summ['max'] = desc['max'].values
    summ['average'] = desc['mean'].values
    summ['standard_deviation'] = desc['std'].values
    summ['first value'] = df.loc[0].values

    return summ

summary(all_df).style.background_gradient(cmap='YlOrBr')

結果
data shape: (5806, 11)

データサマリー

ここまでで分かったこと
・5806タイトルと11の特徴量が格納されている
・age_certificationの欠損数が多く、重要な指標だが使用不可
・seasonsの欠損数が多く、使用不可
・imdb_scoreの欠損数は少なく、補完すれば使用可能
・imdb_votesの欠損数は少なく、補完すれば使用可能

上記から、imdb_scoreとimdb_votesの欠損を補完する。
imdb_scoreは平均値、imdb_votesはminとmaxの差が非常に大きいため中央値で補完する。

 #欠損が523箇所あるため平均値で補完 
all_df['imdb_score'] = all_df['imdb_score'].fillna(all_df['imdb_score'].mean())
 #欠損が539箇所あるため中央値で補完 (標準偏差が大きくばらつきの影響を抑えるため)
all_df['imdb_votes'] = all_df['imdb_votes'].fillna(all_df['imdb_votes'].median())

1-3. データをtypeの観点で考察

type別にもう少し詳しい状況を把握するため、ヒストグラムを用いて可視化する。

 #可視化に用いる特徴量を抽出 
features = ["release_year","runtime","seasons","imdb_score","imdb_votes"]
 #番組と映画の数の比較 
sns.countplot(data=all_df,x="type")

n_cols = len(features)
fig, axs = plt.subplots(n_cols, 1, figsize=(10, n_cols*3))
 #様々な特徴量でのヒストグラムをtype別に可視化 
for idx, col in enumerate(features):
    sns.histplot(data=all_df, x=col, ax=axs[idx], bins=20, hue="type",multiple='dodge')
    axs[idx].set_title(f'Histogram of {col}')
 #x軸のメモリフォーマットの設定 
plt.ticklabel_format(style='plain') #グラフが重ならないようにレイアウトを調整 
plt.tight_layout()
plt.show()

結果

type別(SHOW or MOVIE)の数の比較
様々な特徴量をtype別にヒストグラムで可視化

ここまでで分かったこと
・番組と映画は4:6程度
・番組と映画は両方とも2020年以降の作品がほとんど
・runtimeは番組と映画で差異あり
・imdb_scoreも番組と映画で差異があり、映画の方が若干低め
・imdb_votesはタイプ別に差異はほぼなく、50万以下がほとんど

上記から、特徴量'type'での差異が確認されたため、重要指標として分析に使用するためにOne-Hot Encodeingで数値化する。

 #カテゴリカル変数をOne -Hot Encodeingで数値化
all_df = pd.get_dummies(all_df, columns = ['type']) 

1-4. データをgenresの観点で考察

genres別にもう少し詳しい状況を把握するため、ヒストグラムを用いて可視化する。

 #ジャンル毎にカウントしてヒストグラムで可視化 
all_df['genres']
all_df['genres'].value_counts().head(10).plot(kind='bar')
plt.show()


genres別の出現回数上位10項目

'comedy'や'drama'が多いということが分かる一方で、'comedy,drama'と複数の要素が踏まれるパターンがあり、また'drama,comedy'は別でカウントされてしまっているので、考察するにはgenresの要素毎に分解してカウントする必要がある。

 #genres毎のimdb_scoreの平均値を算出  #後半で利用 
dic_gen_per_score = {}
for gen in all_df["genres"].unique():
    dic_gen_per_score[gen] = all_df[all_df['genres'].isin([gen])]['imdb_score'].mean()
 #imdb_score_meanで降順 
dic_gen_per_score = sorted(dic_gen_per_score.items(), key = lambda x:x[1], reverse = True)
 #DataFrameに変換 
df_gen_per_score = pd.DataFrame(dic_gen_per_score,columns=['genres','imdb_score'])
 #記号を排除する関数を定義 
def symbol_filter(text):

  target_parts_of_speech = [
           "名詞-サ変接続", 
           "名詞-形容動詞語幹", 
           "名詞-一般", 
           "名詞-固有名詞-一般", 
           "名詞-固有名詞-組織", 
           "形容詞-自立"
           ]

  result_word_list = []

  #chasenの出力フォーマットを定義 
  CHASEN_ARGS = r' -F "%m\t%f[7]\t%f[6]\t%F-[0,1,2,3]\t%f[4]\t%f[5]\n"'
  CHASEN_ARGS += r' -U "%m\t%m\t%m\t%F-[0,1,2,3]\t\t\n"'
  tagger = MeCab.Tagger(ipadic.MECAB_ARGS + CHASEN_ARGS)
 
  #文字列を分割 
  for line in tagger.parse(text).splitlines():
    if line is None or line == '' or line == 'EOS' or len(line.split()) < 4:
        continue
    #品詞を削除 
    for target_part_of_speech in target_parts_of_speech:
        if target_part_of_speech == line.split()[3]:
                word = line.split()[2]
                result_word_list.append(word)
  return result_word_list
 #genresの要素を抽出するリスト 
genres_token = []
 #imdb_scoreの平均値上位100にgenresを解析 
tokenizer = Tokenizer() #genresを抽出 
for genres in df_gen_per_score.genres.values:
    #genresをわかち書きして要素を取り出す 
    for text in tokenizer.tokenize(genres,wakati=True):
        #記号を排除 
        token = symbol_filter(text)
        #空白文字を排除 
        if len(token) != 0:
          genres_token.append(token[0])
 #genres_tokenの重複をカウント 
genres_token_counter = dict(collections.Counter(genres_token))
genres_token_counter = sorted(genres_token_counter.items(), key = lambda x:x[1], reverse = True)
genres_token_counter = pd.DataFrame(genres_token_counter, columns=['token','num_token']) #割合の列を追加 
genres_token_counter["Percentage"]=(genres_token_counter['num_token']/genres_token_counter.num_token.values.sum())*100 #表示 
display(genres_token_counter)

結果

genresの要素毎の出現回数と割合

genresに含まれる要素はドラマ、アクション、コメディなど偏りが大きい。imdb_scoreとgenresの関係を考察するため、genres毎のimdb_scoreの平均値が7.0以上の高得点であるタイトルのgenresに含まれる要素を確認。

 #imdb_scoreの平均値が7 .0より大きい項目を抽出
df_gen_per_score = df_gen_per_score[df_gen_per_score['imdb_score'] >= 7.0]

結果

imbd_scoreの平均値が7.0以上のgenresの要素毎の出現回数と割合

ここまでで分かったこと
・imdb_scoreとの関連が強そうなgenresの要素は見つからない
・ドラマ×アクションなど、組み合わせによってはimdb_scoreとの関連が強化される可能性がある

上記からgenresの要素の有無を新たな特徴量とする。

 #genresから要素を取り出す関数を定義 
def symbol_filter(text):
  target_parts_of_speech = [
           "名詞-サ変接続", 
           "名詞-形容動詞語幹", 
           "名詞-一般", 
           "名詞-固有名詞-一般", 
           "名詞-固有名詞-組織", 
           "形容詞-自立"
           ]

  result_word_list = []

  #chasenの出力フォーマットを定義 
  CHASEN_ARGS = r' -F "%m\t%f[7]\t%f[6]\t%F-[0,1,2,3]\t%f[4]\t%f[5]\n"'
  CHASEN_ARGS += r' -U "%m\t%m\t%m\t%F-[0,1,2,3]\t\t\n"'
  tagger = MeCab.Tagger(ipadic.MECAB_ARGS + CHASEN_ARGS)
 
  #文字列を分割 
  for line in tagger.parse(text).splitlines():
    if line is None or line == '' or line == 'EOS' or len(line.split()) < 4:
        continue
    #品詞を削除 
    for target_part_of_speech in target_parts_of_speech:
        if target_part_of_speech == line.split()[3]:
                word = line.split()[2]
                result_word_list.append(word)
  return result_word_list
 #genresの要素を特徴量に加えるための関数を定義 
def genres_token_func(df,features_genres_token):
    for index in range(0,df.shape[0]):
        for fea in features_genres_token:
            for token in symbol_filter(df['genres'][index]):
                if token == fea or df[f'genres_{fea}'][index] == 1:
                                     #genresの要素が含まれれば1 
                  df[f'genres_{fea}'][index] = 1
                else:
                  #genresの要素が含まれなければ0 
                  df[f'genres_{fea}'][index] = 0       
    return df
 #genresの要素 
features_genres_token = ['drama','action','comedy','fantasy','animation','scifi','thriller','crime',
                           'romance','family','documentation','history','european','war','horror',
                           'sport','music','reality','western']
 #データフレームに枠を新たな特徴量を加えるカラムを追加 
for fea in features_genres_token:
    all_df[f'genres_{fea}']= 0  

all_df = genres_token_func(all_df,features_genres_token)
display(all_df)
新たな特徴量として加えたgenresの要素の有無

2. 様々な予測モデルを試してみる

2-1. 準備

 #imdb_scoreを予測する 
target = all_df['imdb_score']
 #学習に用いないカラムを削除 
drop_col = ['title','production_countries','genres','age_certification','seasons','imdb_id','imdb_score']
all_df_2 = all_df.drop(drop_col, axis=1)
display(all_df_2)
学習に用いるデータ

検証方法はK-分割交差検証法を用い、分割数はK=10とする。

2-2. ロジスティック回帰

from sklearn.model_selection import KFold
 #k -分割交差検証を用いる #分割数10とする 
cv = KFold(n_splits=10,random_state=0,shuffle=True)

train_acc_list_kf = []
val_acc_list_kf = []

# fold毎に学習データのインデックスと評価データのインデックスを得る
for i ,(trn_index, val_index) in enumerate(cv.split(all_df_2, target)):

    print(f'Fold : {i}')
    # データ全体(Xとy)を学習データと評価データに分割
    X_train_kf ,X_val_kf = all_df_2.loc[trn_index], all_df_2.loc[val_index]
    y_train_kf ,y_val_kf = target[trn_index], target[val_index]

    #データを整形する 
    X_train_kf = np.array(X_train_kf,dtype=int)
    X_val_kf = np.array(X_val_kf,dtype=int)
    y_train_kf = np.array(y_train_kf,dtype=int)
    y_val_kf = np.array(y_val_kf,dtype=int)
    
    model_kf = LogisticRegression()
    model_kf.fit(X_train_kf,y_train_kf)

   # Train Part
    y_pred_kf = model_kf.predict(X_train_kf)
    train_acc_kf = accuracy_score(y_train_kf,y_pred_kf)
    print(train_acc_kf)
    train_acc_list_kf.append(train_acc_kf)
    
    # Valid Part
    y_pred_val_kf = model_kf.predict(X_val_kf)
    val_acc_kf = accuracy_score(y_val_kf,y_pred_val_kf)
    print(val_acc_kf)
    val_acc_list_kf.append(val_acc_kf)

print('-'*10 + 'Result' +'-'*10)
print(f'Train_acc Ave : {np.mean(train_acc_list_kf)}')
print(f'Valid_acc Ave : {np.mean(val_acc_list_kf)}')

結果
Train_acc Ave : 0.3970221619176843
Valid_acc Ave : 0.3971701584663778

2-3. SVC分類

from sklearn.model_selection import KFold
from sklearn.svm import SVC
 #k -分割交差検証を用いる #分割数10とする 
cv = KFold(n_splits=10,random_state=0,shuffle=True)

train_acc_list_svc = []
val_acc_list_svc = []

for i ,(trn_index, val_index) in enumerate(cv.split(all_df_2, target)):

    print(f'Fold : {i}')
    # データ全体(Xとy)を学習データと評価データに分割
    X_train_svc ,X_val_svc = all_df_2.loc[trn_index], all_df_2.loc[val_index]
    y_train_svc ,y_val_svc = target[trn_index], target[val_index]

    #データを整形する 
    X_train_svc = np.array(X_train_svc,dtype=int)
    X_val_svc = np.array(X_val_svc,dtype=int)
    y_train_svc = np.array(y_train_svc,dtype=int)
    y_val_svc = np.array(y_val_svc,dtype=int)
    
    model_svc = SVC(random_state=0)
    model_svc.fit(X_train_svc,y_train_svc)

   # Train Part
    y_pred_svc = model_svc.predict(X_train_svc)
    train_acc_svc = accuracy_score(y_train_svc,y_pred_svc)
    print(train_acc_svc)
    train_acc_list_svc.append(train_acc_svc)
    
    # Valid Part
    y_pred_val_svc = model_svc.predict(X_val_svc)
    val_acc_svc = accuracy_score(y_val_svc,y_pred_val_svc)
    print(val_acc_svc)
    val_acc_list_svc.append(val_acc_svc)

print('-'*10 + 'Result' +'-'*10)
print(f'Train_acc Ave : {np.mean(train_acc_list_svc)}')
print(f'Valid_acc Ave : {np.mean(val_acc_list_svc)}')

結果
Train_acc Ave : 0.3986679997143469
Valid_acc Ave : 0.39717134548044397

2-4. ランダムフォレスト

from sklearn.model_selection import KFold
from sklearn.ensemble import RandomForestClassifier
 #k -分割交差検証を用いる #分割数10とする 
cv = KFold(n_splits=10,random_state=0,shuffle=True)

train_acc_list_rf = []
val_acc_list_rf = []
 #モデルを学習させ交差検証法で評価 
for i ,(trn_index, val_index) in enumerate(cv.split(all_df_2, target)):

    print(f'Fold : {i}')
    # データ全体(Xとy)を学習データと評価データに分割
    X_train_rf ,X_val_rf = all_df_2.loc[trn_index], all_df_2.loc[val_index]
    y_train_rf ,y_val_rf = target[trn_index], target[val_index]

    #データを整形する 
    X_train_rf = np.array(X_train_rf,dtype=int)
    X_val_rf = np.array(X_val_rf,dtype=int)
    y_train_rf = np.array(y_train_rf,dtype=int)
    y_val_rf = np.array(y_val_rf,dtype=int)
    
    model_rf = RandomForestClassifier(random_state=0)
    model_rf.fit(X_train_rf, y_train_rf)
    
    y_pred_rf = model_rf.predict(X_train_rf)
    train_acc_rf = accuracy_score(y_train_rf,y_pred_rf)
    
    print(train_acc_rf)
    train_acc_list_rf.append(train_acc_rf)
    
    y_pred_val_rf = model_rf.predict(X_val_rf)
    val_acc_rf = accuracy_score(y_val_rf,y_pred_val_rf)
    
    print(val_acc_rf)
    val_acc_list_rf.append(val_acc_rf)


print('-'*10 + 'Result' +'-'*10)
print(f'Train_acc Ave : {np.mean(train_acc_list_rf)}')
print(f'Valid_acc Ave : {np.mean(val_acc_list_rf)}')

結果
Train_acc Ave : 0.9999808612440191(なぜこうなったのか不明)
Valid_acc Ave : 0.4781298593388332

2-5. 考察

・ロジスティック回帰、SVC分類、ランダムフォレストを用いて予測したが、いずれも正答率の平均が0.5を超えないため、genresの要素の組みわせだけではimdb_scoreを正確に予測することは難しいことが分かった。

・今回の学習では用いなかったtitleを考察し、人気タイトルに含まれているワードを分析し特徴量に加えたら精度が向上するかもしれない。

最後に
このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています。


この記事が気に入ったらサポートをしてみませんか?