[go: up one dir, main page]
More Web Proxy on the site http://driver.im/

99から始める有価証券報告書XBRLデータ分析

はじめに

この記事は会計系 Advent Calendar 2024 18日目の記事として投稿させていただいています。

Pythonで会計データの分析をやってみたいけど簡単に使えるデータがない」そんなニーズにお応えして、オープンデータの有価証券報告書の提出データを分析してみます。 しかし、このデータはXBRLと呼ばれる構造化ドキュメントです。XBRLは、その構造が大変複雑で扱いづらい点と、データが散らばっている点で分析が困難です。

そこで、XBRLの複雑な部分をまるっと隠して必要な情報だけ抽出するラッパーをつくりました。 とりあえず、pythonライブラリのpandasで扱えるテーブルにしてしまえば、一般的なテーブルデータと同様に扱えるでしょう。データモデリングでいうところの大福帳(One Big Table)をつくってしまおうという訳です。 「データサイエンスの9割は前処理」と言われますが、XBRLについては99%が前処理です。皆様は混沌としたXBRLの前処理から解放されて、会計データの深い分析にリソースを充てることができます。

ちなみに99の内訳ですが、EDINET APIからのダウンロードが5、XBRLインスタンスのパースが85(Arreleにおまかせ)、スキーマファイルとリンクベースファイルからの情報抽出が9くらいです。私の貢献は10くらいでしょう笑

ラッパーのソースコードGitHubで公開しています。

github.com

また、このブログで実演する分析のノートブックはgoogle colaboratoryでも提供しています。

colab.research.google.com

google colaboratoryは環境構築不要でブラウザさえあれば簡単にpython等を用いた分析を試すことができます。しかし、勝手にライブラリのバージョンが上がっていく困り者で、しばらくはメンテナンスしようと思います。 最終動作確認: 2024/12/19

このブログ本文ではコードを抜粋して説明するので、完全なコードはGitHubリポジトリのノートブックまたはGoogle colaboratoryのものを参照してください。

0. 準備

0.1. 環境構築

具体的な環境構築方法は時代によって大きく変わるため、省かせていただきますが、筆者の環境作成手順を[注1]に記載しております。環境を構築できる方は[注1]を参考に、仮想環境を作成してください。

環境構築をしたことがない方はgoogle colaboratoryでやりましょう。

0.2. EDINET APIキーの発行

EDINET APIの仕様書を参考にAPIキーを発行し、どこかにメモしておいてください。仕様書は以下の「EDINET API関連資料」の「EDINET API仕様書(Version 2)」からダウンロードできます。

disclosure2dl.edinet-fsa.go.jp

0.3. Groq APIキーの発行

※X(旧twitter)のGrokではありません。Groqは高速で生成AIの推論APIを提供してくれるサービスで、2024/12/19時点でフリープランがあります。Googleアカウント等でアカウントを作成し、APIキーを発行してください。

console.groq.com

日本語でAPIキー発行までの手順が記載されているブログがあり参考になると思います。

zenn.dev

0.4. APIキーの設定

以下のコードでAPIキーを変数に代入できます。

input("Please input your API key: ")

Google Colaboratoryでは以下の画像のようにシークレットキーに設定してください。

Google colaboratoryでのシークレットキーの設定

1. EDINET APIから書類一覧を取得し、分析対象を絞る

まずはEDINETからデータをダウンロードする必要がありますが、全てのデータをダウンロードするには1週間くらいかかるため、ここでは分析対象を絞ります。 まずは、EDINET APIでダウンロード可能な書類一覧を取得します。 ダウンロード可能な書類一覧を取得する範囲は提出日でしか絞れないので、まずはざっくり有報の提出が集中する2024/6/15〜2024/6/30の期間で取得します。

# 書類一覧の取得
res_results:EdinetResponseList = request_term(api_key=your_api_key, start_date_str='2024-06-15', end_date_str='2024-06-30')

json Lines形式のレスポンスを格納するクラスを作っているため、これにセットしてjson Lines形式で保存します。 tse_sector_urlは業種情報が入っている東証上場銘柄一覧のダウンロードリンクです。毎月更新されるため、tse_sector_urlへ代入するリンクは適宜書き換えてください。ちなみに、EDINETでもこちらで業種情報を提供しています。

