LoginSignup
2
4

More than 1 year has passed since last update.

公式のAPIで取得できる情報からyoutube動画の再生回数を予測する

Last updated at Posted at 2022-10-22

1.はじめに

初めまして、友と申します。社会人になって20年以上医療業界で働いてきましたが、元々興味があった プログラミングを学びたいと一念発起し、半年ほど前からPythonの講座を受講してきました。
プログラミングを学ぶのに対象となる言語は数多くあると思いますが、昨今のAIブームで様々なニュースを目にすることが多くなりました。
その中で特に驚いたものが、AIに動物の動画を5000ほど学習させることで「猫」という動物を正しく分類できるようになったというものでした。それまでの自分の認識では、機械の学習というものは正確に人間が分類するべき特徴を指定して、それを機械的に分類していくものだと考えていたので、衝撃的なニュースでした。
このニュースから、現在の機械学習というものは人間が気づくもの以上の傾向や特徴に気付いたり、それを活用できるのではないかと考えるようになり、自分が働いている医療の分野でも大いに活躍の機会があると期待を持つようになりました。
まだ知識もなかった自分ですが、AIや機械学習といえばPythonと考え学習を決意しました。

2.本記事の概要

今回自分が約半年間、プログラミングの講座で学んだ成果として表題のようにyoutube動画の再生回数を予測する機械学習モデルを作成してみました。
全くの素人が、オンラインスクールでPythonを学ぶことでどの程度の知識や技術を身につけることができるのか、また、一つの成果物として形を成すものを作成するに至れることを、これからプログラミングを始めてみようと考える方や、既に学んでいる方、果てはプロの方にも見ていただければと思います。

3.実行環境

Mac book proを使用して開発しました。エディタはVSCodeを使用し、実行も同環境で行っています。

4.作成したプログラム

コードが長くなり、ループ文の処理が非常に長くなってしまうことや、自身での管理が難しくなってしまうところ、自分の技術力の問題もあり、4つのパートに分かれています。途中csvファイルに書き出す作業が入ります。

4-1.使用するライブラリ

  1. urllib
  2. pandas
  3. Mecab
  4. numpy
  5. seaborn
  6. matplotlib
  7. tkinter
  8. sklearn

4-2.データの収集・加工

まず初めにyoutubeチャンネルの情報を取得しcsvファイルを作成します

'''
youtubeのチャンネルデータをyoutubeAPIを利用して取得し、公開されているデータから動画再生回数を予測するモデルを
作成する。取得できるデータは、動画公開日時、再生回数、いいね数、コメント総数、タイトル、各動画のコメントになる。
動画に対して投稿されたコメントは、動画単位で上限2000件で全件取得し、ポジティブネガティブ判定を行い、その平均値
を使用する。
また、学習データを可視化することで再生回数に影響を与える要素について視覚化する。
'''

#まずはチャンネルの情報を取得する

