よっしーの私的空間

機械学習を中心に興味のあることについて更新します

過去記事一覧

過去の記事についてまとめます。

1.画像データ分析

① 画像分析モデルの紹介

画像分析モデルの実装方法について。モデルの概要も記載しています。

② モデルの比較

画像分類モデルを試してみた結果。

③ 前処理
④ XAI(説明可能AI) 

2.テーブルデー

① 前処理
② XAI(説明可能AI) 

 3.株価予測

① LightGBMを使用した株価分析
② 前処理

 3.競馬予測

① データ入手
② 過去戦績に基づくレーティング

4.分析環境の構築

5.再現性確保

6.マルチGPU学習

7.エラー対処

J-Quantsデータを使用した重要指標(PER,PBR,ROE)推移の算出方法について

株価の割安さや収益性をはかる指標としてPER,PBR,ROEは非常に重要だと思います。ただ、これら指標の過去データの取得方法についてあまり情報が見つかりませんでした。無いなら自分で導こうということで、試行錯誤してみたのでブログにまとめます。

1.各種指標について

指標に関する説明は簡単に整理しますが、詳しくは色んなサイトで解説されています。

① PER(Price Earning Ratio):株価収益率

② PBR(Price Book-value Ratio):株価純資産倍率

ROE(Return On Equity):自己資本利益率

2.データ取得元:J-Quants

J-QuantsはJPX(日本取引所グループ)が個人投資家向けに運営するAPIデータ配信サービスです。API経由で最新の正確な金融データが簡単に取得できるおすすめのサービスですが、いくつか注意点があります。

  • 用途が個人の私的利用に限定されているのでデータの第三者配信やデータを利用したアプリの提供などが禁止されている。
  • 無償プラン、有償プランがあるが、ちゃんと分析しようとすると確実に有償プランが必要になる。

詳しくはJ-Quantsサイトを参照ください。

jpx-jquants.com

3.J-Quantsからのデータ取得方法

まずは上記J-Quantsサイトでアカウントを作成します。アカウント作成時にメールアドレスとパスワードを登録するのですが、それは後程API呼び出し時にも使います。 その後、プランを選択します。お試しで使いたい場合は無料プランで登録しましょう。

ここまで来たら準備完了です。PythonからAPIを呼び出せます。金融データを呼び出すためにはまずはIDトークンが必要になります。IDトークンはアカウント作成時に登録したメールアドレスとパスワードが必要になります。

import requests
import json

# アカウント情報格納
data={"mailaddress":"<YOUR EMAIL_ADDRESS>", "password":"<YOUR PASSWORD>"}

# リフレッシュトークンの取得
r_post = requests.post("https://api.jquants.com/v1/token/auth_user", data=json.dumps(data))
REFRESH_TOKEN = r_post.json()['refreshToken']

# IDトークンの取得
r_post = requests.post(f"https://api.jquants.com/v1/token/auth_refresh?refreshtoken={REFRESH_TOKEN}")
idToken = r_post.json()['idToken']

次に金融データを呼び出します。金融データの種類によって呼び出すAPIは異なります。例えば「株価四本値」の場合は"https://api.jquants.com/v1/prices/daily_quotes"です。以下は2024/4/1~5/1の任天堂の株価四本値を取得してpandas.DataFrameに格納する例です。

import pandas as pd

# リクエストのヘッダー情報とパラメータを設定
headers = {'Authorization': 'Bearer {}'.format(idToken)}
params = {'code': '79740', 'from':'20240401', 'to':'20240501'}

r = requests.get("https://api.jquants.com/v1/prices/daily_quotes",params=params, headers=headers)
df = pd.DataFrame(r.json()['daily_quotes'])

上記のcodeの部分は基本的に4桁の銘柄コードに”0”を末尾に付けたものですが、正確な情報は上場銘柄一覧のAPIを呼び出すことで確認することが可能です。

4.データ取得とPER,PBR,ROEの計算

