27
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ヒット曲は予測できるのか?~Python初心者の挑戦~

Last updated at Posted at 2023-11-07

はじめに

はじめまして。Python初心者のYumiと申します。
完全に文系で、プログラミングのプの字も統計学のとの字もかじった事のなかった、しがないアラフォー女です。
手に職というものもなく、なんとなーくで生きてきたのですが、この辺りで何か一念発起せねばならんなと思い、Pythonを勉強することに決めました。
心折れつつ右往左往している様をありのままにお届けします。同じように四苦八苦している、初心者のあなたのもとに届けば嬉しいです。

(諸先輩方におかれましては、「こういう間違い、昔やったなあ」と暖かく見ていただければと思います。)

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

概要

  • Spotify APIを使ってヒット曲を予想・分析できるのか?
    • 仮説① 季節と相関する
    • 仮説② 気温と相関する→気象庁データ
    • 仮説③ 株価と相関する→pandas-datareader
  • 機械学習でヒット曲を当てられるか

まずテーマ選びから詰まっていたのですが、ふと音楽を聞いている時に使っているSpotifyを見て、APIを使ってみたい!と思ったのが動機です。
全くの初心者にも敷居が低く感じられるように、というコンセプトのもと書き進めていきます。

データ収集

Spotifyチャートからcsvを取得する

より日本の週間ランキングチャートに移動。
2022/10/1〜2023/9/28間のチャート(csv)52週分を取得(直接ダウンロードできます)。
googleドライブの'weeklydata'フォルダに格納し、それを利用していきます。

googleドライブの利用

決まり文句
from google.colab import drive
drive.mount('/content/drive')

実行&許可するとgoogleドライブにマウントできます。
取得したcsvにどんな情報が入っているか、適当に1つ抜き出して確認してみます。

In
import pandas as pd
#googleドライブの中のcsvを読み込みます
rank0105_df = pd.read_csv('drive/My Drive/weeklydata/regional-jp-weekly-2023-01-05.csv')
print(rank0105_df)
print(rank0105_df.dtypes)
print(rank0105_df.columns.values)
Out
     rank                                   uri           artist_names  \
0       1  spotify:track:49F3htNmwzPKFycPdOrDvf  OFFICIAL HIGE DANDISM   
1       2  spotify:track:3khEEPRyBeOUabbmOPJzAG          Kenshi Yonezu   
2       3  spotify:track:4IfrM44LofE9bSs6TDZS49             Tani Yuuki   
3       4  spotify:track:2Dzzhb1oV5ckgOjWZLraIB                 natori   
4       5  spotify:track:28MATCYDctW5EiBa2repxb                    Ado   
..    ...                                   ...                    ...   
195   196  spotify:track:0V3wPSX9ygBnCm8psDIegu           Taylor Swift   
196   197  spotify:track:45YBVp6zMwQZRbUDcPzmMB          Kenshi Yonezu   
197   198  spotify:track:4qs3qswB84E0VPmM4tsTws                  Aimer   
198   199  spotify:track:26OxcplUEuMjoqkjwVLcPq         PornoGraffitti   
199   200  spotify:track:0cN6iBeCR7NgeBeTIKjLml                    Ado   

    track_name                  source  peak_rank  previous_rank  \
0     Subtitle           IRORI Records          1              1   
1    KICK BACK  Sony Music Labels Inc.          1              2   
2    W / X / Y          Valley Records          1              4   
3     Overdose                     なとり          2              3   
4          新時代     Universal Music LLC          1              5   
..         ...                     ...        ...            ...   
195  Anti-Hero            Taylor Swift         67            189   
196    アイネクライネ     Universal Music LLC         82            187   
197  Deep down  Sony Music Labels Inc.        113            152   
198      サウダージ  Sony Music Labels Inc.        138             -1   
199     阿修羅ちゃん     Universal Music LLC         53             -1   

     weeks_on_chart  streams  
0                13  2775470  
1                13  2079021  
2                54  1603556  
3                17  1589064  
4                30  1588536  
..              ...      ...  
195              11   235020  
196              83   234427  
197               4   234299  
198               9   233629  
199              55   233558  

