インバウンドによる経済効果は予測できるのか 〜育児と仕事とリスキリング記録〜
育休中〜復職後までの6ヶ月間、Aidemyのデータ分析コースを受講しました。この記事では、最終課題で作成したインバウンドによる経済効果予測モデルについて、モデルの構築過程と結果を載せています。併せて、育休中〜復職時期にかけて、どのように受講を進めたかについても、さいごに書いています。子育てしながらリスキリングはできるのか、半年前の私と同じように悩んでいる方にとって、少しでも参考になれば嬉しいです。
自己紹介
こんにちは。ヤドカリです。
以前からPythonやデータ分析というワードに興味を抱いており、いつか勉強したいと思っていたところ、良いタイミングでAidemyのコースを見つけ、一念発起。6ヶ月間のデータ分析コースを受講しました。
自己紹介をすると、現在は機械メーカーにて製品開発を行なっています。プログラミング経験はなし。データ分析は今すぐに必要なスキルではありませんが、将来的な転職に向けたスキルアップや、現在の業務での効率化にも役立つため、受講しました。
インバウンドの経済効果予測
(1)モデル作成の目的
「インバウンド」の言葉を耳にする機会が多いように、訪日外国人数は年々伸びています。下図の推移を見ると、2010年~2019年にかけて2倍以上に増加。コロナ禍で一時は減少しましたが、現在はまた全国各地に賑わいが戻ってきています。
インバウンドで期待されるのは、やはり経済効果です。この経済効果を何かしらの方法で予測できないかと考え、モデルを作成することにしました。
(2)実行環境
プラットフォーム:Google Colaboratory
python version:Python 3.10.12
使用PC:MacBook Air (Intel Core i5)
(3)データ取得
インバウンドに関するデータとして、(1) 訪日外客数、(2) 訪日外国人消費動向を、それぞれ政府の統計データから入手しました。
また、消費動向と関連がありそうなデータとして(3)為替データを、日本銀行/時系列統計データ検索サイトから入手しました。
(1) 訪日外客数
国籍/月別 訪日外客数(2003年~2024年)(Excel)を使用
(2) 訪日外国人消費動向
2013年1月〜2023年12月までの、四半期ごとのデータを使用
訪日外国人へのアンケート調査結果から、訪日の目的や、個人消費額(宿
泊料金、飲食費等の用途別)のデータが入っており、モデルに関連しそう
な項目をピックアップ
(3) 為替データ
東京市場 ドル・円スポット 17時時点/月末データを使用
上記データ3種のうち(2)訪日外国人消費動向データは、現在取得できるデータが2013年1月〜2023年12月の期間に限られ、また区間が四半期ごとでした。これに併せて、他データも同じ期間のデータを入手し、(1)訪日外客数は四半期ごとの合計値、(3) 為替データは四半期ごとの平均値を算出。以上をまとめたCSVを作成しました。
(4)データの前処理
Google Colaboratoryを動かしていきます。
作成したCSVを読み込みます。
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.linear_model import Ridge
from sklearn.linear_model import ElasticNet
from sklearn.metrics import accuracy_score
#データの読み込み
#訪日観光客数、消費額データ
df_inbound = pd.read_csv('/content/drive/MyDrive/インバウンド効果データ.csv')
#為替データ(3ヵ月の平均値)
df_exchange = pd.read_csv('/content/drive/MyDrive/為替平均データ.csv')
display(df_inbound.head())
print()
display(df_exchange.head())
2つのCSVファイルを読み込み、下記2つのデータフレーム を作成しました。
df_inbound :訪日の目的、項目別消費額、訪日外客数(総数)
df_exchange:為替
なお、時期の「2013-01」は、2013年の第一四半期(1~3月)を示します。
df_inboudには、"購入者単価_○○"という名前のカラムが並んでいます。これは、滞在中の個人消費額をアンケート調査し、用途ごとに平均した値です。
次に、データの型を確認します。
print(df_inbound.dtypes)
print()
print(df_exchange.dtypes)
数値データとして扱えるように、文字列データ(object)を数値データ(float)に変更します。
#データ型をstr→floatに変更するにあたって、不要なカンマ","を削除しておく
df_inbound = df_inbound.replace(',', '', regex = True)
#データ型をstr→floatに変更
df_inbound["目的_観光・レジャー"] = df_inbound["目的_観光・レジャー"].astype(float)
df_inbound["購入者単価_宿泊料金"] = df_inbound["購入者単価_宿泊料金"].astype(float)
df_inbound["購入者単価_飲食費"] = df_inbound["購入者単価_飲食費"].astype(float)
df_inbound["購入者単価_交通費"] = df_inbound["購入者単価_交通費"].astype(float)
df_inbound["購入者単価_娯楽サービス費"] = df_inbound["購入者単価_娯楽サービス費"].astype(float)
df_inbound["購入者単価_買い物代"] = df_inbound["購入者単価_買い物代"].astype(float)
df_inbound["購入者単価_その他"] = df_inbound["購入者単価_その他"].astype(float)
df_inbound["訪日外客数(総数)"] = df_inbound["訪日外客数(総数)"].astype(float)
#データ型を確認
print(df_inbound.dtypes)
数値データに変更できました。
現在のデータには、訪日外国人一人あたりの合計消費額がありません。そこで"購入者単価_○○"のカラムを合計し、"一人あたり合計消費額"として算出します。
また、一人あたりの合計消費額と訪日外客数を掛け合わせた"全体消費額"も算出します。この"全体消費額"がインバウンドによる経済効果に当たり、今回のモデルの目的変数になります。
#新たな列を追加
df_inbound["一人あたり合計消費額"] = df_inbound["購入者単価_宿泊料金"] + df_inbound["購入者単価_飲食費"] + df_inbound["購入者単価_交通費"] + df_inbound["購入者単価_娯楽サービス費"] + df_inbound["購入者単価_買い物代"] + df_inbound["購入者単価_その他"]
df_inbound["全体消費額"] = df_inbound["一人あたり合計消費額"] * df_inbound["訪日外客数(総数)"]
#不要な行を削除
df_inbound = df_inbound.drop(33, axis=0)
print(df_inbound.tail())
為替データのみ、DataFrameが分かれているため、全データを結合します。
#訪日観光客データと為替データを結合
df_all = pd.merge(df_inbound, df_exchange, on = '時期', how='inner')
(5)データの傾向の確認
主要項目の時系列推移を確認します。主要項目は下記4つと考えました。
訪日外客数(総数)
一人あたり合計消費額
全体消費額
円/ドル_平均
import matplotlib.pyplot as plt
time = df_all['時期']
number_of_visitors = df_all["訪日外客数(総数)"]
consumption_per_person = df_all["一人あたり合計消費額"]
consumption_whole = df_all["全体消費額"]
exchange = df_all["円/ドル_平均"]
plt.plot(time, number_of_visitors)
plt.xlabel("time")
plt.ylabel("number_of_visitors")
plt.show()
plt.plot(time, consumption_per_person)
plt.xlabel("time")
plt.ylabel("consumption_per_person")
plt.show()
plt.plot(time, consumption_whole)
plt.xlabel("time")
plt.ylabel("consumption_whole")
plt.show()
plt.plot(time, exchange)
plt.xlabel("time")
plt.ylabel("exchange")
plt.show()
全体消費額と円/ドル_平均は、似た挙動で推移しているようです。
次に、主要項目間の相関関係を確認するため、SeabornのHeatmapを用いて可視化します。
#必要なカラムを抽出
df_all = df_all[["訪日外客数(総数)", "一人あたり合計消費額", "円/ドル_平均", "全体消費額"]]
#ヒートマップにカラム名を記載できるよう、英語表記に変更
df_all_rename = df_all.rename(columns={"訪日外客数(総数)": 'number_of_visitors',
"一人あたり合計消費額": 'consumption_per_person',
"円/ドル_平均": 'exchange',
"全体消費額": 'consumption_whole'})
#データ間の相関を確認
sns.heatmap(
df_all_rename[['number_of_visitors', 'consumption_per_person', 'exchange', 'consumption_whole']].corr(),
vmax=1, vmin=-1, annot=True
)
2項目間の相関係数がヒートマップ状に可視化されました。
上図から、下記3点がわかりました。
一人あたり合計消費額(consumption_per_person)と円/ドル_平均(exchange)に、正の強い相関(相関係数0.65)がある
訪日外客数(総数)(number_of_visitors)と円/ドル_平均に弱い相関(相関係数0.32)がある
全体消費額(consumption_whole)と円/ドル_平均に、正の強い相関(相関係数0.65)がある
時系列推移のグラフでは掴めなかった1項、2項の関係も、相関係数を出すことで明確になりました。
以上の結果から、今回は説明変数を円/ドル_平均、目的変数を全体消費額として、モデルを作成することにしました。
(6)モデルの作成、実行(KFoldを実施した回帰分析)
いよいよモデルの作成です。
変数trainに説明変数を、変数targetに目的変数を入れます。
#説明変数、目的変数を定義
#コロナ禍になる前までのデータ(2019年まで)を抽出し使用
train = df_all.drop(["一人あたり合計消費額", "訪日外客数(総数)", "全体消費額"], axis=1)[:28]
target = df_all["全体消費額"][:28]
#データサイズを確認
print(train.shape)
print()
print(target.shape)
train, targetのデータサイズはそれぞれ(28, 1), (28, )であり、同じデータ数(行数)であることを確認できました。
※データ数が異なるとモデル作成時にエラーが発生するため、
事前に確認しておくと安心です(私はこのミスを何度かやりました)
今回はデータ数が少ない点が課題です。モデル評価時には、データを学習用と評価用に分けますが、データ数が少ない場合にはデータの分け方によってモデル精度がブレてしまう可能性があります。そこで、データが少ない場合に適するとされるk-分割交差検証(KFold)を実施します。交差検証では訓練データをk分割し、そのうち1つを検証データ、残りのk-1個を訓練データとします。k値は3, 5, 10を取ることが多く、今回はデータが少ないことからk=3にて実施してみます。
今回行う分析は、説明変数から目的変数の数値を予測する「回帰分析」です。交差検証にてデータを3分割した後、下記4種のモデルで評価し、決定係数が最も良いモデルを判別します。なお、決定係数は、予測データと正解データがどれくらい一致しているかを示します。
線形回帰
ラッソ回帰(L1正則化を行いながら線形回帰)
リッジ回帰(L2正則化を行いながら線形回帰)
ElasticNet回帰(ラッソ回帰とリッジ回帰を組み合わせて線形回帰)
from sklearn.model_selection import KFold
#k-分割交差検証を実施(k値は3とする)
cv = KFold(n_splits=3, random_state=0, shuffle=True)
#各モデルでのFoldごとの決定係数scoreを収納するリストを作成
LinearRegression_score_list = []
Lasso_score_list = []
Ridge_score_list = []
ElasticNet_score_list = []
#データを3分割し、trainデータとvalデータにわける
for i ,(trn_index, val_index) in enumerate(cv.split(train, target)):
X_train ,X_val = train.loc[trn_index], train.loc[val_index]
y_train ,y_val = target[trn_index], target[val_index]
#線形回帰モデルを実行し、決定係数を算出
model = LinearRegression()
model.fit(X_train, y_train)
LinearRegression_score = model.score(X_val, y_val)
LinearRegression_score_list.append(LinearRegression_score)
#Lasso回帰モデルを実行し、決定係数を算出
model = Lasso()
model.fit(X_train, y_train)
Lasso_score = model.score(X_val, y_val)
Lasso_score_list.append(Lasso_score)
#Ridge回帰モデルを実行し、決定係数を算出
model = Ridge()
model.fit(X_train, y_train)
Ridge_score = model.score(X_val, y_val)
Ridge_score_list.append(Ridge_score)
#ElasticNet回帰モデルを実行し、決定係数を算出
model = ElasticNet(l1_ratio = 0.1)
model.fit(X_train, y_train)
ElasticNet_score = model.score(X_val, y_val)
ElasticNet_score_list.append(ElasticNet_score)
#各モデルでの決定係数を出力(決定係数は3つのFoldでの結果を平均して出力)
print('線形回帰の平均決定係数 : {:.3f}'.format(np.mean(LinearRegression_score_list)))
print('Lasso回帰の平均決定係数 : {:.3f}'.format(np.mean(Lasso_score_list)))
print('Ridge回帰の平均決定係数 : {:.3f}'.format(np.mean(Ridge_score_list)))
print('ElasticNet回帰の平均決定係数 : {:.3f}'.format(np.mean(ElasticNet_score_list)))
#4つのモデルの中で、最もよい決定係数のモデルを判断し出力
best_score = 0
list = [["LinearRegression", np.mean(LinearRegression_score_list)], ["Lasso", np.mean(Lasso_score_list)], ["Ridge", np.mean(Ridge_score_list)], ["ElasticNet", np.mean(ElasticNet_score_list)]]
for model, mean_score in list:
if mean_score > best_score:
best_score = mean_score
best_model = model
print()
print("最も良いモデルは{}で、その決定係数は{:.3f}です。".format(best_model, best_score))
>出力結果
線形回帰の平均決定係数 : 0.245
Lasso回帰の平均決定係数 : 0.245
Ridge回帰の平均決定係数 : 0.245
ElasticNet回帰の平均決定係数 : 0.248
最も良いモデルはElasticNetで、その決定係数は0.248です。
最も良いモデルでも決定係数が低く、残念ながら精度は低い結果でした。
また、4つのモデル間での決定係数の差はほぼありませんでした。
(7)モデルの見直し(外れ値の除外)
モデルの予測精度が低い原因として、外れ値を含んでいる可能性があります。そこで、Seabornのboxplotを用いて箱ひげ図を描いてみます。
sns.boxplot(y=df_all["円/ドル_平均"])
上図から、外れ値がいくつか存在していました。これらのデータを除いてモデルを作成すれば、予測精度が向上する可能性があります。
外れ値を除く方法として、LOF(Local Outlier Factor)を用います。
LOF(Local Outlier Factor):データの密度に基づいて外れ値を検知
・k個の近傍点を使ってデータの密度を推定
・推定した密度が相対的に低い点を外れ値と判定
今回用いているデータは総数28個とそもそも少なく、密度を推定するためのk個(n_neighbors)をどのように設定したらよいかわかりませんでした。事前にn_neighborsの値を振ってテストしたところ、n_neighbors=6にて良好なデータを得られましたので、この条件で外れ値を除いた上で、先ほど最良なモデルであったElasticNet回帰モデルで評価します。
from sklearn.neighbors import LocalOutlierFactor
clf = LocalOutlierFactor(n_neighbors=6)
predictions = clf.fit_predict(train)
#外れ値を除去したデータを用意
#この際、インデックスを振り直しておく(後段のKFoldで分割エラーが発生しないようにする)
re_train = train[predictions == 1].reset_index(drop=True)
re_target = target[predictions == 1].reset_index(drop=True)
cv = KFold(n_splits=3, random_state=0, shuffle=True)
score_list = []
for i ,(trn_index, val_index) in enumerate(cv.split(re_train, re_target)):
print(f'Fold : {i}')
X_train ,X_val = re_train.loc[trn_index], re_train.loc[val_index]
y_train ,y_val = re_target[trn_index], re_target[val_index]
model = ElasticNet()
model.fit(X_train, y_train)
score = model.score(X_val, y_val)
print(f'{score:.3f}')
score_list.append(score)
mean_score = np.mean(score_list)
print('-'*10 + 'Result' +'-'*10)
print(f'決定係数平均値 : {mean_score:.3f}')
>出力結果
Fold : 0
0.457
Fold : 1
0.578
Fold : 2
0.328
----------Result----------
決定係数平均値 : 0.455
各Foldで得られた決定係数と、それらの平均値を得られました。
決定係数が0.248→0.455まで向上しており、予測精度が改善されたように見えます。
(8)モデルの妥当性確認(精度のブレはあるか)
前項にて予測精度は改善されましたが、交差検証でのデータの振り分け方によっては、予測精度がブレている可能性があります。最後に、このブレの有無を確認します。
KFoldを行う際、random_stateを固定していましたが、この乱数を0~20で振ることで交差検証でのデータの振り分け方を変え、決定係数がどのように変化するかを確認します。使用するデータは前項で用いた外れ値を除外したデータとします。
mean_score_list = []
#KFoldの乱数を固定せず、0~20で振る
for r in range(0, 21):
cv = KFold(n_splits=3, random_state=r, shuffle=True)
score_list = []
for i ,(trn_index, val_index) in enumerate(cv.split(re_train, re_target)):
X_train ,X_val = re_train.loc[trn_index], re_train.loc[val_index]
y_train ,y_val = re_target[trn_index], re_target[val_index]
model = ElasticNet()
model.fit(X_train, y_train)
score = model.score(X_val, y_val)
score_list.append(score)
mean_score = np.mean(score_list)
mean_score_list.append(mean_score)
x = range(0, 21)
plt.plot(x, mean_score_list)
plt.xlabel("random_state_number")
plt.ylabel("mean_score")
plt.show()
残念ながら、random_stateの値によって決定係数が大きく変動しました。
今回用いたデータ数が少ないために、モデル精度はデータの偏りに大きく影響を受けていました。KFoldを行うことでモデル精度のブレを抑えられると期待しましたが、やはりデータ数が少なすぎたようです。
(9)結果と考察
為替データに基づいてインバウンドによる経済効果を予測した結果、回帰分析でモデルを作成できたように思われましたが、データの分け方によって予測精度が大きくブレてしまいました。精度向上に向けた取り組みとして、下記2項が考えられます。
データ数を増やす:残念ながら消費動向の過去データは存在しないため、未来のデータを増やすしかありません
説明変数を増やす:旅行口コミサイトにおける日本の評価数、注目度ランキング等、訪日の動機付けに関連しそうな数値を説明変数に加える
おわりに
今後の展望
Aidemyを始めたときには、過去受講者の最終成果物を見て「こんなことが私にもできるのか、、?」と不安でした。6ヶ月経った今でも、ゼロから全てを一人で構築することはできません。それでも、コードを見れば何をしているのか理解できるようなりましたし、分析のためにどのような工程が必要なのかがわかりました。成長したな〜と感じます。
せっかく身につけたスキルなので、数年内の転職を目指して、今後もkaggle等を活用しながら勉強を続けていきます。
育児と仕事とリスキリングは、どうにかできる
私がAidemyを始めたのは育休中、あと3ヶ月で復職という時期でした。育休中のリスキリングについは、ネガティブな意味で話題になったこともありますが、私はこのタイミングで受講してよかったと感じています。というのも、ちょうど下記条件が揃っていたからです。
受講を始めた時期に、子の生活リズムが落ち着いていた(1歳4ヶ月頃)
私自身が子育てに慣れてきていた
保育園の一時預かりを利用できた
夫と家事・育児を分担できた
勉強時間の確保ですが、育休中は週一回保育園の一時預かりを利用しました。私の住む地域では、勉強を理由にした一時預かりが可能だったため、非常に助かりました。その他の日は、子のお昼寝中や、夜中に少しずつ受講。復職後は、夜は疲れて子と寝落ちする日々が続いたため、頭がすっきりしている早朝に30分~1時間ほど勉強を進めました(毎日ではありません)。
Aidemyでは、オンラインでカウンセリングを依頼し、質問をすることができます。ただ、時間が22時までで私の生活リズムとは合わなかったため、わからないことがあればSlackでどんどん質問をしました。プログラミングの勉強は、調べてもわからなくてつまづく、ということが多々あるように思います。「こんなこと聞いていいのか?」というレベルの質問も、丁寧に回答をもらえたので、Slackで質問できるシステムは助かりました。
育児と仕事とリスキリングを同時に進めるのは、正直楽ではありませんでした。それでも、「時間はつくるもの」と考えて、どうにか少しずつ進めることができました。
今は受講を無事に終えられてほっとしています。今後もどの程度時間を確保できるかわかりませんが、少しずつ勉強を進めていきたいです。
***
このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています。