XBRLの内容をデータフレームに変換して、Excel形式(xlsx)で保存するコード例です。
2014年以降のEDINET XBRLを読み込むコードです。
実行すると、すべての勘定科目のタグ名・日付・金額がxlsx形式で保存されます。
保存はpandasデータフレームの機能を使っていますので、少し変更すれば、csvやpickleでも保存できます。
XBRLからスキーマファイル(xsd)へのファイルパスも取得します。
勘定科目にPDFのような日本語名・英語名をつけたい場合は、このxsdファイルからラベルファイルをたどって読み込んでいくことになります。
この記事では、XBRLから勘定科目を抽出して保存する部分のコード例を示します。
XBRLを解析するクラスを作る
XBRL解析で財務情報などを取得するクラスを作ります。
もちろん、全部関数で作ってもいいのです。最初そうしようって思って作ってたんですが、引数がいっぱいになっちゃったんですね。試行錯誤するときに、引数を追加したり削ったりするのが大変で、やっぱりクラスにしようってなりました。デバッガで変数を見るときも、まとめて見ることができますので、やはり便利でした。
名前空間を定義する
XBRLファイルを見ると、タグには以下のように接頭辞がくっついています。
jpcrp_cor:NetSalesSummaryOfBusinessResults
lxmlで読み込むと、これらが元の長ーい名前空間文字列に置き換えられます。
ですので、lxmlの .find() や .findall() でタグを取得するときは、接頭辞の部分を名前空間に置き換えて検索するとヒットします。
{http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp/2017-02-28/jpcrp_cor}NetSalesSummaryOfBusinessResults
このURLのような文字列が名前空間です。接頭辞と名前空間の対応は、XBRLファイルの先頭付近に書かれています。lxmlが自動で認識して辞書を作ってくれますので、それを利用します(.nsmapというメンバ変数に入っています)。
また、どのタグにどの名前空間を使っているかは、EDINET XBRLの仕様書に載っていますので、それをPythonモジュールに転記して使います。
以下が、そうして作った名前空間モジュールです。
EDINET関係は正規表現のマッチ関数にしています。XBRL読み込み時にこれでnsmap辞書を検索します。この名前空間は毎年改定されて日付部分が変わるので、そうしています。
「self」は提出者別タクソノミの名前空間です。EDINETコードを含んでいる文字列です。selfは自分で適当に考えてつけた名前です。簡単のために、selfだけ名前空間の接頭辞の方にマッチさせるための正規表現を書いています。
"""xbrl_namespace.py"""
import re
# 報告書インスタンス作成ガイドライン 名前空間宣言 20180228
NS_INSTANCE_20180228 = {
'ix': 'http://www.xbrl.org/2008/inlineXBRL',
'ixt': 'http://www.xbrl.org/inlieXBRL/transformation/2011-07-31',
'xbrli': 'http://www.xbrl.org/2003/instance',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xlink': 'http://www.w3.org/1999/xlink',
'link': 'http://www.xbrl.org/2003/linkbase',
'iso4217': 'http://www.xbrl.org/2003/iso4217',
'self': re.compile(
'^jp[a-z]{3}[0-9]{6}-[a-z0-9]{3}_[A-Z][0-9]{5}-[0-9]{3}$'
).match,
'jpdei_cor': re.compile(
'^http://disclosure.edinet-fsa.go.jp/taxonomy/jpdei/[1-9][0-9]{3}-[01][0-9]-[0-3][0-9]/jpdei_cor$'
).match,
'jpcrp_cor': re.compile(
'^http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp/[1-9][0-9]{3}-[01][0-9]-[0-3][0-9]/jpcrp_cor$'
).match,
'jppfs_cor': re.compile(
'^http://disclosure.edinet-fsa.go.jp/taxonomy/jppfs/[1-9][0-9]{3}-[01][0-9]-[0-3][0-9]/jppfs_cor$'
).match,
'num': 'http://www.xbrl.org/dtr/type/numericXBRL',
'nonnum': 'http://www.xbrl.org/dtr/type/non-numericXBRL',
'xbrldt': 'http://xbrl.org/2005/xbrldt',
'xbrldi': 'http://xbrl.org/2006/xbrldi',
}
これらの中から、使うものだけ選んで使っていきます。
lxmlを使う部分(エラー回復モードを含む)
lxmlの部分は別のモジュール(xbrl_util.py)に移しました。あと、解析エラーで止まるXBRLに遭遇したので、エラー時に処理を継続するモードも追加しました。
lxmlのマニュアルです。
lxml.etree.fromstring
fromstring(text, parser=None, base_url=None)
遭遇したエラーは lxml.etree.XMLSyntaxError という名前でした。シンタックスエラーっていう、つまり構文が間違ってるとか、タグが閉じてないとか、そういうたぐいのエラーですね。
あと、URLからXMLを取得したはずが、エラーページのHTMLだったとか、そういうXMLじゃないファイルを読み込んだ時もこのエラーが出ました。タクソノミを取得するときですね。なので、処理を継続するか否かの判定は、今もいろいろ試行錯誤してます。
この記事では簡単のために割愛しましたが、使い方としては recover=False でエラーに遭遇したら、recover=True で再度解析を試みる、といった感じです。
"""xbrl_util.py"""
from lxml.etree import XMLParser as etree_XMLParser
from lxml.etree import fromstring as etree_fromstring
def get_etree_obj_from_file(file, file_data=None, recover=False):
"""XMLファイルを読み込む"""
# 解析エラーを回復して続行するか否かの設定
parser = etree_XMLParser(recover=recover)
if file_data is None:
# ファイルパスから読み込む
with open(file, 'rb') as f:
return etree_fromstring(f.read(), parser=parser)
else:
# バイナリデータから読み込む
return etree_fromstring(file_data, parser=parser)
XBRLを読み込むところ
いよいよ、XBRLの読み込みです。
はじめにですが、こういうのはやり方に正解はないと思います。決算データベースを作りたいとか、決算書の体裁を再現したいとか、そういった目的によって効率的なコードというのは変わってくると思います。
以下のコードは、私がいろいろ試行錯誤して出来上がってきたものです。
大まかな流れです。クラスの初期化部分で、読み込みからデータ取得まで行っています。
- 外からファイルパス(file)を受け取る。
(URLやzipから読み込んだバイナリ(file_data)があれば、それも受け取る。) - ファイル名から文書情報を取得。
- lxmlで読み込む。
- 名前空間の辞書(nsmap)と接頭辞(ns_prefixes)の辞書を取得。
- 名前空間の定義(ns_def)を取得
- nsmapをたどって、使いたい名前空間だけを取得。
- 検索で使いたいタグ名と属性名を定義。
- xsdファイルパス取得。(ラベル取得で使います)
- コンテキストタグ取得。(日付・連結区分・セグメント情報の辞書です)
- 管理情報・財務諸表データ取得。
- lxmlのrootデータは使い終わったし、サイズも大きいので削除。
そうですね。こういう流れが、試行錯誤の中で出来上がってきました。いきなり箇条書きにできたわけではないです。
以下が、初期化部分のコードです。
"""xbrl_jpcor.py"""
from os.path import join as os_join
from os.path import basename as os_basename
from os.path import splitext as os_splitext
from os.path import dirname as os_dirname
from re import match as re_match
from collections import OrderedDict
import xbrl_namespace
from xbrl_util import get_etree_obj_from_file
class Parser:
"""xbrlファイル解析クラス"""
def __init__(self, file, file_data=None):
self.file = file
# ファイル名解析
self.info = self.parse_filename(os_basename(self.file))
# XBRLファイル読み込み
self.root = get_etree_obj_from_file(self.file, file_data)
self.nsmap = self.root.nsmap
self.ns_prefixes = {v: k for (k, v) in self.nsmap.items()}
# 名前空間(NameSpace)の定義を取得
ns_def = xbrl_namespace.NS_INSTANCE_20180228
# 名前空間 DEI語彙スキーマ (管理情報)
self.ns_dei = None
# 名前空間 企業内容等の開示に関する内閣府令 (表紙・サマリ・本文など)
self.ns_crp = None
# 名前空間 日本基準財務諸表のうち本表に係る部分 (財務諸表)
self.ns_pfs = None
# 名前空間 提出者別タクソノミ
self.ns_self = None
# 勘定科目などを定義している名前空間を取得
ns_list = []
for (ns_prefix, ns) in self.nsmap.items():
if ns_def['jpdei_cor'](ns):
ns_list.append((0, ns))
self.ns_dei = ns
elif ns_def['jpcrp_cor'](ns):
ns_list.append((1, ns))
self.ns_crp = ns
elif ns_def['jppfs_cor'](ns):
ns_list.append((2, ns))
self.ns_pfs = ns
elif ns_def['self'](ns_prefix):
ns_list.append((3, ns))
self.ns_self = ns
# 管理情報(dei)が上に来るとデバッグし易かったのでソート
ns_list.sort(key=lambda x: x[0], reverse=False)
# タグ名/属性名定義
self.link_schema_ref = '{%s}schemaRef' % ns_def['link']
self.xlink_href = '{%s}href' % ns_def['xlink']
self.xbrli_context = '{%s}context' % ns_def['xbrli']
self.xbrli_entity = '{%s}entity' % ns_def['xbrli']
self.xbrli_identifier = '{%s}identifier' % ns_def['xbrli']
self.xbrli_period = '{%s}period' % ns_def['xbrli']
self.xbrli_start_date = '{%s}startDate' % ns_def['xbrli']
self.xbrli_end_date = '{%s}endDate' % ns_def['xbrli']
self.xbrli_instant = '{%s}instant' % ns_def['xbrli']
self.xbrli_scenario = '{%s}scenario' % ns_def['xbrli']
self.xbrldi_explicit_member = '{%s}explicitMember' % ns_def['xbrldi']
self.xsi_nil = '{%s}nil' % ns_def['xsi']
# xsdファイルパス取得
self.xsd = self.get_xsd_filepath(file_data)
# コンテキストタグ(日付情報)取得
self.context_tags = self.get_context_tags()
# 管理情報・財務諸表データ取得
self.xbrl_datas = []
for (number, ns) in ns_list:
self.xbrl_datas.append((ns, self.get_xbrl_datas(ns)))
# 変数削除
del self.root
return
ファイル名から文書情報を取得
第何四半期の決算とか、提出回数とか、決算データベースを作るうえで必要な情報が取得できます。仕様に従って、スライスしていきます。とりあえず、使う・使わないは置いといて、色々な情報を取得しています。
「self.file」と書くのが冗長だったので、@staticmethod でデコって、引数で渡しています。
@staticmethod
def parse_filename(s):
"""ファイル名を解析"""
# 2018年版EDINETタクソノミの公表について
# https://www.fsa.go.jp/search/20180228.html
# 報告書インスタンス作成ガイドライン
# 4-2-4 XBRL インスタンスファイル
# jp{府令略号}{様式番号}-{報告書略号}-{報告書連番(3 桁)}_{EDINETコード又はファンドコード}-
# {追番(3 桁)}_{報告対象期間期末日|報告義務発生日}_{報告書提出回数(2 桁)}_{報告書提出日}.xbrl
# 0 1 2 3 4 5 6
# 0123456789012345678901234567890123456789012345678901234567890
# jpcrp030000-asr-001_E00000-000_2017-03-31_01_2017-06-29.xbrl
od = OrderedDict()
# 第N四半期の数字を判定
t = s[12:15]
if t == 'asr':
# 有価証券報告書
od.update({'第N期': 0})
elif re_match('^q[1-5]r$', t):
# 四半期報告書
od.update({'第N期': int(t[1])})
elif t == 'ssr':
# 半期報告書
od.update({'第N期': 2})
else:
od.update({'第N期': t})
od.update({'EDINETコード_ファンドコード':s[20:26]})
od.update({'追番': s[27:30]})
od.update({'報告対象期間期末日': s[31:41]})
od.update({'提出回数': s[42:44]})
od.update({'提出日': s[45:55]})
return od
スキーマファイル(xsd)のファイルパスを取得
スキーマファイルはXMLの機能です。XBRLは、それを利用して勘定科目とラベルなどを関連付けているかたちになります。
スキーマファイルにはラベルファイルへのリンクなどが載っていますので、ぜひ読み込んでおきたいファイルです。タグ名から日本語ラベルを付けるときに使います。
XBRLに、以下のような感じでリンクが載っています。これを拾います。
<link:schemaRef xlink:type=”simple” xlink:href=”jpcrp030000-asr-001_E00000-000_2017-03-31_01_2017-06-29.xsd“/>
リンクはxsdのファイル名だけですが、これにフォルダパスを補って絶対パスにします。zipを展開した時のように、xbrlと同じフォルダにあるのが普通ですので、xbrlファイルパスからフォルダパスを取得してくっつけます。osモジュールの出番です。
指定する提出者別タクソノミは同一フォルダ内に配置されるため、次の図表のように参照先となるファイル名のみを指定します。
(20180228 報告書インスタンス作成ガイドライン 報告書インスタンスの作成 タクソノミの参照)
zipを展開せずに直接読み込んだ場合はファイルパスがありませんので、xsdデータの入った辞書のキーを作って返すようにします。バイナリデータ(file_data)を受け取ったか否かで、そのあたりを判定してます。
私はxsdのファイル名をキーにしていますので、以下のメソッドではファイル名を返すようになっています。os_basename()は冗長かもしれませんが、一応つけてます。
def get_xsd_filepath(self, file_data):
"""提出者別タクソノミのxsdファイルパス取得"""
# xsdファイル名取得
element = self.root.find('.//%s' % self.link_schema_ref)
# 絶対パス生成
if file_data is None:
return os_join(os_dirname(self.file), element.get(self.xlink_href))
else:
return os_basename(element.get(self.xlink_href))
コンテキストタグを取得(日付情報・連結区分・セグメント)
コンテキストタグの説明は「EDINETタクソノミの設定規約書」の「コンテキストの定義」に載っていました。
損益計算書の開始日・終了日や貸借対照表の期末日など、勘定科目の日付情報を定義しているのがコンテキストタグです。このタグのid属性が、各勘定科目タグのcontextRef属性に対応しています。なので、idをキーにして日付の辞書を作成します。
日付のほかにも、勘定科目の連結区分や、どのセグメントに属しているか、といった情報が定義されています。
タグの構造上、辞書の中に辞書を入れたりと、階層が深くなっています。このあたりは、必要に応じて簡素化してもいいように思います。
あと、assert文やenumerateを使った検証用のコードが入っています。取り除くのが手間だったので、そのまま載せちゃいます。
def get_context_tags(self):
"""contextタグ取得"""
od = OrderedDict()
# contextタグ取得
for element in self.root.findall('.//%s' % self.xbrli_context):
# id属性の値を取得
key_id = element.get('id')
# idは重複しないはず
assert key_id not in od
# id属性をキーにして辞書を作成
od.update({key_id: OrderedDict()})
# entityタグ取得
entity = OrderedDict()
for (n, et_entity) in enumerate(
element.findall('.//%s' % self.xbrli_entity), start=1):
# entityは通常1つ
assert n == 1
# identifierタグ取得
entity.update(self.get_identifier_tags(et_entity))
od[key_id].update({'entity': entity})
# periodタグ取得
period = OrderedDict()
for (n, et_period) in enumerate(
element.findall('.//%s' % self.xbrli_period), start=1):
# periodは通常1つ
assert n == 1
# startDate, endDate, instantタグ取得
period.update(self.get_date_tags(et_period))
od[key_id].update({'period': period})
# scenarioタグ取得
scenario = OrderedDict()
for (n, et_scenario) in enumerate(
element.findall('.//%s' % self.xbrli_scenario), start=1):
# scenarioは通常1つ
assert n == 1
# explicitMemberタグ取得
scenario.update(self.get_explicit_member_tags(et_scenario))
od[key_id].update({'scenario': scenario})
return od
identifierタグ取得
identifierタグは一応取得していますが、まだ詳しく調べていません。
少し調べた感じでは、
「EDINETタクソノミの設定規約書」の「entity」、
「報告書インスタンス作成ガイドライン」の「シナリオ要素の設定」、
「提出者別タクソノミ作成ガイドライン」の「ディメンションで定義される構造のイメージ」といった部分が参考になりそうです。
def get_identifier_tags(self, element):
"""identifierタグ取得"""
od = OrderedDict()
for (n, et_identifier) in enumerate(
element.findall('.//%s' % self.xbrli_identifier), start=1):
# identifierは通常1つ
assert n == 1
for (name, value) in et_identifier.items():
od.update({name: value})
od.update({'text': et_identifier.text})
return od
startDate, endDate, instantタグ取得
開始日、終了日、期末日を取得する部分です。
def get_date_tags(self, element):
"""日付タグ取得"""
datas = OrderedDict()
et_start_date = element.find('.//%s' % self.xbrli_start_date)
if et_start_date is not None:
# 開始日を追加
od = OrderedDict()
for (name, value) in et_start_date.items():
od.update({name: value})
od.update({'text': et_start_date.text})
datas.update({'start_date': od})
# 終了日を追加
et_end_date = element.find('.//%s' % self.xbrli_end_date)
od = OrderedDict()
for (name, value) in et_end_date.items():
od.update({name: value})
od.update({'text': et_end_date.text})
datas.update({'end_date': od})
else:
# 期末日を追加
et_instant = element.find('.//%s' % self.xbrli_instant)
od = OrderedDict()
for (name, value) in et_instant.items():
od.update({name: value})
od.update({'text': et_instant.text})
datas.update({'instant': od})
return datas
ところで、私は「きまつび(期末日)」って書いてますが、「きじゅんび(基準日)」って書いたりもしてました。もともとの「インスタント(instant)」ですが、開始日、終了日と来て、「時点」と呼ぶのもなんだか馴染みがない感じがして、ちょっと迷いながら書いてます。
explicitMemberタグ取得
エクスプリシット・メンバータグには、勘定科目の「連結区分」や「セグメント」に関する情報が入っています。
連結と個別(非連結)の判別方法は、2018年2月28日公表の「報告書インスタンス作成ガイドライン ⇒ コンテキストの定義 ⇒ シナリオ要素の設定 ⇒ 連結又は個別を表すシナリオ要素の設定」に詳しく載っていました。
連結区分のほかにも、「セグメント情報(セグメント別の売上高など)」や「大株主の状況」といった表のどれに属しているか、といった情報が取得できます。
これらの情報は、勘定科目を集約する時に活躍します。
ひと口に売上高といっても、連結財務諸表の売上高なのか、個別財務諸表の売上高なのか、はたまたセグメント情報のところに載っている事業別の売上高なのか、色々あって分かりません。
そんな時に、ここの情報で区別します。
def get_explicit_member_tags(self, element):
"""explicitMemberタグ取得"""
od = OrderedDict()
for et_explicit_member in element.findall('.//%s' % self.xbrldi_explicit_member):
key = et_explicit_member.get('dimension')
assert key not in od
od.update({key: et_explicit_member.attrib})
od[key].update({'text': et_explicit_member.text})
return od
unitタグ取得
ユニットタグには、数値の「単位」が入っています。
- 日本円ならJPY(ジェイピーワイ、ジャパニーズエン)。
- 株式数ならshares(シェアース)。
- 割合(%)、整数、小数、人数ならpure(ピュア)。
単位の種類は「EDINETタクソノミの設定規約書」に載っています。
ユニットタグの取得コードは、「EDGAR XBRLを読み込むコード例」の「unit 取得」に書きました。
勘定科目を取得する部分
社名やEDINETコードなどの管理情報や、財務諸表の勘定科目などを取得する部分です。
カバーページ(PDFの表紙の内容)とテキストブロック(本文のHTML)はサイズが大きいので、軽くしたいときはスキップしてます。これらのタグは、主に接頭辞 jpcrp_cor の名前空間にありましたので、それも見てスキップしています。
また、正規表現よりも「in」で判定したほうが5倍くらい速かったので、inで済むところはinで判定するようにしました。timeitモジュール、便利です。
def get_xbrl_datas(self, namespace):
"""データ取得"""
datas = OrderedDict()
for element in self.root.findall('.//{%s}*' % namespace):
# 本文のテキストブロックは不要なのでスキップ
# if self.ns_crp:
# if self.ns_crp in element.tag:
# if 'CoverPage' in element.tag:
# # print('skipped %s %s' % (
# # element.tag.split('}')[1], element.text[:20]))
# continue
# elif 'TextBlock' in element.tag:
# # print('skipped %s %s' % (
# # element.tag.split('}')[1], element.text[:20]))
# continue
# tag名、contextRef、idのタプルをキーにして辞書を作成
key = (element.tag, element.get('contextRef'), element.get('id'))
data = OrderedDict()
# 属性の内容を追加
data.update(element.attrib)
# テキストも追加
data.update({'text': element.text})
if key in datas:
if data == datas[key]:
# キーも値も同じなのでスキップ
continue
else:
print('キー重複 %s' % str(key))
# リストに追加
datas.update({key: data})
return datas
実際にXBRLを読み込んでExcelに保存する
XBRLから読み取ったコンテキストと勘定科目を組み合わせながら、リストを作り、データフレームに変換して保存する部分です。
このあたりは、欲しい情報を選んでリストにしていくだけです。目的によって、大きく変わる部分ですね。
以下は決算データベースを意識したリストですが、テキストから「増収・増益」とか「疑義」とか、そういった表現を検出してまとめる、といったこともやっていました。
自分の興味・研究に応じて作っていくところだと思います。
"""xbrl_jpcor.py"""
def main():
"""モジュールテスト"""
xbrl_file = r"*****\jpcrp030000-asr-001_E00000-000_2017-03-31_01_2017-06-29.xbrl"
print('XBRL解析中...\n%s' % xbrl_file)
xbrl = Parser(xbrl_file, None)
# XBRL -> リスト -> データフレーム -> xlsx保存
print('XBRL -> リスト 変換中...')
# 保存ファイル名
data_dir = r'(xlsxファイルを保存するフォルダパス)'
xlsx_file = os_join(
data_dir,
'%s.xlsx' % os_splitext(os_basename(xbrl_file))[0],
)
# リスト変換で使う自作モジュール
from xbrl_util import conv_str_to_num
# データフレーム変換で使うモジュール
from pandas import DataFrame as pd_DataFrame
from pandas import to_datetime as pd_to_datetime
# 列ラベルを定義
labels = [
'提出日', '第N期', '名前空間接頭辞', 'tag', 'id',
'context', '開始日', '終了日', '期末日', '連結', '値',
]
datas = []
# ファイル名から取得した文書情報をリストにしておく
xbrl_info = [xbrl.info['提出日'], xbrl.info['第N期']]
# コンテキストタグのperiod辞書から日付を取得する関数
def get_dates(period):
"""period辞書から日付を取得"""
if 'start_date' in period:
return [period['start_date']['text'], period['end_date']['text'], None]
else:
return [None, None, period['instant']['text']]
# コンテキストタグのscenario辞書から連結の真偽値を取得する関数
CONSOLIDATED_OR_NONCONSOLIDATED_AXIS = 'jppfs_cor:ConsolidatedOrNonConsolidatedAxis'
NON_CONSOLIDATED_MEMBER = 'jppfs_cor:NonConsolidatedMember'
def get_consolidated_or_nonconsolidated(x):
"""連結の真偽値を取得"""
if 'scenario' in x:
if CONSOLIDATED_OR_NONCONSOLIDATED_AXIS in x['scenario']:
if x['scenario'][CONSOLIDATED_OR_NONCONSOLIDATED_AXIS]['text'] == NON_CONSOLIDATED_MEMBER:
return False
return True
# 管理情報
# 表紙・サマリ・本文など
# 財務諸表
# 提出者別タクソノミ
for (ns, xbrl_data) in xbrl.xbrl_datas:
# キーのタプル(タグ名・コンテキスト・ID)
# 値の辞書(属性・テキスト)
for ((k_tag, k_context_ref, k_id), v) in xbrl_data.items():
# タグ名から名前空間を分離 & 接頭辞に変換
ns_name = k_tag.rsplit('}', maxsplit=1)
if len(ns_name) == 2:
tag_ns_prefix = xbrl.ns_prefixes[ns_name[0].lstrip('{')]
tag_name = ns_name[1]
else:
tag_ns_prefix = None
tag_name = ns_name[0]
# 非表示のタグをスキップ
if xbrl.xsi_nil in v:
if v[xbrl.xsi_nil].lower() == 'true':
print('skipped nil %s:%s' % (str(tag_ns_prefix), tag_name))
continue
# リストに追加
datas.append(
# 文書情報
xbrl_info +
# 名前空間接頭辞 タグ名 id属性 コンテキスト
[tag_ns_prefix, tag_name, k_id, k_context_ref] +
# 開始日 終了日 期末日
get_dates(xbrl.context_tags[k_context_ref]['period']) +
# 連結区分 型変換した値
[
get_consolidated_or_nonconsolidated(xbrl.context_tags[k_context_ref]),
conv_str_to_num(v['text']),
]
)
print('リスト -> データフレーム 変換中...')
df = pd_DataFrame(datas, columns=labels)
# 日時型に変換
df['提出日'] = pd_to_datetime(df['提出日'])
df['開始日'] = pd_to_datetime(df['開始日'])
df['終了日'] = pd_to_datetime(df['終了日'])
df['期末日'] = pd_to_datetime(df['期末日'])
print('xlsx 保存中...\n%s' % xlsx_file)
df.to_excel(xlsx_file)
print('終了')
return
数値型に変換する関数を作る
Excelで開く場合は文字列のままでも自動で認識してくれますが、Python上で分析を行う場合は数値型に変換しておく必要があります。
以下がその変換関数です。他にもいろいろなやり方があると思います。
"""xbrl_util.py"""
import re
from dateutil.parser import parse as dateutil_parser_parse
RE_INT_MATCH = re.compile('^[+-]?[0-9]+[.]?$').match
RE_INT_PERIOD_END_SUB = re.compile('\.$').sub
RE_FLOAT_MATCH = re.compile('^[+-]?(?:[0-9]+\.[0-9]*|[0-9]*\.[0-9]+)$').match
def conv_str_to_num(s):
"""文字列を数値型等に変換"""
# None
if s is None:
return s
# 数値型
a = s.strip().replace(',', '')
if RE_INT_MATCH(a):
return int(RE_INT_PERIOD_END_SUB('', a))
elif RE_FLOAT_MATCH(a):
return float(a)
# bool型
b = s.lower()
if b == 'true':
return True
elif b == 'false':
return False
# 日付型
try:
t = dateutil_parser_parse(s)
except ValueError:
pass
else:
return t
# その他
return s
補足です。整数なのか小数なのかは、正規表現で判定しています。XBRLの金額部分に関しては、今のところうまく判定してくれています。int()やfloat()で例外をキャッチして判定していくのもアリだと思います。
日付型に関してですが、タイムゾーン情報があると、データフレームのExcel出力でエラーになります。その内容ですが、Excelではタイムゾーン情報が扱えないので、あらかじめ削除しておいてください、という旨のエラーです。
TypeError: Excel doesn’t support timezones in datetimes.
Set the tzinfo in the datetime/time object to None or use the ‘remove_timezone’ Workbook() option※ pandas.DataFrame.to_excel() で発生したエラー。
タイムゾーンの削除方法です。
以下のように .replace()メソッドの tzinfo に None を渡すだけです。
なお、タイムゾーン付きのdatetime型を作るには、デートユーティル(dateutil)をインポートして使うのが簡単です。dateutil は事前にpipでインストールしておきます。
import dateutil.parser
t = dateutil.parser.parse('2018-04-06T00:00:00+09:00') # datetime型に変換
t = t.replace(tzinfo=None) # タイムゾーンの削除
以下のように、タイムゾーンが削除されます。
datetime.datetime(2018, 4, 6, 0, 0, tzinfo=tzoffset(None, 32400))
datetime.datetime(2018, 4, 6, 0, 0)
データフレームからタイムゾーンを削除する方法です。
t = dateutil.parser.parse(‘2018-04-06T00:00:00+09:00’)
df = pd.DataFrame([{‘日時’: t}])
df[‘日時’] = df[‘日時’].apply(lambda x: x.tz_localize(None))
以下のように削除されます。
0 2018-04-06 00:00:00+09:00
Name: 日時, dtype: datetime64[ns, tzoffset(None, 32400)]
0 2018-04-06
Name: 日時, dtype: datetime64[ns]
あとは、エラー文の最後にあった、保存時に remove_timezone オプションを使う方法です。以下のように、pandas.ExcelWriter()に「保存ファイルパス」、「engine」、「options」を指定して、それをto_excel()に渡します。
with pd.ExcelWriter('*****.xlsx', engine='xlsxwriter',
options={'remove_timezone': True}) as w:
df.to_excel(w)
XlsxWriter のマニュアルの場所です。
XlsxWriter -> Working with Dates and Time -> Timezone Handling
全上場企業について実行する
今回作ったプログラムを実行すると、決算ごとのデータフレームができますので、企業ごとにデータフレームを連結して保存すれば、その企業の時系列グラフを作ったりできます。機械学習でいろいろモデルを作ってみるのも良さそうですね。
言葉にすると簡単なのですが、実際には旧EDINETのXBRL対応であったり、IFRS対応であったり、データをキレイに揃えていくための作業がたくさん出てきます。あと、PDFのような日本語ラベルがあったほうが見やすいですし、勘定科目の特定にも役立ちそうです。
そういえば、IFRSはまさかのURL変更で、スキーマファイルからリンクされている古いタクソノミの一部が取得できなくなっています。ちょっと探した感じ、ラベルのzipは引き続き公開されているのを見つけましたが、それ以外はよくわかりません。ラベルはぜひつけたいので、そのあたりもどうにかしたいなとは思います。
いろいろ思いつきますが、一度にすべてを把握して作ることは不可能ですので、必要なところを、少しずつ、小さく小さく作って付け足していく感じで、作っていってます。
米国のEDGAR XBRLも基本は同じですので、名前空間の定義を変えるなどして調整すれば、日付と勘定科目の金額は取得できます。