[200 rows x 9 columns]
rank               int64
uri               object
artist_names      object
track_name        object
source            object
peak_rank          int64
previous_rank      int64
weeks_on_chart     int64
streams            int64
dtype: object
['rank' 'uri' 'artist_names' 'track_name' 'source' 'peak_rank'
 'previous_rank' 'weeks_on_chart' 'streams']

これだけでも結構な情報が得られます。
特に重要なのはuriですね。各楽曲のデータにアクセスするためのキーとなります。

データ整理

まずは現在取得済のこれらのデータを整理していきます。

  • 1〜20位をA、21〜50位をB、51〜100位をC、101位〜200位をDとして分類→'score'
  • それぞれのcsvファイル毎に識別記号→'date'

全てのデータを結合(weekly_all_df)し、ランクでソート、期間内の最高位だけ拾いたいので重複削除します。

In
#出力用の空データフレームの作成
weekly_all_df = pd.DataFrame()

import glob
import numpy as np

#weeklydataフォルダ内のcsvファイル名を取得しリスト化
csv_files = glob.glob('drive/My Drive/weeklydata/*.csv')
csv_list = []
for csv_name in csv_files :
    csv_list.append(csv_name)
#一度リストをprintすると降順になっていたので昇順に並べ直す
csv_list.sort()

#for文を利用してcsvを処理していく
for file in csv_list :
    df = pd.read_csv(file)
#csvファイル毎に識別番号(csv_listのインデックス)をつけた列を追加し'date'と名付ける
#あとから考えたら○週目なので'week'の方が良かったですね
    df.loc[:,'date'] = csv_list.index(file)
    weekly_all_df = pd.concat([weekly_all_df,df],ignore_index = True,axis = 0)

#ランク毎にA〜Dの'score'をつける
conditions = [(weekly_all_df['rank'] <= 20),(weekly_all_df['rank'] <=50),(weekly_all_df['rank'] <= 100)]
choices = ['A','B','C']
weekly_all_df['score'] = np.select(conditions,choices,default='D')

#一旦csvに出力する。index=Falseつけなかったら謎の列発生した
weekly_all_df.to_csv(f'/content/drive/My Drive/songdata/weeklyall.csv',index = False)

ここまでの間にしょっちゅうprintしてます。確認大事。

In
#重複数の確認
print(weekly_all_df.duplicated(subset = 'uri').sum())
Out
9718

えっ、、、マジかよ、、、
200×52週分で10400件分のデータがあるのですが、ほぼ重複と判明。
確かにランキング下位は以前のヒット曲の可能性高いですね。
それにしても重複しすぎだろう…新しい曲がどんどん入れ替わるということはないということ…

とりあえず重複を除いたデータ(weekly_unique_df)を作成します。

In
#集計期間内最高位のランクを参照したいので、ランクでソートのち重複削除
weekly_unique_df = weekly_all_df.sort_values(by = 'rank')
weekly_unique_df.drop_duplicates(subset = 'uri',inplace = True)

print(weekly_unique_df['score'].value_counts())
Out
D    307
C    211
A     96
B     68
Name: score, dtype: int64

データが少ない気がする…
1〜20位を取れる楽曲は52週で96曲。なんと狭き門かよくわかります。
ひとまず分析を進めてみます。

仮説① 季節の追加

季節によって特定のワード使った曲が流行ったりとかありますよね。なので相関があるんじゃないかと考えました。
便宜上テレビ局のカウントに倣って、4〜6月(春)、7〜9月(夏)、10月〜12月(秋)、1月〜3月(冬)とします。
反映した列を先ほどのデータフレーム(weekly_unique_df)に追加します。
(あとから考えるとここで追加する意味なかった)

In
conditions2 = [(weekly_unique_df['date'] <= 12),(weekly_unique_df['date'] <= 25),(weekly_unique_df['date'] <= 38)]
choices2 = ['autumn','winter','spring']
weekly_unique_df['season'] = np.select(conditions2,choices2,default='summer')

仮説② 気温の追加

季節が関係してくるなら気温も関係するのでは?

より東京の2022/10/1〜2023/9/30の日別気温をcsvで取得。
googleドライブに保存して使用します。