PER,PBR,ROEを計算する場合は、株価四本値と財務情報のAPIが必要になります。財務情報APIでEPSとBPSを取得、株価四本値APIで株価を取得してそれらを計算することでPER,PBR,ROEを計算します。

import pandas as pd

# リクエストのヘッダー情報とパラメータを設定
headers = {'Authorization': 'Bearer {}'.format(idToken)}
params = {'code': '79740'}

# 株価四本値の取得
r = requests.get("https://api.jquants.com/v1/prices/daily_quotes",params=params, headers=headers)
df_quotes = pd.DataFrame(r.json()['daily_quotes'])

# 財務情報の取得
r = requests.get("https://api.jquants.com/v1/fins/statements", params=params, headers=headers)
df_statements = pd.DataFrame(r.json()['statements'])

EPSは現在値と会社予想値がありますが、将来予測を目的としているため、会社予想値を採用したいと思います。該当する変数は「ForecastEarningsPerShare」です。ただ、データを見ていただければ分かると思いますが、欠損値があります。これは年度末のデータに見られる傾向で、年度末時点でEPSは確定しているため発生しているものと思われます。そのため、欠損値は次年度のEPS「NextYearForecastEarningsPerShare」で補完します。そのうえで、株価データと突合してPERとPBRを計算します。

def replace_null(df, col):
    df.loc[df[col]=='', col] = None
    df[col] = df[col].fillna(method='ffill')
    return df

df_statements .loc[df_statements ['ForecastEarningsPerShare']=='', 'CurrentFiscalYearEndDate'] = df_statements ['NextFiscalYearEndDate']
df_statements .loc[df_statements ['ForecastEarningsPerShare']=='', 'ForecastEarningsPerShare'] = df_statements ['NextYearForecastEarningsPerShare']

df_statements = replace_null(df_statements , 'BookValuePerShare')
df_statements = replace_null(df_statements , 'NextFiscalYearStartDate')

df_statements = replace_null(df_statements , 'ForecastEarningsPerShare')
df_statements = replace_null(df_statements , 'BookValuePerShare')
df_statements ['ForecastEarningsPerShare'] = df_statements ['ForecastEarningsPerShare'].astype(float)
df_statements ['BookValuePerShare'] = df_statements ['BookValuePerShare'].astype(float)

df_adj = df_quotes[~(df_quotes['AdjustmentFactor']==1)].copy()

for index, row in df_adj.iterrows():
    # EPS
    adj_date = row['Date']
    adj_factor = row['AdjustmentFactor']
    df_stats.loc[df_stats['CurrentFiscalYearEndDate']<adj_date, 'ForecastEarningsPerShare'] = df_stats['ForecastEarningsPerShare']*adj_factor
    # BPS
    df_stats.loc[df_stats['NextFiscalYearStartDate']<adj_date, 'BookValuePerShare'] = df_stats['BookValuePerShare']*adj_factor

df_stats = df_stats.loc[~(df_stats.duplicated(subset=['DisclosedDate'], keep='last'))]

df= df_quotes.merge(df_statements , how='left', left_on='Date', right_on='DisclosedDate')
df['ForecastEarningsPerShare'] = df['ForecastEarningsPerShare'].fillna(method='ffill')
df['BookValuePerShare'] = df['BookValuePerShare'].fillna(method='ffill')
df['DisclosedDate'] = df['DisclosedDate'].fillna(method='ffill')
df['per'] = df['AdjustmentClose'] / df['ForecastEarningsPerShare']
df['pbr'] = df['AdjustmentClose'] / df['BookValuePerShare']
df['roe'] = df['pbr'] / df['per']

Pythonのf-stringsを使ったPandas.DataFrameの列名の動的設定

変数を使用してPandas.DataFrameの列名を動的に設定する方法についてまとめます。 Pythonにはフォーマット済み文字列リテラル(f-strings)という記法が存在するのですが、今回はそれを使用します。

