EDGAR XBRLをリストやデータフレームに変換するコード例【Python】

スポンサーリンク

EDGAR XBRLを読み込むコード例で読み込んだデータを、Python のリストや pandas のデータフレームに変換します。そのコード例です。

データフレームへの変換ですが、EDGAR XBRL では西暦3000年といった、pandas のタイムスタンプで扱える範囲を超えた日付が登場します。

そのようなデータは、自動変換に任せるとエラーになってしまうので、いったん文字列のデータフレームにしてから Python の datetime 型に変換します。

データをリストに変換

コンテキストやユニットの辞書を参照しながら、勘定科目データを Python リストに変換する関数です。

リストには、決算年度や第N四半期といった情報を、情報列として追加しています。これらは DEI から取得します。

のちのちデータベース上で勘定科目を抽出する時に役立ちます。追加する情報は _INFO_TAGS のリストで定義しています。

"""xbrl_proc"""

from collections import OrderedDict
from sys import exc_info as sys_exc_info
from traceback import format_exc
from traceback import format_exception_only
from xbrl_edgar import Parser as xbrl_edgar_Parser


# 情報列のタグ
_INFO_TAGS = [
    'EntityCentralIndexKey',
    'TradingSymbol',
    'DocumentPeriodEndDate',
    'DocumentFiscalYearFocus',
    'DocumentFiscalPeriodFocus',
    'AmendmentFlag',
    ]

# 情報列のラベル
_INFO_LABELS = [
    'cik',
    'symbol',
    'period_end',
    'fiscal_year',
    'fiscal_period',
    'amendment',
    ]

# 勘定科目データ列のラベル
_DATA_LABELS = [
    'prefix', 'tag', 'id',
    'contextRef', 'unitRef',
    'identifier',
    'start', 'end', 'instant', 'forever',
    'segment', 'unit', 'value',
    ]

# 連結したラベル
LABELS = _INFO_LABELS + _DATA_LABELS

def get_xbrl_datas(
        xbrl_file=None,
        xbrl_file_data=None,
        skip_textblock=False,
        skip_text_len=None,
        strip_space=True,
        newline='\n',
        ):
    """データ取得"""

    # xbrlファイル読み込み
    xbrl = xbrl_edgar_Parser(
        file=xbrl_file,
        file_data=xbrl_file_data,
        skip_textblock=skip_textblock,
        skip_text_len=skip_text_len,
        strip_space=strip_space,
        newline=newline,
        )

    context_tags = xbrl.context_tags
    unit_tags = xbrl.unit_tags

    # 勘定科目抽出に有用なものを取得
    info_datas = get_info_datas(xbrl.xbrl_datas)

    datas = []
    datas_append = datas.append

    # コンテキストと単位の内容を展開してリストに変換
    for ((t_tag, t_context_ref, t_id, t_unit_ref), v) in xbrl.xbrl_datas.items():
        # キーのタプル(タグ名・コンテキスト・ID)
        # 値の辞書(属性・テキスト)

        # タグ名から名前空間を分離
        (t_ns, t_tag_name) = t_tag.rsplit('}', maxsplit=1)

        try:
            datas_append(
                # 勘定科目抽出を補助する情報列
                info_datas +

                # prefix tag id contextRef unitRef identifier
                [
                    # 名前空間接頭辞に変換
                    xbrl.ns_prefixes[t_ns.lstrip('{')],

                    t_tag_name,
                    t_id,
                    t_context_ref,
                    t_unit_ref,
                    get_identifier(context_tags[t_context_ref]['entity']),
                ] +

                # start end instant forever
                get_dates(context_tags[t_context_ref]['period']) +

                # segment unit value
                [
                    get_segment(context_tags[t_context_ref]),
                    get_unit(unit_tags, t_unit_ref),
                    v['text'],
                ]
            )
        except:
            print(format_exc())
    return (LABELS, datas)

DEI から文書情報を取得

DEI から勘定科目抽出に有用なものを取得する関数です。

def get_info_datas(xbrl_datas):
    """勘定科目抽出に有用なものを取得"""
    # タグ名の辞書を作成
    tags = {}

    for (key, value) in xbrl_datas.items():
        # key: [0]tag [1]context_ref [2]id [3]unit_ref

        # tagを[0]名前空間接頭辞と[1]タグ名に分割
        temp = key[0].rsplit('}', maxsplit=1)

        # [1]タグ名をキーに追加
        tags[ temp[1]] = value['text']

    del (key, value, temp)

    # 勘定科目抽出に有用なものを取得
    info_datas = []
    for key in _INFO_TAGS:
        if key in tags:
            info_datas.append(tags[key])
        else:
            info_datas.append(None)
    return info_datas

情報列では TradingSymbol という、いわゆるティッカー・シンボルの入ったタグも一応取得しています。ですが、定義されていない XBRL が多かったので、あまりアテにならないかもしれません。

また、ティッカー・シンボルは日本の証券コードちがって、使われなくなった記号がのちのち別の企業で再利用されたりしています。

コンテキスト辞書とユニット辞書