In
#'utf-8' codec can't decode byte 0x83 in position 0: invalid start byte
#と怒られたので、エンコードを入れます
temp_df = pd.read_csv('drive/My Drive/data/data.csv',encoding = 'Shift-JIS')
print(temp_df)
print(temp_df.dtypes)
print(temp_df.columns.values)
Out
                         ダウンロードした時刻:2023/11/02 03:54:53
NaN       東京      東京                                  東京
年月日       平均気温(℃) 平均気温(℃)                        平均気温(℃)
NaN       NaN     品質情報                              均質番号
2022/10/1 23.6    8                                    1
2022/10/2 23.5    8                                    1
...                                                  ...
2023/9/26 24.6    8                                    1
2023/9/27 25.3    8                                    1
2023/9/28 27.4    8                                    1
2023/9/29 26.3    8                                    1
2023/9/30 25.6    8                                    1

[368 rows x 1 columns]
ダウンロードした時刻:2023/11/02 03:54:53    object
dtype: object
['ダウンロードした時刻:2023/11/02 03:54:53']

気象庁のcsv、使いづらくない?

とても変なデータだ………ダウンロードした時間バレるの恥ずかしい
なんとか整地して、最終的に週の平均気温(木曜集計)を出していきます(temp_w_df)。

In
#インデックスを振り直して不要な行をdropします
temp_df.reset_index(inplace=True)
temp_df.drop(range(0,3),inplace=True)

#不要な列もdrop、カラムも新しく名付けます
temp_df.drop(['level_2','ダウンロードした時刻:2023/11/02 03:54:53'],axis=1,inplace=True)
temp_df.rename(columns={'level_0':'date','level_1':'temp'},inplace=True)

print(temp_df.head())
Out
          date  temp
3    2022/10/1  23.6
4    2022/10/2  23.5
5    2022/10/3  23.0
6    2022/10/4  25.2
7    2022/10/5  19.8
In
import datetime as dt

#曜日で抽出できるようにしたいので日付をdatetime型に変えます
temp_df['date'] = pd.to_datetime(temp_df['date'],format='%Y-%m-%d')
#temp列を数値に変えます
temp_df['temp'] = pd.to_numeric(temp_df['temp'],errors='coerce')
#日付をindexに変えます
temp_df.set_index('date',inplace=True)

#週毎の平均気温を出します(集計曜日木曜)
temp_w_df = temp_df.resample('W-Thu').mean()
print(temp_w_df.head())
Out
                 temp
date                 
2022-10-06  21.366667
2022-10-13  17.071429
2022-10-20  17.671429
2022-10-27  14.885714
2022-11-03  15.300000

weekly_unique_dfに左外部結合します
(再度言いますがここで結合する意味はあんまりなかった)

In
#結合しやすいようにtemp_w_dfに'key'列を増やしてint型にしておきます
temp_w_df['key'] = range(0,len(temp_w_df.index))
temp_w_df[['key']].astype('int')

#左外部結合
weekly_unique_df = pd.merge(weekly_unique_df,temp_w_df,how='left',left_on='date',right_on='key')
#key列の役目は終わったのでdrop
weekly_unique_df.drop(['key'],axis=1,inplace=True)

仮説③ 株価の追加

完全にイメージですが、景気が良くなるとみんな踊り出したくなるんじゃない?
2023年は久々に日経平均が30,000円を越え、最高値と最低値の差が大きいと思われるので、昨年分よりも相関がわかりやすいかもと思いました。

pandas-datareaderで株価取得

pandas-datareaderを使ってstooqというサイトから日経平均を取得していきます。

In
#pandas-datareaderをインストール
!pip install pandas-datareader

from pandas_datareader import data
#日経平均を2022/10/1〜2023/9/30の1年分取得します
start = "2022-10-01"
end = "2023-09-30"
stk_df = data.DataReader("^NKX","stooq",start,end)

#始値だけ残す
stk_df = stk_df.drop(['High','Low','Close','Volume'],axis=1)

#先ほどと同じようにto_datetimeを使っていきます
stk_df.reset_index(inplace=True)
pd.to_datetime(stk_df['Date'],format='%Y-%m-%d')
#曜日を追加
stk_df['Day'] = stk_df['Date'].dt.weekday
#木曜日のみ抽出して昇順に並べ直します
stk_thu_df = stk_df[stk_df['Day'] == 3]
stk_thu_df = stk_thu_df.sort_values(by='Date')
#使い終わった'Day'を削除
stk_thu_df = stk_thu_df.drop(['Day'],axis=1)
stk_thu_df.reset_index(inplace=True)