1.フォーマット済み文字列リテラル(f-strings)とは

文字列の中に変数や式の値を埋め込む手法です。例を見た方が早いので、以下に例示します。

name = 'Tarou'
print(f'My name is {name}')

以下のように出力されます。

このように、文字列をf''で囲い、その中で変数や式を{}で囲うことで、変数や式の値を文字列に埋め込むことができます。 他にも出力する数字の桁数を指定したり色々とできます。詳細は以下、公式ドキュメントを参照ください。

docs.python.org

2.DataFrameの列名の動的設定

フォーマット済み文字列リテラルはDataFrameにも使えます。これも例を見てみましょう。

import pandas as pd
import plotly.express as px

df = px.data.stocks()
for i in range(1,4):
    df[f'GOOG_{i}'] = df['GOOG'].shift(i)
display(df.head())

こんな感じでラグ変数の変数名を設定するときに使うことができます。その他、変数の掛け合わせ等様々な用途でフォーマット済み文字列リテラルは使えます。

pandasでデータ抽出する際の速度の比較(loc対query)

Pandas Dataframeに対して、locで抽出した方が良いのか、queryで抽出した方が良いのか。 可読性はqueryの方が良さそうですが、今回は性能面で比較してみようと思います。

1.データダウンロード

データはsklearnの「カリフォルニア住宅価格」を使用しました。

import pandas as pd
from sklearn import datasets
df = datasets.fetch_california_housing(as_frame=True).frame

2.処理時間計測

locとqueryそれぞれで%%timeitを使用して実行時間を計測します。

%%timeit -n 100
df.loc[(df['MedInc']>2.5) & (df['MedInc']<5)]

%%timeit -n 100
df.query('2.5<MedInc<5')

3.結果

結果はlocの方が倍ぐらい早いようでした。 複雑な条件になる場合はqueryの方が可読性が高いので、何度も呼び出さない場合はqueryでも良いと思いますが、速度を重視する場合はlocの方が良さそうです。

foliumを使ってハザードマップを重ねる

1.概要

Pythonの地図ライブラリ「folium」を使ってハザードマップを重ね合わせたいと思います。

ハザードマップ国土交通省国土地理院が公開しているのですが、APIも公開しているので、色々自分でカスタマイズすることもできます。ハザードマップAPIについては以下にまとまってます。
ハザードマップポータルサイト

地図のベースはfoliumを使用します。FoliumはLeaflet.jsライブラリをPythonで元々はJavascriptで作られたライブラリです。サイトとかを作るならLeaflet.jsの方が便利だと思います。 foliumを使うことで、簡単に地図上にマーカー、ライン、ポリゴンなどの地理情報を表示し、これらの要素にカスタマイズしたポップアップ情報を追加することができます。また、地図のズーム、位置、スタイルを制御できます。
GitHub - python-visualization/folium: Python Data. Leaflet.js Maps.

2.foliumについて

まずはfoliumから簡単に触ってみます。

import pandas as pd
import folium

latlons = ['35.658581', '139.745433'] # 東京タワーの緯度経度

fmap = folium.Map(
    location=latlons,
    tiles = "OpenStreetMap",
    zoom_start = 15, 
    width = 500, height = 500
)
folium.Marker(latlons, popup="東京タワー").add_to(fmap)
fmap

上記を実行すると以下のような地図が表示されます。

地図情報はOpenStreetMapというものを使用しています。GoogleMapを使うことも可能ですが、OpenStreetMapは無料なので、個人的にはこちらの方が好きです。 東京タワーの緯度経度情報をインプットに地図を表示しています。少し調べると住所から緯度経度情報を取得する方法も色々あるので、組み合わせてみても良いと思います。

3.ハザードマップを重ねてみる

次にハザードマップを重ねてみようと思います。

import pandas as pd
import folium

latlons = ['35.658581', '139.745433'] # 東京タワーの緯度経度