edinet_response_metadata_obj = edinet_response_metadata(
    tse_sector_url = "https://www.jpx.co.jp/markets/statistics-equities/misc/tvdivq0000001vg2-att/data_j.xls",
    tmp_path_str = str(DATA_PATH)
)
edinet_response_metadata_obj.set_data(res_results)
filename = str(DATA_PATH / "data.jsonl")
edinet_response_metadata_obj.save(filename)

インスタンス化の際にfilenameを指定することで保存したjson Linesを読み込めます。

edinet_response_metadata_obj = edinet_response_metadata(
    filename = str(DATA_PATH / "data.jsonl"),
    tse_sector_url = "https://www.jpx.co.jp/markets/statistics-equities/misc/tvdivq0000001vg2-att/data_j.xls",
    tmp_path_str = str(DATA_PATH)
    )

edinet_response_metadataインスタンスの.get_yuho_df()メソッドで提出書類一覧の中から有価証券報告書に絞ったリストをデータフレームで出力します(有価証券報告書は書類種別コード='120'かつ政令コード='010'かつ様式コード='030000'で抽出しています。)

全部分析すると時間がかかるため、ここでは業種を絞ります。業種情報はカラム「sector_label_33」に入っています。 リスポンスのうち提出書類が有価証券報告書であるものをpandas.DataFrameで出力し、業種を食料品に絞ります。

yuho_df = edinet_response_metadata_obj.get_yuho_df()
yuho_df_filtered:EdinetResponseDf = yuho_df.query("sector_label_33 == '食料品'")
print("業種が食料品の有価証券報告書数:",len(yuho_df_filtered))

79個あると少し時間がかかってしまうため以降では上から30個を分析します。 また、ここで「docID」カラムをインデックスに指定しておきます。「docID」カラムはダウンロードした提出書類のファイル名にもなっており、提出された有価証券報告書を特定するキーとして利用します。

yuho_df_filtered = yuho_df_filtered.set_index("docID").head(30)
# 5行表示
yuho_df_filtered.head()

書類一覧APIのレスポンスデータ

ちなみに、Google colaboratoryや(静的解析の拡張機能が入った)VSCodeでは画像のようにpythonの変数の型ヒントとドキュメンテーションをマウスオーバーで表示できるので利用する際に参考にしてください。

型ヒントとドキュメンテーションの表示

2. EDINET APIからの有価証券報告書のダウンロード

ダウンロードします。業種を絞った提出書類一覧からdocidを渡せばダウンロードできます。 ダウンロード先はout_filenameで指定します。

res_results = []
for docid in tqdm(yuho_df_filtered.index):
    out_filename = str(DATA_PATH / "raw/xbrl_doc" / (docid + ".zip"))
    res_results.append(request_doc(api_key=your_api_key, docid=docid, out_filename_str=out_filename))
    sleep(0.5)
print("取得失敗数: ",len([res for res in res_results if res.status == 'failure']))

3. 有報XBRLデータの大福帳(One Big Table)の作成

2024年の共通タクソノミ(勘定科目マスタのようなもの)をダウンロードします。2023年の有価証券報告書を分析する場合は2023年のものが必要です。1分40秒くらいかかります。

account_list_common_obj_2024 = account_list_common(
    data_path=DATA_PATH,
    account_list_year="2024"
)

有価証券報告書の中の財務諸表等(貸借対照表損益計算書、注記など)は「role」カラム(目次項目の拡張リンクロール)で分けられています。全部取得すると時間がかかるため、必要なものに絞って処理します。 ロール名を検索するキーワードを指定します。 BS: 貸借対照表、PL: 損益計算書、CF: キャッシュフロー計算書、SS: 株主資本等変動計算書、notes: 注記、report: 前段(事業の状況など) report以外には連結と個別の両方があります。