#'index'列ができていたのでそれも削除
#結合を見越してindexをint型、日付をdatetime型→str型に変更
stk_thu_df = stk_thu_df.drop(['index'],axis=1)
stk_thu_df.index.astype('int')
stk_thu_df['Date'] = stk_thu_df['Date'].astype('str')
stk_thu_df.info()
Out
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 49 entries, 0 to 48
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   Date    49 non-null     object 
 1   Open    49 non-null     float64
dtypes: float64(1), object(1)
memory usage: 912.0+ bytes

ん?なんかサイズが合わない
確認のためtemp_w_dfと結合してわざと欠損を作ります

In
conf_df = pd.merge(temp_w_df,stk_thu_df,how='left',left_on='date',right_on='Date')
Out
          date       temp        Date      Open
0   2022-10-06  21.366667  2022-10-06  27137.98
1   2022-10-13  17.071429  2022-10-13  26398.29
2   2022-10-20  17.671429  2022-10-20  26981.75
3   2022-10-27  14.885714  2022-10-27  27407.23
4   2022-11-03  15.300000         NaN       NaN
5   2022-11-10  14.628571  2022-11-10  27459.08
:
19  2023-02-16   6.071429  2023-02-16  27654.72
20  2023-02-23   7.828571         NaN       NaN
21  2023-03-02   9.900000  2023-03-02  27564.82
:
29  2023-04-27  15.828571  2023-04-27  28340.59
30  2023-05-04  18.642857         NaN       NaN
31  2023-05-11  17.600000  2023-05-11  29110.79
:
:

祝日ですね!欠損値を休場直前の日のデータで補充します

In
#行挿入のために一旦分割
stk_thu_df1 = stk_thu_df.iloc[:4,:].copy()
stk_thu_df2 = stk_thu_df.iloc[4:19,:].copy()
stk_thu_df3 = stk_thu_df.iloc[19:28,:].copy()
stk_thu_df4 = stk_thu_df.iloc[28:,:].copy()

#該当日の株価を確認
print(stk_df[stk_df['Date']=='2022-11-02'])
print(stk_df[stk_df['Date']=='2023-02-22'])
print(stk_df[stk_df['Date']=='2023-05-02'])

#↑で確認したデータを分割後の各データフレームに追加
stk_thu_df1.loc[4] = ['2022-11-02',27678.92]
stk_thu_df2.loc[19] = ['2023-02-22',27265.99]
stk_thu_df3.loc[28] = ['2023-05-02',29278.8]

#再結合
stk_cor_df = pd.concat([stk_thu_df1,stk_thu_df2,stk_thu_df3,stk_thu_df4])
#結合しただけだとindexがおかしいままなのでindexを振り直します
stk_cor_df.reset_index(inplace=True)
#あとで結合することを考えてindexをint型にしておきます
#また'index'できてるのでdrop
stk_cor_df.index.astype('int')
stk_cor_df = stk_cor_df.drop(['index'],axis=1)

これで52週分の木曜日の株価データができました!
weekly_unique_dfに横結合します
(何度も言いますが、ここで結合しなくてもよかった)

In
weekly_unique_df = pd.merge(weekly_unique_df,stk_cor_df,how='left',left_on='date',right_on=stk_cor_df.index)

Spotify APIの使用

にて自分のIDをもとに登録をしておく必要があります。
アプリ登録までいくとclient_IDclient_secretが振られますので、それをコード内に記載しなければいけません。

決まり文句
#spoti"py"のインストールが必要です
!pip install spotipy

import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
client_id = 'My_id'
client_secret = 'My_secret'
client_credentials_manager = spotipy.oauth2.SpotifyClientCredentials(client_id, client_secret)
spotify = spotipy.Spotify(client_credentials_manager=client_credentials_manager)

何ができるかテスト、Adoさんのアーティスト情報・『唱』の曲情報を取ってみます。