fmap = folium.Map(
    location=latlons,
    tiles = "OpenStreetMap",
    zoom_start = 15, 
    width = 500, height = 500
)
folium.Marker(latlons, popup="東京タワー").add_to(fmap)

fmap = add_tile_layer(
    fmap=fmap,
    tiles='https://disaportaldata.gsi.go.jp/raster/05_dosekiryukeikaikuiki/{z}/{x}/{y}.png',
    name='土砂災害警戒区域(土石流)'
)

fmap = add_tile_layer(
    fmap=fmap,
    tiles='https://disaportaldata.gsi.go.jp/raster/01_flood_l2_shinsuishin_data/{z}/{x}/{y}.png',
    name='洪水浸水想定区域(想定最大規模)'
)

fmap = add_tile_layer(
    fmap=fmap,
    tiles='https://disaportaldata.gsi.go.jp/raster/04_tsunami_newlegend_data/{z}/{x}/{y}.png',
    name='津波浸水想定'
)
folium.LayerControl().add_to(fmap)
fmap

国土地理院の公開するAPIから土石流、洪水、津波に関するハザードマップを呼び出してます。 LayerControlで重ねたハザードマップのレイヤーを消したり表示したりするコントロールを付与してます。 東京タワー周辺、、結構赤くなってますね。。

他にも公開されている情報はたくさんあるので、色々組み合わせてみると面白いかもしれません。

Pythonで株価データや金融データを取得する方法(pandas_datareader)

pandas-datareaderというpythonライブラリを使用して株価データや金融データを取得する方法についてまとめます。pandas-datareaderとはウェブ上の様々なデータソースにアクセスするライブラリです。今回は以下にアクセスします。

アクセス先 概要
Stooq ポーランドのサイトで日本の株価データも取り扱っている。
St.Louis FED (FRED) セントルイス連邦準備銀行の公開している経済統計データ集。
株価データは取り扱っていない。

上記以外のデータソースは以下にまとまっています。
Data Readers — pandas-datareader v0.10.0 documentation

1.pandas-datareaderの使用方法

① インストール

pipコマンドを使用して以下の通りインストール。

pip install pandas-datareader

② Stooqからデータ取得

以下コードでStooqからデンソーの株価データを取得します。

# ライブラリのインポート
import pandas_datareader.stooq as web
from datetime import datetime

# データ取得期間の設定
st = datetime(2015, 1, 1)
ed = datetime(2020, 1, 1)

# 株価価格等の取得
lst1=['6902.JP', #デンソー
    ]
stooq = web.StooqDailyReader(lst1,start=st,end=ed).read()
display(stooq.head())

<出力結果>

③ FREDからデータ取得

次にFREDからは株価以外の経済統計データを取得してみます。
試しにResidential Property Prices for Japanを取得してみます。

# ライブラリのインポート
import pandas_datareader.fred as web
from datetime import datetime

# データ取得期間の設定
st = datetime(2015, 1, 1)
ed = datetime(2020, 1, 1)

# Residential Property Prices for Japanを取得
fred = web.FredReader('QJPN368BIS',start=st,end=ed).read()
display(fred.head())

<出力結果>

Seleniumエラー:Element is not clickable at point (xxx, xxx)について

Selenium使用時に「Element is not clickable at point (xxx, xxx)」というエラーが出て色々調べたので、その内容についてまとめます。

1.結論

最初に結論だけ述べると、、

  • エラー原因:画面外のelementを操作しようとしたため
  • 解決方法:対象のelementまでSeleniumでスクロールする
2.事象再現

まずは適当にSeleniumを使って事象を再現してみます。

#モジュールのインポート
import glob
import datetime
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

# ブラウザ起動
driver = webdriver.Chrome()
driver.maximize_window()

# アクセスするURL
TARGET_URL = "https://suumo.jp/chintai/tokyo/ensen/"

# 対象サイトへアクセス
driver.get(TARGET_URL)
time.sleep(2)