# 拡張リンクロール検索キーワード
fs_dict = {
    'BS':["_BalanceSheet","_ConsolidatedBalanceSheet"],
    'PL':["_StatementOfIncome","_ConsolidatedStatementOfIncome"],
    'CF':["_StatementOfCashFlows","_ConsolidatedStatementOfCashFlows"],
    'SS':["_StatementOfChangesInEquity","_ConsolidatedStatementOfChangesInEquity"],
    'notes':["_Notes","_ConsolidatedNotes"],
    'report':["_CabinetOfficeOrdinanceOnDisclosure"]
}

指定したキーワードを含むロールの財務諸表等データをpandas.DataFrameで取得します。1書類あたり30秒弱かかってしまいます(高速化を検討していますが、筆者の手元にある10年分の有報データがだいたい45000書類くらいですが、20コア程度で並列化をすることで1日くらいで完了してしまい高速化のモチベが湧きません)。

# XBRLから必要なロールの財務情報を取得
fs_tbl_df_list = []
for docid in tqdm(yuho_df_filtered.index):
    fs_tbl_df:FsDataDf = get_fs_tbl(
        account_list_common_obj=account_list_common_obj_2024,
        docid=docid,
        zip_file_str=str(DATA_PATH / "raw/xbrl_doc" / (docid + ".zip")),
        temp_path_str=str(DATA_PATH / "raw/xbrl_doc_ext" / docid), 
        role_keyward_list=fs_dict['BS']+fs_dict['PL']+fs_dict['report'], # ロールを指定
    )
    fs_tbl_df = fs_tbl_df.assign(
        filerName=yuho_df_filtered.loc[docid,'filerName'],
        sector_label_33=yuho_df_filtered.loc[docid,'sector_label_33']
    )
    fs_tbl_df_list.append(fs_tbl_df)
fs_tbl_df_all:FsBigTableDf = FsBigTableDf(pd.concat(fs_tbl_df_list))

有報XBRLの大福帳データの主なカラム

これでXBRLのデータをまとめることができました。以降はいくつかの探索的データ分析の例をご紹介するだけなので、自分で分析したい方は、ブログを閉じて分析を楽しんでください。

4. 有価証券報告書前段部分のテキスト分析

それでは、ここから分析に入っていきます。

データ分析は大きく仮説検証と仮説探索の2つがあります。前者は既に分析者に仮説があって、それをデータで検証します。A/Bテストや因果効果の測定等がこれに該当します。後者はデータをあれこれ見ながら仮説を探索します。今回はデータから仮説を見つけたいので後者の分析を進めていきます。(したがって、見つけてきた仮説については、独立したデータで再現性を確認する必要があります。)

まずは、有価証券報告書前段の事業等のリスクを分析してみます。どんなことが書かれているか大雑把に把握することが目的です。テキストを全部生成AIに丸投げして要約でもよいのですが、文章量が多いため、ここではLDA(latent dirichlet allocation)という古典的なモデルを使います。

まずは、有価証券報告書の前段部分を抽出します。roleは3.に記載されている拡張リンクロール検索キーワードを参考にしてください(そのうち日本語のラベルもつけます。)。

text_df = fs_tbl_df_all.query("role.str.contains('CabinetOfficeOrdinanceOnDisclosure')")

4.1. 事業等のリスクの分析

gensimのインストールでハマってしまった方は4.1を飛ばしてください。 日本語のフォント設定がないと図の文字が「口」になってしまうためmatplotlib_fontjaというライブラリからフォントを取得します。利用にあたってはIPAフォントライセンスv1.0に同意してください。

# wordcloud用に日本語のフォントを取得
font_dir_path = get_font_path()
font_dirs = [font_dir_path]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

有報の前段部分から事業等のリスクを抽出します。

business_risk_df = text_df.query("key == 'jpcrp_cor:BusinessRisksTextBlock'")

テキストの前処理をします。XBRLデータのテキストはhtml形式になっているため、テキスト部分以外を削除し、余計な改行や特殊な記号を削除します。形式的な処理なのでedinet_xbrl_prep.preproc_nlp()に処理を入れ込んでしまっています。他の処理をしたい方は自作しましょう。

