EDINETのCSVから財務数値を取り出す|XBRLをPythonで扱う
/ 11 min read
Table of Contents
はじめに
EDINET APIをPythonで使うで取得したファイルから、実際に財務数値を取り出します。
XBRLと聞くとタクソノミの解析から始めたくなりますが、EDINETは同じ内容をCSVに変換したものを配ってくれています。財務数値を集計したいだけなら、そちらを使うほうが圧倒的に速いです。
この記事ではCSV(書類取得APIの type=5)を前提に進めます。
生のXBRLとCSV、どちらを使うか
type=1(XBRL) |
type=5(CSV) |
|
|---|---|---|
| 中身 | インラインXBRL+タクソノミ | XBRLをCSVに変換したもの |
| 解析の手間 | 名前空間・コンテキストの解決が必要 | 列を読むだけ |
| 向いている用途 | 注記や独自タグまで拾いたい | 財務数値の集計 |
まずCSVで足りるかを確認し、足りないときだけXBRLに降りる、という順番がおすすめです。この記事はCSVを扱います。
CSVの読み込み:文字コードと区切り文字に注意
ZIPを開くと、jpcrp(提出本文書)や jpaud(監査報告書)を含む名前のファイルが入っています。
ここが最初の関門です。拡張子は .csv ですが、中身はUTF-16LEのタブ区切りです。何も指定せずに read_csv すると文字化けするか、そもそも読めません。
import zipfilefrom pathlib import Path
import pandas as pd
def read_edinet_csv(zip_path: Path) -> pd.DataFrame: """EDINETのCSV(ZIP)から提出本文書のデータを読む""" with zipfile.ZipFile(zip_path) as zf: targets = [n for n in zf.namelist() if "jpcrp" in n and n.endswith(".csv")] if not targets: raise ValueError(f"対象CSVが見つかりません: {zip_path}")
with zf.open(targets[0]) as f: # UTF-16LE / タブ区切り return pd.read_csv(f, encoding="utf-16le", sep="\t")
df = read_edinet_csv(Path("edinet/S1000001.zip"))print(df.columns.tolist())print(len(df), "行")CSVの構造
1行が1つのタグ(勘定科目など)に対応していて、だいたいこういう列が並んでいます。
| 列 | 内容 |
|---|---|
| 要素ID | XBRLの要素名(例:jppfs_cor:NetSales) |
| 項目名 | 日本語の項目名(例:売上高) |
| コンテキストID | どの期間・どの範囲の数値かを表す識別子 |
| 相対年度 | 当期/前期など |
| 連結・個別 | 連結か個別か |
| 期間・時点 | ストック(時点)かフロー(期間)か |
| ユニットID | 単位の識別子 |
| 単位 | JPY、株 など |
| 値 | 数値または文字列 |
同じ「売上高」が何行も出てきます。 連結と個別、当期と前期がすべて別の行として入っているためです。ここを区別せずに拾うと、前期の数字を当期として集計してしまいます。
欲しい数値を取り出す
「連結・当期」の数値だけを抜き出します。
COL_ITEM = "項目名"COL_VALUE = "値"COL_CONSOLIDATED = "連結・個別"COL_PERIOD = "相対年度"
def pick(df: pd.DataFrame, item: str) -> str | None: """連結・当期の値を1つ取り出す""" hit = df[ (df[COL_ITEM] == item) & (df[COL_CONSOLIDATED] == "連結") & (df[COL_PERIOD] == "当期") ] if hit.empty: return None return hit.iloc[0][COL_VALUE]
for item in ["売上高", "営業利益", "経常利益", "当期純利益"]: print(item, pick(df, item))値は文字列として入ってくるので、数値として使うときは変換します。取得できない項目には - などが入ることがあるため、変換失敗を欠損値に倒すのが安全です。
def to_number(value) -> float | None: """文字列の値を数値に変換する。変換できなければ None""" return pd.to_numeric(value, errors="coerce")errors="coerce" を付けると、変換できない値は例外ではなく NaN になります。1社でも変な値が入っていると全体が止まる、という事態を避けられます。
会社ごとに違う科目名をどう吸収するか
ここが実務で効いてくるところです。
同じ利益概念でも、会社によって項目名が違います。 「売上高」「営業収益」「売上収益」はいずれも同じ位置の数字ですが、業種や適用基準(日本基準/IFRS)によって使われる名前が変わります。
項目名で1つずつ照合すると取りこぼすので、候補を並べて最初に見つかったものを採用します。
ALIASES = { "売上高": ["売上高", "営業収益", "売上収益"], "営業利益": ["営業利益", "営業利益又は営業損失(△)"], "経常利益": ["経常利益", "経常利益又は経常損失(△)"], "当期純利益": [ "親会社株主に帰属する当期純利益", "当期純利益", "親会社の所有者に帰属する当期利益", ],}
def pick_any(df: pd.DataFrame, candidates: list[str]) -> float | None: """候補の項目名を順に試して、最初に見つかった値を返す""" for name in candidates: value = pick(df, name) if value is not None: return to_number(value) return Noneより厳密にやるなら、項目名ではなく要素ID(jppfs_cor:NetSales など)で照合します。要素IDはタクソノミで定義されているので表記ゆれがありません。日本語の項目名は読みやすい反面ゆれる、というトレードオフがあります。
複数社を1つの表にまとめる
ここまでを組み合わせて、ZIPの束から比較表を作ります。
def extract_summary(zip_path: Path, meta: dict) -> dict: """1社分のZIPから主要数値を取り出す""" df = read_edinet_csv(zip_path) row = { "docID": meta["docID"], "証券コード": meta["secCode"], "会社名": meta["filerName"], "決算期": meta["periodEnd"], } for label, candidates in ALIASES.items(): row[label] = pick_any(df, candidates) return row
rows = []for meta in docs: # 書類一覧APIで絞り込んだリスト zip_path = Path("edinet") / f"{meta['docID']}.zip" if not zip_path.exists(): continue try: rows.append(extract_summary(zip_path, meta)) except Exception as e: # 1社の失敗で全体を止めない print("skip:", meta["docID"], e)
summary = pd.DataFrame(rows)summary.to_csv("summary.csv", index=False, encoding="utf-8-sig")書き出しの encoding="utf-8-sig" は、Excelで開いたときに文字化けさせないためのBOM付きUTF-8です。ここをただの utf-8 にすると、Excelでは日本語が崩れます。
これで、こういう表ができます。
| 証券コード | 会社名 | 決算期 | 売上高 | 営業利益 | 経常利益 | 当期純利益 |
|---|---|---|---|---|---|---|
| 10000 | エディネット株式会社 | 2023-03-31 | 1234567 | 234567 | 245678 | 156789 |
あとは summary を使って、利益率を並べるなり、前期と比較するなり、通常のpandasの作業になります。
注意しておきたいこと
- 単位を確認する。 「値」は原則として円単位ですが、百万円単位で記載される項目もあります。
単位列とあわせて確認してください - 連結を出していない会社がある。 連結財務諸表を作成していない会社では「連結」の行が存在しません。
pickがNoneを返したら個別にフォールバックする、といった処理が要ります - 会計基準の違いを意識する。 日本基準とIFRSでは、そもそも「経常利益」という概念がありません。横並びで比較する場合は基準を揃えるか、比較可能な指標に限定してください
3つ目は、集計そのものは動いてしまうぶん、後から気づきにくいところです。IFRS適用会社の「経常利益」が一律で空欄になっていたら、バグではなく仕様と考えてよいです。
おわりに
XBRLを直接パースしなくても、EDINETのCSVを使えば財務数値の集計はpandasの作業に落とせます。
つまずくのは次の3点でした。
- CSVはUTF-16LEのタブ区切り
- 同じ科目が連結/個別・当期/前期で複数行ある
- 科目名は会社や会計基準によって違う
このうち3つ目だけは、機械的な処理ではなく会計側の知識が必要になります。逆に言えば、そこさえ設計できればあとは自動化できます。