受け取った辞書を読み込む関数群です。辞書の内容を、2次元の表に適した形へと加工しています。

def get_identifier(x):
    """識別子取得"""
    if x is None:
        return None
    return '%s#%s' % (x['scheme'], x['text'])


def get_dates(x):
    """日付取得"""
    if 'start_date' in x:
        return [x['start_date'], x['end_date'], None, None]
    elif 'instant' in x:
        return [None, None, x['instant'], None]
    elif 'forever' in x:
        return [None, None, None, True]
    else:
        raise KeyError('未対応の日付キー %s' % str(x))


def get_segment(x):
    """セグメント情報取得"""
    if 'segment' in x:
        if x['segment']:
            # 複数のメンバーがあるときは縦棒で連結
            return '|'.join(
                # コロンでdimension(軸要素)とtext(メンバー要素)を連結
                '%s:%s' % (
                    # 名前空間接頭辞でないほうを取得
                    v['dimension'].split(':')[1],
                    v['text'].split(':')[1],
                    ) for v in x['segment'].values())
    return None


def get_unit(unit_tags, unit_ref):
    """単位取得"""
    if unit_ref is None:
        return None

    if unit_ref in unit_tags:
        return unit_tags[unit_ref]
    else:
        return None

識別子(identifier)をシャープ記号「#」で連結しているのは、独自に考えたものです。特にそういった決まりがあるわけではないです。連結後のURLが指している場所にも意味はありません。単なる文字列です。

セグメントの連結方法についても同様です。複雑な内容を表に収めようとして、独自に考えたものです。とりあえず連結しておけば、データベース上で抽出できますので。

複雑なデータをどうやって表にするかは、いろいろなやり方があると思います。

データフレームに変換

リストを pandas データフレームに変換する関数です。広い日付範囲などに対応するために、いったん文字列のデータフレームを生成して、そこから個別に型変換をしています。

from pandas import DataFrame as pd_DataFrame
from xbrl_util import conv_str_to_num
from xbrl_util import conv_str_to_datetime
from xbrl_util import conv_str_to_bool
from xbrl_util import conv_str_to_int


def conv_list_to_df(labels, datas):
    """リストをデータフレームに変換"""

    # 文字列型(str)でデータフレームを作成
    a = pd_DataFrame(
        datas,
        columns=labels,
        dtype='str',
        )

    # 型変換
    # a['cik'] = a['cik']
    # a['symbol'] = a['symbol']
    a['period_end'] = a['period_end'].apply(conv_str_to_datetime)
    a['fiscal_year'] = a['fiscal_year'].apply(conv_str_to_int)
    # a['fiscal_period'] = a['fiscal_period']
    a['amendment'] = a['amendment'].apply(conv_str_to_bool)
    # a['prefix'] = a['prefix']
    # a['tag'] = a['tag']
    # a['id'] = a['id']
    # a['contextRef'] = a['contextRef']
    # a['unitRef'] = a['unitRef']
    # a['identifier'] = a['identifier']
    a['start'] = a['start'].apply(conv_str_to_datetime)
    a['end'] = a['end'].apply(conv_str_to_datetime)
    a['instant'] = a['instant'].apply(conv_str_to_datetime)
    a['forever'] = a['forever'].apply(conv_str_to_bool)
    # a['segment'] = a['segment']
    # a['unit'] = a['unit']
    a['value'] = a['value'].apply(conv_str_to_num)
    return a

変換しない列はコメントアウトしています。

個別に Python 関数を適用するぶん、データフレームの速さはありません。その代わり、Python の datetime型を保持できるので、広い日付範囲が扱えるようになっています。速くないとはいっても、十分実用的な速さで動きました。

型変換

文字列型から数値型などに変換する関数群です。日付や金額が混在している列用と、そうでない列用の関数を作りました。

"""(xbrl_util に追記)"""

from re import compile as re_compile
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):
    """文字列を数値型等に変換"""
    if isinstance(s, str):
        # 数値型
        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
    return s


def conv_str_to_datetime(s):
    """文字列をdatetimeに変換"""
    if isinstance(s, str):
        try:
            return dateutil_parser_parse(s)
        except ValueError:
            pass
    return s


def conv_str_to_bool(s):
    """文字列をboolに変換"""
    if isinstance(s, str):
        t = s.lower()
        if t == 'true':
            return True
        elif t == 'false':
            return False
    return s


def conv_str_to_int(s):
    """文字列をintに変換"""
    if isinstance(s, str):
        try:
            return int(s.replace(',', '').replace('.', ''))
        except ValueError:
            pass
    return s

変換方法はいろいろ考えられると思います。これが定石というわけではないのですが、うまく動いてくれています。

さて、企業ごとにすべての決算を連結してデータフレームにできれば、あとは欲しい勘定科目を抽出することで時系列分析が可能になります。日付情報もありますので、株価と重ねて関連性を見ることもできます。

日本のEDINET XBRLと米国のEDGAR XBRLですが、XBRLの基本的な構造は同じでした。プログラミングの参考になれば幸いです。

タイトルとURLをコピーしました