次に、単語単位での分析をするため、janomeライブラリを用いてトークン化します。トークン化に際して、名詞と形容詞以外は削除しています。インストールの容易さだけでjanomeを選んでいるので、しっかり分析する場合には他のライブラリも検討しましょう。

janome_tokenizer = Tokenizer() # janomeのトークナイザー

def tokenize_janome(text:str)->str:
    """単語分割をして、名詞と形容詞のみを取り出す"""
    return [token.surface for token in janome_tokenizer.tokenize(text) if token.part_of_speech.split(',')[0] in ['名詞','形容詞']]

business_risk_df['text_processed'] = business_risk_df.data_str.apply(preproc_nlp,drop_htmp=True,drop_number=True,reduce_return=True)
business_risk_df['text_tokenized'] = business_risk_df.text_processed.apply(tokenize_janome)

business_risk_df.head()['text_tokenized']

単語分割の様子

このように単語で分割されています。

分割した単語から辞書を作成します。作成に際しては、複数書類にわたって出現しているが、出現率が70%未満である単語に絞ります。1書類にしか登場しない単語は固有名詞などが多く、また、多くの書類で登場する単語は特徴のない単語ですので、このようなノイズになる単語は削除しておきます。 出現頻度が少ない特徴的な単語を際立たせるためTF-IDFを計算し、それに対してLDAモデルを訓練します。

dictionary = Dictionary(business_risk_df['text_tokenized'].to_list())
# 1書類にしか出現しない単語、70%以上の書類に出現する単語を削除
dictionary.filter_extremes(no_below=2, no_above=0.7,keep_n=400000)
# トークンIDに変換
business_risk_df = business_risk_df.assign(text_tokenized_id=business_risk_df.text_tokenized.apply(dictionary.doc2bow))

tf_idf = True
bow_corpus = business_risk_df.text_tokenized_id.to_list()
# TF-IDFを計算
if tf_idf:
    tfidf = TfidfModel(bow_corpus)
    corpus_tfidf = tfidf[bow_corpus]
    corpus = corpus_tfidf
else:
    corpus = bow_corpus

NUM_TOPICS = 10 # トピック数
# LDAモデルの学習
lda=LdaModel(corpus=corpus, num_topics=NUM_TOPICS, id2word=dictionary,alpha='auto',random_state=0)

LDAトピックの成分である単語をワードクラウドで可視化します。文字サイズはトピックにおける単語の重要度です。

事業等のリスクのトピック

4.2. 事業内容の抽出

1.で取得したように東証やEDINETが業種情報を提供していますが、企業によっては該当する業種区分がなかったり、複数の事業を行ってることもあるため、不十分な場合があります。 そこで、有価証券報告書前段の事業の内容に記載されたテキストからその会社の事業内容を生成AIで抽出します。生成AIはGroqを使用しますので、「0.3. Groq APIキーの発行」が完了している必要があります。

前段部分から事業の内容を抽出します。

business_desc_df = text_df.query("key == 'jpcrp_cor:DescriptionOfBusinessTextBlock'")
business_desc_df['text_processed'] = business_desc_df.data_str.apply(preproc_nlp,drop_htmp=True,drop_number=False,reduce_return=True)

生成AIのインプットにするプロンプトを作成し、例を出力します。 生成AIプロンプトは洗練されたものではないですが、いくつかポイントがあります。
- markdown形式で記載
- 例を記載
- 抽出項目に「事業内容」と分けて「具体的な説明」をつくっておくことで、「事業内容」に簡潔なテキストが入るように誘導する。

# プロンプトの作成
example_text="""
##### 文章
3 【事業の内容】当社の主たる事業は物流業であります
その事業は....(このブログ上では省略します)

##### 回答
``json
{"事業内容":"貨物運送事業","具体的な説明":"食料品、日用品雑貨等の消費関連貨物の輸送に加え、第一・第二種利用運送事業も実施。"}
{"事業内容":"倉庫事業","具体的な説明":"営業倉庫と物流センターの運営。集荷・保管・流通加工・配送・回収までの一貫したサービスを提供"}
...``
以上の指示に従って、提供された文章から、事業内容を抽出して整理してください。
"""