import urllib.request
import urllib.parse
import json
import csv
import isodate
import datetime
#-------↓パラメータ入力↓-------
APIKEY = #APIキーを入力する
channel_id = #目標のチャンネルIDを入力する
#-------↑パラメータ入力↑-------
dt_now = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
nextPageToken = ''
item_count = 0
outputs = []
outputs.append(['publishedAt', 'title', 'url', 'categoryId',  'duration', 'viewCount', 'likeCount', 'commentCount',])
n = 0
while True:
    #searchメソッドでvideoid一覧取得
    param = {
        'part':'snippet',
        'channelId':channel_id,
        'maxResults':50,
        'order':'date',
        'type':'video',
        'pageToken':nextPageToken,
        'key':APIKEY
    }
    target_url = 'https://www.googleapis.com/youtube/v3/search?'+urllib.parse.urlencode(param)
    print('動画リスト取得')
    print(target_url)
    req = urllib.request.Request(target_url)
    try:
        with urllib.request.urlopen(req) as res:
            search_body = json.load(res)
            item_count += len(search_body['items'])
            video_list = []
            for item in search_body['items']:
                #videoメソッド用list作成
                video_list.append(item['id']['videoId'])

            #videoメソッドで動画情報取得
            param = {
                'part':'id,snippet,contentDetails,liveStreamingDetails,player,recordingDetails,statistics,status,topicDetails',
                'id':",".join(video_list),
                'key':APIKEY
            }
            target_url = 'https://www.googleapis.com/youtube/v3/videos?'+(urllib.parse.urlencode(param))
            print('動画情報取得:合計' + str(item_count)+'件')
            print(target_url)
            req = urllib.request.Request(target_url)
            try:
                with urllib.request.urlopen(req) as res:
                    videos_body = json.load(res)
                    #CSV書き込み用データ準備
                    for item in videos_body['items']:
                        #値が存在しない場合ブランク
                        publishedAt = item['snippet']['publishedAt'] if 'publishedAt' in item['snippet'] else ''
                        title = item['snippet']['title'] if 'title' in item['snippet'] else ''
                        url = 'https://www.youtube.com/watch?v=' + item['id'] if 'id' in item else ''
                        categoryId = item['snippet']['categoryId'] if 'categoryId' in item['snippet'] else ''
                        if 'duration' in item['contentDetails']:
                            #durationを時分秒へ変換
                            duration = isodate.parse_duration(item['contentDetails']['duration'])
                        else:
                            duration = ''
                        viewCount = item['statistics']['viewCount'] if 'viewCount' in item['statistics'] else 0
                        likeCount = item['statistics']['likeCount'] if 'likeCount' in item['statistics'] else 0
                        commentCount = item['statistics']['commentCount'] if 'commentCount' in item['statistics'] else 0
                        outputs.append([publishedAt, title, url, categoryId, duration, viewCount, likeCount, commentCount])
                        n += 1
                    #CSV書き込み
                    with open(dt_now + '_' + channel_id + 'toshio_channel_data.csv', 'w', newline='', encoding='UTF-8') as f:
                        writer = csv.writer(f)
                        writer.writerows(outputs)
            except urllib.error.HTTPError as err:
                print(err)
                break
            except urllib.error.URLError as err:
                print(err)
                break

        #nextPageTokenが表示されなくなったらストップ
        if 'nextPageToken' in search_body:
            nextPageToken = search_body['nextPageToken']
        else:
            break
    except urllib.error.HTTPError as err:
        print(err)
        break
    except urllib.error.URLError as err:
        print(err)
        break

続いて、先ほど取得したチャンネル情報の一覧から各動画のコメントを取得します
動画単位のコメント取得は、APIの仕様で2000件が上限になります

'''チャンネル情報からvideoIdを抜き出し、for分ですべての動画に対してのコメント取得を行う。'''

import requests
import urllib.parse as parse
import csv
import pandas as pd

#パラメータ入力
APIKEY = #APIキーを入力する

#コメントを取得するチャンネル情報のcsvファイルを読み込む(channel_getで作製したファイルを読み込む)
df= pd.read_csv('/Users/tomoshigki/Desktop/python blog/data_folda/ega_channel_data/ega_channel_data_2.csv')

#次いでurlのリストを作成する
url_list = df['url']
videoID_list = []
#videoIDの部分を自然言語処理を用いて抽出しリスト化、新しいカラムとして追加する
for s in url_list:
    target = '='
    idx = s.find(target)
    r = s[idx+1:]
    videoID_list.append(r)
df['videoID'] = videoID_list

API_KEY = APIKEY
URL_HEAD = "https://www.googleapis.com/youtube/v3/commentThreads?"

for videoID, date in zip(df['videoID'], df['publishedAt']):
    API_KEY = 'AIzaSyBbZ7JMkEpEwu9ifk10rc6jPg8KWMVqoA4'
    URL_HEAD = "https://www.googleapis.com/youtube/v3/commentThreads?"
    nextPageToken = ''
    item_count = 0
    items_output = [
        ['videoId']+
        ['textDisplay']+
        #['textOriginal']+
        #['authorDisplayName']+
        #['authorProfileImageUrl']+
        #['authorChannelUrl']+
        #['authorChannelId']+
        ['canRate']+
        ['viewerRating']+
        ['likeCount']+
        ['publishedAt']+
        ['updatedAt']
    ]
    #パラメータ設定
    #video_id = video_id
    exe_num = 20
    for i in range(exe_num):
        #APIパラメータセット
        param = {
            'key':API_KEY,
            'part':'snippet',
            'videoId':videoID,
            'maxResults':'2000',
            'moderationStatus':'published',
            'order':'time',
            'pageToken':nextPageToken,
            'searchTerms':'',
            'textFormat':'plainText',
        }
        #リクエストURL作成
        target_url = URL_HEAD + (parse.urlencode(param))
        #データ取得
        res = requests.get(target_url).json()
        #件数
        item_count += len(res['items'])
        #print(target_url)
        print(str(item_count)+"件")
        #コメント情報を変数に格納
        for item in res['items']:
            items_output.append(
                [str(item['snippet']['topLevelComment']['snippet']['videoId'])]+
                #[str(item['snippet']['topLevelComment']['snippet']['textDisplay'].replace('\n', ''))]+
                [str(item['snippet']['topLevelComment']['snippet']['textOriginal'])]+
                #[str(item['snippet']['topLevelComment']['snippet']['authorDisplayName'])]+
                #[str(item['snippet']['topLevelComment']['snippet']['authorProfileImageUrl'])]+
                #[str(item['snippet']['topLevelComment']['snippet']['authorChannelUrl'])]+
                #[str(item['snippet']['topLevelComment']['snippet']['authorChannelId']['value'])]+
                [str(item['snippet']['topLevelComment']['snippet']['canRate'])]+
                [str(item['snippet']['topLevelComment']['snippet']['viewerRating'])]+
                [str(item['snippet']['topLevelComment']['snippet']['likeCount'])]+
                [str(item['snippet']['topLevelComment']['snippet']['publishedAt'])]+
                [str(item['snippet']['topLevelComment']['snippet']['updatedAt'])]
            )
        #nextPageTokenがなくなったら処理ストップ
        if 'nextPageToken' in res:
            nextPageToken = res['nextPageToken']
        else:
            break
    #CSVで出力
    #コメントのチャンネル名を使ってファイル名を指定し、csvのリストから日付を取得しファイル名に書き加える
    f = open('(保存先のパスを入力する)/comments_{}.csv'.format(date), 'w', newline='', encoding='UTF-8')
    writer = csv.writer(f)
    writer.writerows(items_output)
    f.close()

