Pythonで財務分析2 ~有価証券報告書からのデータ抽出~
はじめに
前回はEDINET APIを介して調べたい企業の10年分の有価証券報告書に基づくデータを取得するところまでできました。
本記事では取得したデータからROAツリー作成に必要なデータを抽出する部分を紹介します。
やりたいこと
こんな感じで財務指標をツリー構造にしてグラフ表示させたい。
ROA
|--総資産回転率
| |--固定資産回転率
| |--有形固定資産回転率
| |--無形固定資産回転率
| |--運転資金回転日数
| |--売掛債権回転日数
| |--買掛債権回転日数
| |--在庫回転日数
|--営業利益率
| |--原価率
| |--販管費率
それぞれの指標は
- ROA = 営業利益 / 資産合計
- 総資産回転率 = 売上高 / 資産合計
- 固定資産回転率 = ...
など有価証券報告書から得られる財務データを使うことで求められる。
10年分のデータをいちいち目視&手動で打ち込むなんてやってられない、文明の利器を使えば自動化なんて楽ショーでしょう。
意気揚々と取り組み始めたものの待ち受けていたのは地獄そのものでした。
こちらの記事に大いに賛同します。
PythonでTDNETとEDINETからXBRLデータを集めたら地獄だった話。
EDINETから取得したデータとの格闘
EDINETからは有価証券報告書に基づくデータが取得できると前述しましたが、具体的にはxbrl形式, csv形式, pdf形式で得られます。
旧来はxbrl形式が主流だったようですが最近のアップデートでcsv形式でも取得できるようになり、人間に優しい仕様変更が行われたようです。
ボツ案1: csvファイルからのデータ抽出
csv形式であればpandasでDataFrame化して必要な情報だけ抜き出せばよいと甘く考えていましたが、やはり現実は厳しい。
下記理由で今回やりたいことの目的を果たせないと判断しました。
- 連結決算の財務情報が欠落している
- 企業によって会計基準が異なり、個別の財務情報を表すタクソノミが異なる。
すなわち企業ごとに{"取得したい財務指標":"企業ごとに微妙に異なるタクソノミ"}の辞書を手動で用意しなければならない。
ボツ案2: xbrlファイルからのデータ抽出
csvファイルには連結決算のデータが欠落しているのが致命的でした。そこで有価証券報告書から目視で読み取れる連結決算の売上の数値をxbrlファイルで検索するとちゃんとヒットするではないか。つまり頑張れば財務情報抽出の完全自動化ができるということです。
ところがよくよく見てみると連結決算の財務情報はインラインxbrlの中に埋め込まれており、企業によって構造がまちまちだったので諦めました。
ボツ案3: pdfファイルから目視で手打ち
csvにしろxbrlにしろ企業間でのデータ構造の差異を解決するためには手動で何かしら準備しなければならないことが判明し、もはやそうなればpdfファイルを開いて目視で打ち込んだほうが早いのではと考えるに至りました。
ところが数値をテンキーで打ち込むと当然打ち込みミスが発生したりするわけで、数値をコピーしようとするも筆者環境のpdfリーダーでは文字列コピーができず、著しく効率が悪いため、この方法もボツとしました。
苦闘の末に採用した案
最終的に採用した手段は「xbrlファイルを取得する際に同梱されるhtmファイルから連結財務指標が記載されたファイルのみを抽出し、これらをブラウザで開いて個別の財務情報を手動で別のcsvファイルにコピペする」です。
pdfファイルから目視で手打ちする方法と比べると飛躍的に転記ミスが減り、現時点で考えられる方法の中で最も確実かつ効率的と考え、採用に至りました。
かろうじて「xbrlファイルを取得する際に同梱されるhtmファイルから連結財務指標が記載されたファイルのみを抽出し、」の部分だけはPythonで自動化することができ、幾ばくかの効率改善に寄与しました。今回使用したソースコードを公開します。このままで動くわけではないですが部分的にでも参考になれば幸いです。
import os
from pathlib import Path
import zipfile
import pandas as pd
def main():
try:
# 処理対象を受け付ける
print("Enter edinet code.")
edinet_code = input(">>> ")
data_dpath = os.path.join(Path(__file__).parent.parent, "data", edinet_code)
raw_dpath = os.path.join(data_dpath, "00_raw")
dst_dpath = os.path.join(data_dpath, "01_dst")
if os.path.exists(raw_dpath) == False or os.path.exists(dst_dpath) == False:
print("raw data or dst data was not found.")
raise Exception()
# zipを解凍する
zipfiles = [f for f in os.listdir(raw_dpath) if f.endswith("_1.zip")]
for fname in zipfiles:
zipfpath = os.path.join(raw_dpath, fname)
with zipfile.ZipFile(zipfpath, "r") as zip_ref:
files = zip_ref.namelist()
target = [f for f in files if f.endswith("htm") and 'honbun' in f]
for t_fname in target:
zip_ref.extract(t_fname, path=raw_dpath)
# 必要なファイルを特定
htm_dpath = os.path.join(raw_dpath, "XBRL", "PublicDoc")
htm_files = [f for f in os.listdir(htm_dpath)]
for htm_fname in htm_files:
fpath = os.path.join(htm_dpath, htm_fname)
with open(fpath, "r", encoding='utf-8') as f:
lines = f.readlines()
s = ''.join(lines)
if "【連結損益計算書】" in s or "【連結財務諸表】" in s:
print(f"extract {fpath}")
else:
# 不要なファイルを削除
os.remove(fpath)
# ROAツリー用データのテンプレートを生成
years = [v for v in range(2013, 2023)]
t_df = pd.DataFrame(columns=years, index=['010_売上高', '020_売上原価', '030_売上総利益', '040_販管費', '041_研究開発費', '050_営業利益',
'110_資産合計', '120_流動資産', '121_売上債権', '122_棚卸資産', '130_固定資産',
'131_有形固定資産', '132_無形固定資産', '140_負債合計', '150_流動負債', '151_買入債務',
'160_固定負債', '170_純資産', '171_株主資本等'])
dst_fpath = os.path.join(dst_dpath, "roatree.csv")
if os.path.exists(dst_fpath) == False:
t_df.to_csv(dst_fpath, encoding='shift-jis')
print(f"save file. {dst_fpath}")
except Exception as ex:
print(ex)
if __name__ == '__main__':
main()
Discussion