skip to content
barorin&

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 zipfile
from 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つ目だけは、機械的な処理ではなく会計側の知識が必要になります。逆に言えば、そこさえ設計できればあとは自動化できます。

この記事を書いた人

barorinのプロフィール画像

barorinCPA & Engineer

会計とITの二足のわらじで働く生き物。Pythonを中心に、会計・監査の実務で使えるコードや、Ubuntu・Docker等の設定メモなどを書いています。