prompt_ext_business = Prompt(**{
    "instruction": """提供される文章はある会社の事業内容に関する文章の抜粋です。ここからわかる会社の主な事業内容を日本語で列挙してください。""",
    "example": example_text,
    "constraints_list": ["できるだけ文章中の記載をそのまま引用してください。","固有名詞は伏せてください。","具体的な説明がある場合は、それも記載してください。ない場合は「なし」としてください。"],
    "output_format": """#### 回答形式\n\nフォーマットは個別のjson形式で回答してください。\n\n{"事業内容":"(事業内容1)","具体的な説明":"(具体的な説明1)"}\n{"事業内容":"(事業内容2)","具体的な説明":"(具体的な説明2)"}""",
    })

Promptクラスに埋め込んでしまっていますが、以下のようにプロンプトの構成要素をつなげています。

def export(self,provided_text):
    constraint = "#### 注意事項"+"\n"+" * "+("\n"+" * ").join(self.constraints_list)
    system_prompt = self.instruction+"\n\n"+constraint+"\n\n"+self.output_format+"\n\n"+"#### 例"+"\n"+self.example
    user_prompt = "#### 文章"+"\n"+provided_text+"\n\n"+"#### 回答" +"\n"
    return system_prompt,user_prompt

Groqを用いて生成AIで事業内容を抽出します。Groqはモデルごとにレート制限が1分単位と1日単位であります。1日単位の制限も秒単位で回復していくため少し待てば応答してくれますが、そこそこ時間がかかるため、引っかかった場合にはモデルを変更するか諦めて寝ましょう。

your_api_key_groq: str = input("Please input your Groq API key: ")
GroqAPI_obj = GroqAPI(api_key=your_api_key_groq)


def extract_business_description(sr:pd.Series)->list:
    sys_prompt,usr_prompt = prompt_ext_business.export(provided_text=sr.text_processed)
    response = GroqAPI_obj.request(sys_prompt=sys_prompt,usr_prompt=usr_prompt,model_name='llama-3.1-8b-instant')
    if not response.output_json_validation():
        sleep(1)
        response2 = GroqAPI_obj.request(sys_prompt=sys_prompt,usr_prompt=usr_prompt,model_name='llama-3.1-8b-instant')
        if not response2.output_json_validation():
            print("Error")
            return []
        else:
            response = response2
    output_json_list = response.extract_output_json()
    return output_json_list

# レコードそれぞれについて生成AIで事業内容を抽出
business_desc_df['business_extracted'] = business_desc_df.apply(extract_business_description,axis=1)
# 抽出した内容を少し確認
business_desc_df.head()[['docid','text_processed','business_extracted']]

生成AIによる事業内容の抽出結果

事業内容を抽出できました。抽出した事業内容は5.2で使います。

5. 財務数値の分析

5.1. 棚卸資産の内訳の可視化

これまでテキストデータの分析が続いていましたが、数値データ(金額)の分析です。

BSを抽出し、棚卸資産データを抽出します。日本語のラベルはlabel_jpカラムにあるため、pandas.DataFrame.value_counts()などで検索しましょう。 しっかり分析したい場合は、スキーマと要素名を結合したインデックスがkeyカラムにあるため、これで抽出してください。スキーマと要素名は金融庁のサイトからダウンロードできる勘定科目リストで調べることができます。

# BSの抽出
bs_df = fs_tbl_df_all.query("non_consolidated_flg==0 and current_flg==1 and role.str.contains('BalanceSheet')")
# 在庫の抽出
inventory_df = bs_df.query("label_jp.str.contains('商品及び製品') or label_jp.str.contains('仕掛品') or label_jp.str.contains('原材料及び貯蔵品')")

数値データの前処理をします。テキスト同様に形式的な処理なのでedinet_xbrl_prep.preproc_nlp()に処理を入れ込んでしまっています。他の処理をしたい方は自作しましょう。なお、ここでは提出者名とラベル情報を結合キーにしていますが、"docid"と"key"の方が望ましいです。