In
#idはアーティストページのURLの最後にあります
artist_id = '6mEQK9m2krja6X1cfsAjfl' #Adoさん
spotify.artist(artist_id)
Out
{'external_urls': {'spotify': 'https://open.spotify.com/artist/6mEQK9m2krja6X1cfsAjfl'},
 'followers': {'href': None, 'total': 3282053},
 'genres': ['j-pop', 'japanese teen pop'],
 'href': 'https://api.spotify.com/v1/artists/6mEQK9m2krja6X1cfsAjfl',
 'id': '6mEQK9m2krja6X1cfsAjfl',
 'images': [{'height': 640,
   'url': 'https://i.scdn.co/image/ab6761610000e5ebf3bb04995cb61f04936424ee',
   'width': 640},
  {'height': 320,
   'url': 'https://i.scdn.co/image/ab67616100005174f3bb04995cb61f04936424ee',
   'width': 320},
  {'height': 160,
   'url': 'https://i.scdn.co/image/ab6761610000f178f3bb04995cb61f04936424ee',
   'width': 160}],
 'name': 'Ado',
 'popularity': 73,
 'type': 'artist',
 'uri': 'spotify:artist:6mEQK9m2krja6X1cfsAjfl'}
In
#各楽曲のURLの最後にあります
track_id = '2tlOVDJ3lQsUxz22vPJ4c4' #『唱』
spotify.audio_features(track_id)
Out
[{'danceability': 0.616,
  'energy': 0.975,
  'key': 10,
  'loudness': -0.425,
  'mode': 0,
  'speechiness': 0.208,
  'acousticness': 0.172,
  'instrumentalness': 1.62e-06,
  'liveness': 0.143,
  'valence': 0.742,
  'tempo': 132.054,
  'type': 'audio_features',
  'id': '2tlOVDJ3lQsUxz22vPJ4c4',
  'uri': 'spotify:track:2tlOVDJ3lQsUxz22vPJ4c4',
  'track_href': 'https://api.spotify.com/v1/tracks/2tlOVDJ3lQsUxz22vPJ4c4',
  'analysis_url': 'https://api.spotify.com/v1/audio-analysis/2tlOVDJ3lQsUxz22vPJ4c4',
  'duration_ms': 189773,
  'time_signature': 4}]

特に重要視すべきは楽曲の各パラメータでしょうか。
説明変数として使いたいです。

関連がありそうなのはこの辺り
acousticness アコースティック度(0.0〜1.0)
danceability ダンスのしやすさ(0.0〜1.0)
energy 激しさ、強度と活動性(0.0〜1.0)
mode 長調なら1、短調なら0
valance 明るいほど1に近づく(0.0〜1.0)
tempo テンポ、BPM