4-3.データ作成・CSVファイルへ書き出し

上記のプログラムでコメントファイルを一つのフォルダにまとめておきます。最後の部分で保存先のパスを指定できますので、予めフォルダを作成しそのフォルダを指定します
続いて、コメントファイルのあるフォルダを指定してすべてのコメントをポジティブ、ネガティブ判定を行いPN値を計算します。

import MeCab
import re
import pandas as pd
import numpy as np
import csv
import glob
from statistics import mean
import seaborn as sns
import matplotlib.pyplot as plt
import dill
from statistics import mean
import seaborn as sns
import datetime

#変数をまとめて管理する
#全コメントのdataframeフォルダを読み込む
path = '全コメントの入ったフォルダのパス'
#channel_dataを呼び出すパスを指定しておく
data_path = 'APIで取得したチャンネルデータのcsvファイルのパス'
#次いで書き込みの変数を定義しておく
PN_file = '書き込み先のフォルダパスとファイル名を指定する'

#全コメントのdataframeを作成する
file_path_lists = glob.glob("{}/**".format(path), recursive=True)
comments_all = []
for file_path in file_path_lists[1:]:
    df_comments = pd.read_csv(file_path,lineterminator='\n')
    comments_all.append(df_comments)
    all_comments = pd.concat(comments_all)


## word_list, pn_listにそれぞれリスト型でWordとPNを格納
pn_df = pd.read_csv('(極性辞書のファイルパスを入力する)',\
                    sep=':',
                    encoding='shift-jis',
                    names=('Word','Reading','POS', 'PN')
                   )
word_list = list(pn_df['Word'])
pn_list   = list(pn_df['PN'])
pn_dict = dict(zip(word_list, pn_list))

# pn_dictとしてword_list, pn_listを格納した辞書を作成する
pn_dict = dict(zip(word_list, pn_list))
# word_listにリスト型でWordを格納する
word_list = list(pn_df['Word'])
#pn_listにリスト型でPNを格納する
pn_list = list(pn_df['PN'])

# pn_dictとしてword_list, pn_listを格納した辞書を作成する
pn_dict = dict(zip(word_list, pn_list))

#PN値とBaseFormの対応付けがある辞書を作成する
m = MeCab.Tagger('')

def add_pnvalue(diclist_old, pn_dict):
    diclist_new = []
    for word in diclist_old:
        base = word['BaseForm']        # 個々の辞書から基本形を取得
        if base in pn_dict:
            pn = float(pn_dict[base])
        else:
            pn = 'notfound'            # その語がPN Tableになかった場合
        word['PN'] = pn
        diclist_new.append(word)
    return diclist_new

# 各コメントのPN平均値を求める
def get_mean(dictlist_new):
    pn_list = []
    for word in dictlist_new:
        pn = word['PN']
        if pn!='notfound':
            pn_list.append(pn)
    if len(pn_list)>0:
        pnmean = np.mean(pn_list)
    else:
        pnmean=0
    return pnmean