# 数値データの前処理
inventory_df = preproc_num(inventory_df) # テキストを数値に変換など
inventory_df_filled = fill_df(inventory_df) # 欠損値の補完
# ピボットテーブルの作成
inventory_df_pivot = pd.crosstab(index=inventory_df_filled['filerName'], columns=inventory_df_filled['label_jp'], values=inventory_df_filled['data'], aggfunc='sum',normalize='index')

ちなみに、有価証券報告書に金額0が入っている場合は表示桁数に満たない金額で存在しています(その他に含めず独立掲記しなければならない場合など)。そのため、次のようにもともと0が入っている場合は表示桁数 x 0.5で埋めて、値が入っていない場合を0で埋めます。

df.data = df.data.fillna(-1) # いったんNoneを-1に退避
df.data = df.data.replace(0,np.nan).fillna(0.5*10**(df['decimals'].astype(float)*-1)) # 0をNoneに置き換えて、表示桁数 x 0.5で埋める
df.data = df.data.replace(-1,0) # -1を0に置き換える

棚卸資産の内訳を比較しやすいように、内訳が類似している企業が近くなるように第一主成分で並べ替えます[注3]。並べ替えたデータで積み上げ棒グラフを作成します。完成品の在庫量が比較的大きい企業とそうでない企業があることがわかります。

棚卸資産の内訳

5.2. 棚卸資産の内訳と事業内容

では、完成品の在庫量の大きい企業、小さい企業はどのような事業でしょうか。さきほど取得した事業内容を整列させて図示してみます。

# 4.2で取得した事業内容を結合
reordered_inventory_df_pivot2 = pd.merge(
    reordered_inventory_df_pivot,
    business_desc_df.set_index('filerName')[['business_extracted']],
    left_index=True,
    right_index=True,
    how='left'
)

事業内容と一緒に描画します。いい感じにばらけるようにyの値を調整しています。

棚卸資産の内訳と事業内容

完成品の割合が大きい会社はお肉や加工食品を扱う事業である傾向がなんとなく読み取れます。

5.3. 営業利益率の分析

もう一つ財務数値の分析をしてみます。

# PLの取得

pl_df = fs_tbl_df_all.query("non_consolidated_flg==0 and current_flg==1 and role.str.contains('StatementOfIncome')")

営業利益率算定のために、営業利益と売上高、規模との関係をみるために総資産の金額を取得して、5.1同様の前処理をします。 事業セグメントごとのレコードも入ってしまっていますが、context_refに「_XXXXXXX」のような追加がされているものなので、コンテキストをアンダーバーで区切った要素数が一番短いものに絞ることで除外しています。

# PLから営業利益の取得(売上、総資産も同様)
profit_df = pl_df.query("label_jp.str.contains('営業利益又は営業損失(△)')")
profit_df = preproc_num(profit_df)
profit_df = profit_df.sort_values('context_ref_len',ascending=True).drop_duplicates(keep='first',subset=['docid','key']) # コンテキストをアンダーバーで区切った要素数が一番短いものに絞る
print(len(profit_df))

散布図を描画します。

営業利益率と総資産の散布図

規模が大きいほど営業利益率が大きい傾向がなんとなく読み取れます。規模の経済でしょうか。事業内容によっても変わりそうなのでなんとも言えません。