#チェックボックスをクリック
elements = driver.find_elements(By.CLASS_NAME, 'js-fr-checkSingle')
elements[10].click()
elements[30].click()

試していただければ分かると思いますが、実はelement[10].click()をコメントアウトすると、element[30]が画面外にあるボタンであるにも関わらず、エラーは起きないです。つまり、より詳細な原因としては、「一つ目のボタンを選択した状態で、画面外のボタンを操作しようとするとエラーになる」ようです。

3.解決方法

解決するために、Seleniumを使って画面スクロールします。

#element[30]が画面の真ん中らへんに来るように調整
window_size = driver.get_window_size()
loc_y =  elements[30].location['y']
if loc_y < window_size['height']/2:
    scroll_y = 0
else:
    scroll_y = loc_y - window_size['height']/2
driver.execute_script(f"window.scrollBy(0, {scroll_y});")
elements[30].click()

以下のようにシンプルに対象elementまでスクロールしちゃってもいいのですが、WEBページによってはヘッダが邪魔で対象elementが隠れちゃうことがあります。隠れちゃうと同じエラーが出るので、少しめんどくさいですが、対象elementが真ん中らへんに来るように調整してます。スマートな実装があれば教えてほしいです。

4.ソース一式

#モジュールのインポート
import glob
import datetime
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

# ブラウザ起動
driver = webdriver.Chrome()
driver.maximize_window()

# アクセスするURL
TARGET_URL = "https://suumo.jp/chintai/tokyo/ensen/"

# 対象サイトへアクセス
driver.get(TARGET_URL)
time.sleep(2)

#チェックボックスをクリック
elements = driver.find_elements(By.CLASS_NAME, 'js-fr-checkSingle')
elements[10].click()

#element[30]が画面の真ん中らへんに来るように調整
window_size = driver.get_window_size()
loc_y =  elements[30].location['y']
if loc_y < window_size['height']/2:
    scroll_y = 0
else:
    scroll_y = loc_y - window_size['height']/2
driver.execute_script(f"window.scrollBy(0, {scroll_y});")
elements[30].click()

タスクスケジューラで陥りやすいトラブル集

タスクスケジューラ関連で個人的に躓いたポイントとその解決策についてまとめておきたいと思います。

1.予定時刻になってもタスクが実行されない問題

① 事象

以下のように繰り返し実行のタスクを作成するが、予定時刻になってもタスクが実行されないという事象。

予定時刻になっても、エラー表示も出ないままタスクが実行されず、勝手に「次回の実行時刻」が延期されます。エラーが出ないので、原因特定に時間がかかりました。

② 原因と対策

どうやら、これは「トリガー」設定に問題があったようです。以下のように、周期を[毎日]と設定したうえで、継続時間を[無期限]と設定するとタスクが正常に動作しないとのことです。Microsoft Japan Windows Technology Support Blogに情報があったので、詳しくはそちらを参照ください。

正確には以下のパターンのときに問題が発生するようです。
× : 毎日 / 毎週 / 毎月 + 繰り返し間隔の継続時間「無期限」

以下の場合はOKとのこと。
○ : 毎日 / 毎週 / 毎月 + 繰り返し間隔の継続時間「1 日間」など
○ : 1 回 + 繰り返し間隔の継続時間「無期限」

2.タスクは実行されるけどバッチが実行されない問題

① 事象

タスクスケジューラ上で「前回の実行時刻」が更新される(タスク自体は実行されている)のに、タスクから呼び出すバッチが起動しない事象。

これもエラーも出ず、ただただバッチが実行されないので、原因がなかなかわかりませんでした。

② 原因と対策

「操作の編集」の「開始(オプション)」が指定されていないのが問題らしいです。タスクスケジューラに関する記事を見ても「開始(オプション)」を必須と紹介する記事は少ないので、設定しなくても問題なく動くパターンもあるんだと思います。