def get_diclist(text):
    parsed = m.parse(text)
    lines = parsed.split('\n')
    lines = lines[0:-2]
    diclist = []
    for word in lines:
        l = re.split('\t|,',word)  # 各行はタブとカンマで区切られてるので
        d = {'BaseForm':l[7]}
        diclist.append(d)
    return diclist


# 空のリストを作り、動画ごとの平均値を入れていく
means_list = []
for comment in all_comments['textDisplay']:
    dl_old = get_diclist(str(comment))
    dl_new = add_pnvalue(dl_old, pn_dict)
    pnmean = get_mean(dl_new)
    means_list.append(pnmean)
all_comments['pn'] = means_list

#ここまでで、_all_commentsというdatframeに各コメントのPN平均値のカラムを追加できる
#channel_dataとall_commentsをvideoIdをキーにして結合する
df_channel = pd.read_csv(data_path)
new_df = pd.merge(df_channel,all_comments, on='videoId', how='outer')

#同じvideoIdのpn値の平均を出し、新たなカラムとして追加する
#判定ができないコメントがPN値0.00となり平均値への影響が強いため削除する
new_df = all_comments[all_comments['pn'] != 0.00]
PN_mean_list = []
new_df_date = new_df.set_index('videoId')
date_list = new_df_date.index
for p in date_list:
    PN_mean = new_df_date.loc[p, 'pn'].mean()
    PN_mean_list.append(PN_mean)
new_df_date['PN_mean'] = PN_mean_list

df_time_categories = pd.read_csv(data_path)

#時刻のカテゴリーは気象庁の時間細分図より3カテゴリーのものを使用する
#0時から9時を1,9時から18時を2,18時から24時までを3と分類していく
time_categories_lists = []
for time in df_time_categories['publishedAt']:
    target = 'T'
    idx = time.find(target)
    r = time[idx+1:]
    date_r = datetime.datetime.strptime(r, '%H:%M:%SZ')
    time_r = date_r + datetime.timedelta(hours=9)
    up_time = time_r.hour
    if 0 <= up_time < 9:
        time_categories_lists.append(1)
    elif 9 <= up_time < 18:
        time_categories_lists.append(2)
    elif 18 <= up_time <= 23:
        time_categories_lists.append(3)

df_time_categories['up_date_time'] = time_categories_lists


#channel_dataのdataframeにPN_meanのカラムを追加するコードを書いていく
#重複している行を削除する
df_date = new_df_date['PN_mean']
df_PN_mean = df_date[~df_date.index.duplicated(keep='first')]

# 空のリストを作り、タイトルごとの平均値を入れていく
means_list = []
for comment in df_time_categories['title']:
    dl_old = get_diclist(str(comment))
    dl_new = add_pnvalue(dl_old, pn_dict)
    pnmean = get_mean(dl_new)
    means_list.append(pnmean)
df_time_categories['title_PN'] = means_list

#タイトルに対してもPN分析をしていく
# 空のリストを作り、タイトルごとの平均値を入れていく
means_list = []
for comment in df_time_categories['title']:
    dl_old = get_diclist(str(comment))
    dl_new = add_pnvalue(dl_old, pn_dict)
    pnmean = get_mean(dl_new)
    means_list.append(pnmean)
df_time_categories['title_PN'] = means_list

#PN_meanカラムを追加しcsvファイルで書き出す
df_PN = pd.merge(df_time_categories ,df_PN_mean,right_index=True,left_on='videoId')
df_PN.to_csv(PN_file)

4-4.学習結果の出力

最後にここまでで作成した表を微調整し、再生回数を予測してスコアを出します。また学習に使用した表データをヒートマップで表示し、再生回数に影響する特徴量を視覚化します。

from tkinter import N
import pandas as pd
import csv
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
import sklearn.model_selection
import datetime
from datetime import datetime
from sklearn.linear_model import Lasso
from sklearn.linear_model import Ridge
from sklearn.linear_model import ElasticNet

#変数を読み込んでいく
#PN_createで作製したcsvファイルを読み込む
csv_path = #ファイルのパスを入力


#csvファイルを読み込む
df = pd.read_csv(csv_path)

#up_date_timeはカテゴリ変数のため、encordingする
df = pd.get_dummies(df, columns= ["up_date_time"])

#動画時間を分へ変換してカラムを追加する
video_time = []
for time in df['duration']:
    d_minute = datetime.strptime(time, "%H:%M:%S").minute
    d_second = datetime.strptime(time, "%H:%M:%S").second
    times = d_minute + d_second/60
    video_time.append(times)
df['video_time'] = video_time