5.4. BS科目の多変量分析(独立成分分析

せっかく大福帳データをつくったのに、特定の科目に着目した分析のみでは勿体無いです。そこで、BS科目全体の変動パターンを分析してみます。 当期のBSと前期のBSを抽出し、5.1と同様の前処理をします。

# 当期BSの抽出
bs_cy = fs_tbl_df_all.query("non_consolidated_flg==0 and current_flg==1 and role.str.contains('BalanceSheet') and label_jp != '-'")
# 前期BSの抽出
bs_py = fs_tbl_df_all.query("non_consolidated_flg==0 and prior_flg==1 and role.str.contains('BalanceSheet') and label_jp != '-'")
# 数値データの前処理
bs_cy = preproc_num(bs_cy).sort_values('context_ref_len',ascending=True).drop_duplicates(keep='first',subset=['docid','key']) # context_refが短い
bs_cy_filled = fill_df(bs_cy)

bs_py = preproc_num(bs_py).sort_values('context_ref_len',ascending=True).drop_duplicates(keep='first',subset=['docid','key'])
bs_py_filled = fill_df(bs_py)

増減額を計算します。規模を揃えるために当期の総資産で割っています。

# 前期金額カラムを結合
cy_py = bs_cy_filled.set_index(['filerName','key']).join(
    bs_py_filled.set_index(['filerName','key']).rename(columns={'data':'data_py'})[['data_py']]
    ,how='left'
    )
# 総資産金額カラムを結合
cy_py = pd.merge(
    cy_py.reset_index(),
    bs_cy_filled.query("key == 'jppfs_cor:Assets'").set_index(['filerName','key']).rename(columns={'data':'total_asset'})[['total_asset']],
    on=['filerName'],
    how='left'
    )
# 総資産金額でスケーリングした増減を計算
cy_py = cy_py.assign(
    data_diff=(cy_py.data - cy_py.data_py) / cy_py.total_asset,
)

次のようにカラムに勘定科目をもつ表データが作成されます。

スケーリングしたBS増減ベクトル

多変量分析は独立成分分析(ICA: Independent Component Analysis)を用います。scikit-learnにあるので簡単に使えます。

ICA = FastICA(n_components=10, random_state=0)
X_transformed = ICA.fit_transform(cy_py_pivot)

独立成分分析の各成分を図示します。

BS科目の変動成分

データサイズが小さいので、あまり綺麗に分かれず解釈が難しいですが、Component_1は全体的な変動、Component_2は仕入、Component_4は設備の完成、Component_7は設備投資あたりでしょうか。ちょっと苦しいですね。

6. XBRLデータをデータベースに出力して生成AIに分析してもらう(Claude+MCPを利用した分析)

MCPは、例えばコンピュータのローカルのデータベースに接続してデータを抽出するといった外部アプリ連携を生成AI(Claude)に提供します。つまり、人間が分析する必要がなくなります。私の仕事がなくなる日も近いですね。

MCPでClaudeにデータを与えて分析してもらうために、SQLiteというデータベースファイルにXBRLデータを出力します。 テキスト情報が多い前段部分と財務数値部分を別のテーブルに分けて出力しました。

import sqlite3
file_sqlite3 = DATA_PATH / "xbrl_parsed.db"
conn = sqlite3.connect(str(file_sqlite3))
fs_tbl_df_export_ci.drop('data_str',axis=1).to_sql('corporate_information',conn,index=None,if_exists='replace') # 前段
fs_tbl_df_export_num.to_sql('xbrl_parsed',conn,index=None,if_exists='replace') # 財務数値
conn.close()

以下のプロンプトを与えて、Claudeに分析してもらいます。

SQLiteデータベースに接続して、主な事業内容と各社の連結の営業利益率を比較して、事業内容をクラスタリングし、営業利益率にどのような関係があるか散布図を作成し教えてください。
事業の内容はcorporate_informationテーブルのtagが"DescriptionOfBusiness"を含んでいる場合で探してください。
財務数値はxbrl_parsedテーブルにあります。連結の財務数値はnon_consolidated_flg=0で抽出します。また、当期はcurrent_flg=1、前期はprior_flg=0で抽出できます。

うまくいかずに何度も何度か試行錯誤し、テーブルのカラム名をわかりやすい一般的な名称にしたり、不要なカラムは削除しておくことでなんとか分析させることができました。 とはいえ、クエリの失敗が多いので、テーブルメタデータやクエリの例とか与えてあげるとスムーズに行くのではないかと思います。

以下、Claudeの出力です。クエリで失敗しているところは省いています。

Claudeの出力1
テーブル構造を確認して、クエリを作成しています。

Claudeの出力2
クエリを書いてデータを抽出しています。失敗部分は折りたたんでいますが、だいたいはクエリの結果として空のデータが出力されています。最後になんとか抽出できたようです。

Claudeの出力3

図の右側に散布図がありますが、マウスオーバーで抽出した事業内容を表示できるインタラクティブな可視化を作成してくれます。

まとめ

XBRLデータを分析しやすい大福帳(One Big Table)データにすることで、さまざまな分析のハードルが下がります。また、生成AIにとっても扱いやすいデータになります。 皆様も色々分析してみてください。読んでいただきありがとうございました。

注1 環境構築

なお、筆者はuvで環境を構築しているので、その方法だけ記載しておきます。 0.1.1 uvのインストール

brew install uv

0.1.1 プロジェクトの作成します。

cd projectフォルダを作成したいフォルダ
uv init プロジェクト名
cd プロジェクト名

0.1.2 プロジェクトの作成します。 プロジェクトを作成すると自動作成されるpyproject.tomlを編集します。requires-pythonとdependenciesを以下に書き換えます。

requires-python = "==3.12"
dependencies = [
    "arelle-release>=2.35.17",
    "gensim>=4.3.2",
    "groq>=0.13.1",
    "ipykernel>=6.29.5",
    "janome>=0.5.0",
    "japanize-matplotlib>=1.1.3",
    "matplotlib-fontja>=1.0.0",
    "matplotlib>=3.10.0",
    "pandas>=2.2.3",
    "pandera>=0.21.1",
    "requests>=2.32.3",
    "scikit-learn>=1.6.0",
    "scipy==1.12",
    "seaborn>=0.13.2",
    "setuptools>=75.6.0",
    "tqdm>=4.67.1",
    "wordcloud>=1.9.4",
    "xlrd>=2.0.1",
]

0.1.3 仮想環境を作成します。

uv sync

uvについては以下が大変参考になります。

zenn.dev

注2 XBRLデータの抽出の難しいところ

XBRLはエクセルのようにかなり自由なデータ形式です。自由な形式ほどデータ抽出は困難を極めます。データの抽出精度が9割でよければ簡単ですが、これを10割にするのはものすごく大変です。例えば、有価証券報告書のPDF(やインラインXBRLというhtmファイル)にはあるものの、XBRLインスタンスに含まれていない項目があったり、誤解しやすいタグを使っていたりするケース(提出者のミスなのか行き過ぎた自由なのか)が散見されます。こういうのは生成AIで検知する仕組みを作りたいですね。また、公開しているソースコードではイレギュラーなXBRLは扱えていませんので、今後アップデートしていきたいと思います。

注3 組成データの距離について

合計が1になる内訳データは組成データと呼ばれており、組成データ用の分析手法があります。例えばAが10%とBが20%の場合とAが20%でBが40%の場合はどちらもBがAの2倍という点で類似していますが、そのままユークリッド距離を計算するとこの点が反映されません。そこで、比率関係をの類似性を扱うスケーリングを施したAitchison距離が提案されています。 しかしながら、今回扱う財務データ(金額)には欠損や0値、相対的にすごく小さい値があり、Aitchison距離はあまり適していません。また、今回は小さい値は重要でないことから、通常のユークリッド距離を用いて(主成分分析をして)います。

注4 XBRLデータ抽出の参考になるページ

99までの知識に興味がある方は以下が参考になります。網羅的に調査できていないため、紹介できていないブログ等ありましたらごめんなさい。

一般社団法人 XBRL Japan - XBRL Japan Inc. - XBRLのテクノロジー
XBRLの様々な情報が調べられます。

シラベルノート | 業績から株式銘柄の良さを調べています。決算分析のコード例は『XBRLまとめ』からどうぞ。
筆者は2018年頃からEDINETのXBRLデータの前処理と定期的に戦っていましたが、このブログをずっと後追いしていました。Arelleもこのブログで知りました。大変お世話になりました。

財務分析に欠かせない、XBRLを読む: タクソノミ文書編|piqcy
タクソノミの説明がわかりやすいです。仕様書が丁寧に解説されています。まだ情報が少ない中の公開で大変お世話になりました。

ゼロから始めないXBRL解析(Arelleの活用)
Arelleの公開情報が少ないため参考になります。コメントに読み方も書いてあります。

Pythonで学ぶXBRL入門:KB農園
最近出版されているため、pythonやライブラリのバージョンが新しく実践しやすいと思います。

PythonとExcelによるXBRL解析 株式投資に役立つ財務分析の準備
こちらも最近出版されており、実践しやすいと思います。