これらが辞書型で与えられることがわかりました。
視覚化のために情報を集めていきます(chart_an_df

In
#'uri'列からtrack_idのみ抜き出します
weekly_unique_df['track_id'] = weekly_unique_df['uri'].str.split(':',expand=True)[2]

#各楽曲のパラメータ情報のみでデータフレームを作ります
chart_an_df = pd.DataFrame()
for track_id in weekly_unique_df['track_id'] :
    df = pd.DataFrame.from_dict(spotify.audio_features(track_id))
    chart_an_df = pd.concat([chart_an_df,df],ignore_index=True,axis=0)

と、ここで気付くのです。
どう考えてもこっちに仮説の各データを結合すべきやん

………。このまま続行します。

In
#もう、そのまま移植します
chart_an_df[['week','score','season','temp','Date','Open']] =
 weekly_unique_df[['date','score','season','temp','Date','Open']]

紆余曲折ありましたが、これで可視化の準備ができました。

データの可視化

今回はSeabornのpairplotを主に使っていきます。複数項目の散布図を一気に作れるのが利点。
'danceability' 'energy' 'mode' 'acousticness' 'valence' 'tempo' と 'week' 'score' 'season' 'temp' 'Open'に相関がないかを見ていきたいです。

In
import seaborn as sns
import matplotlib.pyplot as plt

#'score'を色分けして打点
sns.pairplot(chart_an_df,
             x_vars = ['week','temp','Open'],
             y_vars = ['danceability','energy','acousticness','valence','tempo'],
             hue = 'score')

#'season'を色分けして打点
sns.pairplot(chart_an_df,
             x_vars = ['week','temp','Open'],
             y_vars = ['danceability','energy','acousticness','valence','tempo'],
             hue = 'season')

snsall.png
seasonall.png

  • danceabilityは0.4以上が好まれている
  • energyもdanceability同様0.4以上
  • どちらかというと激しい曲調が好まれているようだ
  • 季節的には秋〜冬にかけてdanceabilityが減少、その後増加する傾向があるが気温との相関ではなさそう→曲提供側として冬曲はおとなしめの曲を出す傾向にあるから?
  • valenceはそこまで相関がないが0.2以上、どちらかといえば陽気寄り
  • tempoもばらつきがあるが、150程度が多いか

'temp''Open'で空白地帯があるのが気になったため確認します。

In
fig = plt.figure(figsize=(16,5))

#気温と株価をヒストグラムで確認
ax1 = fig.add_subplot(1,3,1)
ax1.hist(temp_w_df['temp'])
ax1.set_title('temperature')
ax2 = fig.add_subplot(1,3,2)
ax2.hist(stk_cor_df['Open'])
ax2.set_title('stock')

#modeを棒グラフで確認
ax3 = fig.add_subplot(1,3,3)
ax3 = sns.countplot(x='mode',data=chart_an_df)
ax3.set_title('mode')
plt.show()

tempstkmode.png

  • 長調・短調で言えば長調の方が圧倒的に多い
  • 株価の空白地帯は日経平均30,000円の時期がなかったため
  • 気温で空白地帯ができているのは、気温10℃付近の時期が少なかったため

ということが読み取れます。

散布図自体がちょっと見辛いのでAB群(チャート50位内)に限定して再度見てみます。

In
#'score'がA・Bのみを抽出
chart_anab_df = chart_an_df[chart_an_df['score'].isin(['A','B'])]

sns.pairplot(chart_anab_df,
             x_vars = ['week','temp','Open'],
             y_vars = ['danceability','energy','acousticness','valence','tempo'],
             hue = 'score')

snsab.png

  • 0週の集中は省いた方が良さそうです。(データ取得開始日のため、どうしても曲数が集中する)
  • dansability,energyが週数を重ねるにつれて緩く右肩あがりになっている。気温上昇にも相関していそう。
  • valenceもごく緩く相関しているかな。
  • tempoは株価と緩く相関していそう。
  • acousticnessは、dansabilityやenergyと負の相関にあるのは間違いなさそうなので省く

教師データとして整えていきます。

In
#使わない列の削除
chart_ancor_df = chart_an_df.drop(['Unnamed: 0','key','loudness','speechiness',
                                   'instrumentalness','liveness','type','id','uri',
                                   'track_href','analysis_url','duration_ms',
                                   'time_signature','Date','acousticness'],axis=1)
#0週のデータを削除=0週のデータ以外を抽出
chart_ancor_df = chart_ancor_df[chart_ancor_df['week'] != 0]
chart_ancor_df = chart_ancor_df.drop(['week'],axis=1)
#次の段階でstr型が使えなかったので数値に変更します
chart_ancor_df = chart_ancor_df.replace({'spring':1,'summer':2,'autumn':3,'winter':4})
chart_ancor_df = chart_ancor_df.replace({'A':4,'B':3,'C':2,'D':1})
print(chart_ancor_df)
Out
     danceability  energy  mode  valence    tempo  score  season       temp  \
1           0.649   0.683     1    0.381  130.000      4       4  13.957143   
2           0.574   0.935     1    0.836  166.008      4       2  29.628571   
3           0.577   0.941     1    0.292  101.921      4       3  14.428571   
4           0.566   0.850     0    0.722  117.048      4       2  24.328571   
5           0.425   0.939     1    0.638  150.015      4       1  16.100000   
..            ...     ...   ...      ...      ...    ...     ...        ...   
676         0.675   0.747     1    0.481  119.975      1       4   7.428571   
677         0.637   0.485     1    0.403  109.035      1       4  12.742857   
678         0.486   0.766     1    0.417  154.992      1       3   8.400000   
680         0.672   0.541     1    0.453  151.985      1       1  22.642857   
681         0.707   0.500     1    0.583  129.970      1       1  19.028571   

         Open  
1    27368.62  
2    32019.06  
3    27097.38  
4    32018.64  
5    28321.54  
..        ...  
676  26346.69  
677  28385.29  
678  27633.96  
680  33399.15  
681  30909.61  

[596 rows x 9 columns]

これで準備ができました。
いよいよ機械学習していきます。

機械学習

今回は最終的に『様々なパラメータを持つ楽曲の』『順位を予想する』ため、分類問題が得意なランダムフォレストを使用したいと思います。
目的変数を'score'、説明変数をそれ以外とします。

ランダムフォレスト

In
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

#カラム名の表示
print(chart_ancor_df.columns.values)

#学習用とテスト用にデータを分割
train_X = chart_ancor_df.drop(['score'],axis=1)
train_y = chart_ancor_df['score']
(train_X,test_X,train_y,test_y) = train_test_split(train_X,train_y,test_size=0.3,random_state=42)

random_forest = RandomForestClassifier(max_depth=10,n_estimators=15, random_state=42)
random_forest.fit(train_X, train_y)

#特徴量の重要度
print(random_forest.feature_importances_)

y_pred = random_forest.predict(test_X)
trainaccuracy_random_forest = random_forest.score(train_X, train_y)

#精度
print('TrainAccuracy:{}'.format(trainaccuracy_random_forest))
accuracy_random_forest = accuracy_score(test_y, y_pred)
print('Accuracy:{}'.format(accuracy_random_forest))
Out
[0.17076721 0.18475234 0.02893315 0.17395332 0.18158803 0.0349908
 0.10950091 0.11551425]
TrainAccuracy:0.920863309352518
Accuracy:0.3743016759776536

4割…?

かなり精度が低い

ハイパーパラメータを調整してもこれ以上にはなりませんでした。
特徴量の重要度を見るに、'mode'と'season'はあまり影響しないことがわかりました、が、如何せん精度が低い。
一旦心が折れましたが、精度を上げるにはと思考します。

仮説:データ整理の際に危惧していた通り、データ数が少ないのでは。

リトライ

重複データありで作り直します(chart_all_df

In
#謎の'Unnamed: 0'ができていたので一緒にdrop
chart_all_df = weekly_all_df.drop(['Unnamed: 0','rank','artist_names','track_name','source',
                                  'peak_rank','previous_rank','weeks_on_chart','streams'],axis=1)
                                  
#各楽曲のパラメータは重複なしデータを作る際に取得しているので、それを利用して左外部結合します
#spotifyAPIから全部取得しようとしたら制限かかりました。10,400件だもんな
chart_all_df = pd.merge(chart_all_df,chart_an_df,how='left',on='uri')

#'season'を最初から数字で振ります
conditions3 = [(chart_all_df['date'] <= 12),(chart_all_df['date'] <= 25),(chart_all_df['date'] <= 38)]
choices3 = [3,4,1]
chart_all_df['season'] = np.select(conditions3,choices3,default=2)

#'temp'を左外部結合
temp_w_df['key'] = temp_w_df.index
temp_w_df = temp_w_df.drop(['date'],axis=1)
chart_all_df = pd.merge(chart_all_df,temp_w_df,how='left',left_on='date',right_on='key')

#'Open'を左外部結合
chart_all_df = pd.merge(chart_all_df,stk_cor_df,how='left',left_on='date',right_on=stk_cor_df.index)

#不要な列削除(新しくできてる不要な列も一緒に)
chart_all_df = chart_all_df.drop(['uri','date','Unnamed: 0_x','key_x',
                                  'loudness','speechiness','acousticness',
                                  'instrumentalness','liveness','type','id',
                                  'track_href','analysis_url','duration_ms',
                                  'time_signature','key_y','Unnamed: 0_y','Date'],
                                 axis=1)

#'score'を数値に
chart_all_df = chart_all_df.replace({'A':4,'B':3,'C':2,'D':1})

今までの積み重ねがあるので、サクッと作ることができました。
ランダムフォレストにデータを渡します。

In
print(chart_all_df.columns.values)

#別のセルで実行したのでtrain_X・train_yのままでいけたけど、本来は別名を当てるべき
train_X = chart_all_df.drop(['score'],axis=1)
train_y = chart_all_df['score']
(train_X,test_X,train_y,test_y) = train_test_split(train_X,train_y,test_size=0.3,random_state=42)

random_forest2 = RandomForestClassifier(max_depth=25,n_estimators=80, random_state=42)
random_forest2.fit(train_X, train_y)
print(random_forest2.feature_importances_)

y_pred = random_forest2.predict(test_X)
trainaccuracy_random_forest2 = random_forest2.score(train_X, train_y)
print('TrainAccuracy:{}'.format(trainaccuracy_random_forest2))
accuracy_random_forest2 = accuracy_score(test_y, y_pred)
print('Accuracy:{}'.format(accuracy_random_forest2))
Out
['score' 'danceability' 'energy' 'mode' 'valence' 'tempo' 'season' 'temp'
 'Open']
[0.17016471 0.16592873 0.01521319 0.16867085 0.16955531 0.05301018
 0.13359326 0.12386376]
TrainAccuracy:1.0
Accuracy:0.8483974358974359

かなり精度が向上しました!やはりデータ数不足だったか…
'mode'があまり重要でないのは変わらずですが、'season'の重要度は少し上がりました。
前回の教師データでは、重複削除をした際に季節(週)で偏りが出てしまったのかもしれません。

検証

今回の教師データに入っていない、2023/10/5付の20位以内チャート(chart_test_df)で検証します。
気温22.4℃、株価30,994.67円、季節は秋
予測値で「4」が出れば正解です。

In
#chart_test_dfの作成 & データ整理

#20位以内のデータのみ抽出
pd1005_df = pd.read_csv('drive/My Drive/regional-jp-weekly-2023-10-05.csv')
test_df = pd1005_df[pd1005_df['rank'] <= 20]

#spotify APIより楽曲パラメータを取得
test_df['track_id'] = test_df['uri'].str.split(':',expand=True)[2]
chart_test_df = pd.DataFrame()
for track_id in test_df['track_id'] :
    df2 = pd.DataFrame.from_dict(spotify.audio_features(track_id))
    chart_test_df = pd.concat([chart_test_df,df2],ignore_index=True,axis=0)

#不要な列を削除
chart_test_df = chart_test_df.drop(['key','loudness','speechiness',
                                    'instrumentalness','liveness','type','id','uri',
                                    'track_href','analysis_url','duration_ms',
                                    'acousticness','time_signature'],axis=1)

#季節・気温・株価を追加
chart_test_df['season'] = 3
chart_test_df['temp'] = 22.4
chart_test_df['Open'] = 30994.67

#'score'列は作りません

ねえ、今更気づいたんだけどこのドロップするリストを別口で作っておいたら良かったんじゃない?

反省点も見つかったところで、検証データを2回目のランダムフォレストに渡してみます。

In
y_pred = random_forest2.predict(chart_test_df)
print(y_pred)
Out
[4 4 1 4 4 1 3 1 2 4 4 1 1 4 4 4 4 4 4 2]

12/20、6割は正解しています。
散布図で見た時にはびっくりするぐらい緩い相関だと思ったのですが、なんとかここまで漕ぎつけることができました。ランダムフォレストさんすごい!
(この結果を見て思いついた仮説④はアーティストの認知度との相関なのですが、今回は割愛させていただきます。)

ちなみに最初に作ったモデルだとこうなります。

In
y_pred = random_forest.predict(chart_test_df)
print(y_pred)
Out
[1 1 2 1 1 1 4 1 1 1 1 1 4 1 4 2 4 1 4 1]

やはり精度が低い。データ数だったんでしょうね。

総括

ここに書いたミス等はごく一部で、実際はもっとトライ&エラーを繰り返しています。
今回の作成を経て、データクレンジングとデータ数の重要性に改めて気付くことができました。本番よりも事前準備が大事なのはどの仕事も一緒ですね。
また、こまめなバックアップやデータ自体の確認が必要だったり、色々な経験を得ました。諦めない心大事。
やってみたい事はまだまだあるので、自己研鑽を積んでいきたいと思います。

最後に。

0からのPythonでも、ここまでできるぞ!

と叫ばせていただき、筆を置かせていただきます。
ここまでお付き合いいただき、ありがとうございました。

27
21
3

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
27
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?