# 今回学習に用いないカラムを削除
drop_col = ['publishedAt', 'title', 'categoryId', 'url', 'duration', 'videoId', 'Unnamed: 0']
df = df.drop(drop_col, axis=1)

#説明変数を指定して削除する
X = df.drop('viewCount', axis=1)
#yに目的変数を入れる
y = df.viewCount

train_X, test_X, train_y, test_y = train_test_split(X, y, random_state=42)

#線形重回帰分析を行う。各種分析モデルを比較する。

#model = LinearRegression()
#model = Lasso()
#model = Ridge()
model = ElasticNet()

model.fit(train_X, train_y)
print(model.score(test_X, test_y))
#print(model.predict(test_X))

#データをヒートマップで表示する
plt.figure()
sns.heatmap(df.corr(), cmap='coolwarm', annot=True)
plt.show()

4-5.結果について

以上になります。
実行後のスコアは0.82111...となりました。予測再生回数は、7万回台の再生回数から最多で77万回台の再生回数となりました。予測結果の幅は大きいですが、実際のチャンネル情報と比較しても同様のばらつきがありスコアと矛盾はないかと考えます。

参考までに最終的に表示されるヒートマップをお見せします。
heatmap.jpg

5.まとめ

元々は利用できるデータセットを活用して分析・予測モデルを作成しようと考えたのですが、妻と食事している際に以前よくみていたチャンネルの動画に対するコメントが最近減っていて、
コメントも否定的になっている気がするし、再生数も減っているように感じるという発言で「これを分析に活用できないか?」と思い立って、今回の作成に至りました。
結果的には、ヒートマップでもわかるように、いいね数、コメント数が多いほど再生回数が多いということは顕著ですが、コメントのポジティブ、ネガティブは強い影響はないように感じます。いいねやコメント数は、時間と共に増加するものでもあり、動画公開の時点での再生回数の予測というのは困難かと考えますが、動画公開からある程度の時間経過で、どの程度の再生回数が見込めるかの予測は可能ではないかと考えます。
また、学習データを確認するとPN平均値はどのようなチャンネルでもすべてネガティブな値を示しました。全件のコメントを読んで確認したわけではありませんが、明らかに好意的なコメントもPN平均値はマイナスになっています。幾つかコメントを取り出して確認してみると、ネット独特の言い回しがあるのか、判定不能な言葉が多いことがわかりました。
データをヒストグラムでみると、圧倒的に突出したPN値は0.00であり判定結果を確認すると、カタカナ表記で判定不能であったり、文字数が少ないことに加えて絵文字が多かったりで、判定ができなかったケースで0.00という値となっていることがわかりました。そのため、PN値が0.00になるデータを削除して計算してみましたが、平均値や中央値の変化はわずかでさらに最終的な予測結果の変化も微々たるものとなりました。全体量に対して逸脱した値が微量となることで、全体的な結果には影響が小さかったかと考えています。
また、判定可能な言葉の多くはネガティブに判定されていました。文脈ではポジティブに感じられるものも、単語だけを取り出すとネガティブに評価されることも多く、ネット独特の言い回しや絵文字や顔文字などネット上の書き込みに対しては、今回使用した極性辞書で分類することが難しい点もあるのではないかと考えられました。
このような問題も考慮すると、SNSなどのネット上の書き込みを正確に分類するためには、極性辞書そのものを作成することも必要ではないかと思われます。

6.今後の活用

今回はAPIを使用してデータを取得し分析に活用する手法を行いました。ウェブスクレイピングを活用した、さらに実用性のあるデータの取得や分析に繋げられると思っています。
データを可視化しながら、データの分布や傾向を考えながら、より正確なモデルを作成していく過程を実践してみたことで、今後さらに精度の高いモデル作成に取り組んでいけると考えます。
また、講座では画像認識も学んでいます。画像データの分析にも繋げらればと考えています。
学んだことを活用して、形にしていくことで知識を定着し技術とても伸ばしいきたいと思います。

7.おわりに

  • 形にしていくことで知識が深まり、技術力が伸びることが実感できました
  • データを分析するにあたり、分布などを可視化することでより正確なモデル作成のためのデータの前処理に繋げらることを実感できました
  • 今後は画像データの分析もディープラーニングの技術を活用して行っていきたいと思っています

8.参考文献

APIからデータを取得する部分は、以下の記事を参考にさせていただきました。
[Youtube Data APIを使ってPythonでYoutubeデータを取得する]https://qiita.com/g-k/items/7c98efe21257afac70e9

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4