EDINET開示のXBRLデータから、平均給与等の従業員情報を自動で抽出してみよう(5/10)

Advent Calendar ５回目の投稿では、EDINET APIで取得してきたXBRLデータから欲しい情報を抽出してみます。今回は、就職活動などでも良く目にする企業の平均給与や平均年齢などを対象にしてみました。

（本記事のプログラムは、一切の保証なく、現状で提供されるものであり、XBRLJapanは、本プログラムの利用に伴って発生した不利益や問題について、原因のいかんを問わず、一切の責任を負わないものとします。）

# 1. XBRLデータから特定の情報を抽出する
## 1.1 プログラム概要
EDINET APIで取得したzipファイル（XBRLデータ含む）を解凍・解析し、有価証券報告書から給与等の情報を抽出した後、CSVとして必要な情報を出力するPython言語のプログラムです。（「3. ソースコード」に全コード記載）
XBRLデータから抽出する項目は以下表のとおりです。

|項目 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |XBRLで定義された要素名&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|取得例 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |
|:---|---|---|
|EDINETCODE  |EDINETCodeDEI  |E00004 |
|企業名  |FilerNameInJapaneseDEI  |カネコ種苗株式会社  |
|平均年間給与（円）  |AverageAnnualSalaryInformationAboutReportingCompanyInformationAboutEmployees|5,554,933  |
|平均勤続年数（年） |AverageLengthOfServiceYearsInformationAboutReportingCompanyInformationAboutEmployees  |13.0  |
|平均勤続年数（月）|AverageLengthOfServiceMonthsInformationAboutReportingCompanyInformationAboutEmployees  |ー  |
|平均年齢（歳） |AverageAgeYearsInformationAboutReportingCompanyInformationAboutEmployees  |42.2 |
|平均年齢（月） |AverageAgeMonthsInformationAboutReportingCompanyInformationAboutEmployees  |ー  |
|従業員数（人） |NumberOfEmployees  |625  |

その他、特徴的な点を、以下に記載します。
○企業の`業種`を特定するため、EDINETコードリストを使用する
○企業の`独自拡張項目`の取得には対応していない
○`Arelle`と呼ばれるOSSのパーサーをXBRL解析に利用した
○平均勤続年数（月）、平均年齢（月）があれば、`年換算`（小数第二位四捨五入）する。



## 1.2 事前準備
以下内容について、プログラム実行前に対応してください。また、その他ライブラリのインストールなども事前に実施が必要です。実行環境に応じて、encodingやパス指定の方法も適宜変更します。

### 1.2.1 XBRLデータの準備
「XBRLで記述されたデータを探してみよう」、「XBRLで記述されたデータを集めてみよう」などを活用し、EDINETからzipファイル（XBRLデータ含む）をダウンロードします。平均年間給与（円）やその他項目は、EDINETで[新たに追加された](https://www.fsa.go.jp/search/20171204/1b_GaiyoSetsumei_Shinkyu.pdf)項目のため、ひとまずは、2019年4月1日以降に開示されたXBRLデータをダウンロードしましょう。

### 1.2.2 XBRLParserのインストール
[Arelle](http://arelle.org/)と呼ばれるXBRLParserをインストールします。`Arelle`は、インスタンスを読み込むと外部のタクソノミ情報なども含めてDTSセットで解析を行います。そのため、高度なXBRLデータの活用には向いていますが、インスタンスのみ使用する場合などは、処理に時間がかかるためOSSのXMLライブラリや、みなさんが開発されているパーサーで代替することも検討しましょう。

※2024/04/09：arelleインストールの補足
正式なarelleプロジェクトがpypiにリリースされています。
arelleの[github](https://github.com/Arelle/Arelle)からでもよいですが、公式のプロジェクトを利用するようにしましょう。
https://pypi.org/project/arelle-release/

qiitaの記事に、Arelleのインストール方法や、その他開発しているパーサーの情報もありますので、一部をここに記載しておきます。
・[ゼロから始めないXBRL解析(Arelleの活用)](https://qiita.com/xtarou/items/fb3cc72b1b600b4309db)
・[EDINETのXBRL用のPythonライブラリを作った - Parser編](https://qiita.com/shoe116/items/dd362ad880f2b6baa96f)
・[UFOキャッチャーからXBRLをダウンロード&パースするクラスを作った](https://qiita.com/sawadyrr5/items/67059635545cf0a11c8e)
・[上場会社の有価証券報告書XBRLを取得して値を読み込む(←主に名前空間の取得の部分を改良](https://qiita.com/Fortinbras/items/340e6702270e065b4d95)

### 1.2.3 EDINETコードリストの準備
[EDINETサイト](http://disclosure.edinet-fsa.go.jp/)の[ダウンロード]タブ遷移後の、ページ下段にある`EDINETコードリスト`よりダウンロードが可能です。このリストは、`EDINETCODEと業種が紐付いて記載`されています。そのため、本リストを活用することで、企業の業種も併せて取得することができます。
<img width="250" alt="スクリーンショット 2019-12-01 20.34.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500494/0b20fad3-e309-06d5-f923-3e14e49ae4cb.png">

### 1.2.4 格納/解凍/出力フォルダの決定
○ダウンロードしたEDINETコードリストを読み込むための格納フォルダを決めましょう。
`edinetcodedlinfo_filepath = 'C://Users//xxx//Desktop//xbrlReport//EdinetcodeDlInfo.csv'`

○EDINET APIで取得してきたzipファイルを格納するフォルダを決めましょう。
`zip_dir = 'C://Users//xxx//Desktop//xbrlReport//SR//'`
※これにより、xbrlファイルが配置されるフォルダが以下のように決まります。
`xbrl_file_expressions = 'C://Users//xxx//Desktop//xbrlReport//SR//XBRL//PublicDoc//*.xbrl'`

○CSVを出力するフォルダ、ファイル名を決めましょう。
`employee_frame.to_csv("C://Users//xxx//Desktop//xbrlReport//xbrl_qiita.csv", encoding='cp932')`

## 1.3 実行結果
EDINET APIで取得済みのzipファイルを解凍し、配置されたPublicDocフォルダ配下の`インスタンスファイル`（.xbrl拡張子）の一覧を`glob`で取得します。（zip解凍あたりの処理は説明を割愛しました）

```python:Code1
xbrl_file_expressions = glob.glob('C:\\Users\\xxx\\Desktop\\xbrlReport\\SR\\XBRL\\PublicDoc\\*.xbrl')

```

```:Result1
['C:\\Users\\XXX\\Desktop\\xbrlReport\\SR\\XBRL\\PublicDoc\\jpcrp030000-asr-001_E00112-000_2019-03-31_01_2019-06-27.xbrl', 'C:\\Users\\XXX\\Desktop\\xbrlReport\\SR\\XBRL\\PublicDoc\\jpcrp030000-asr-001_E01422-000_2019-03-31_01_2019-06-27.xbrl', 'C:\\Users\\XXX\\Desktop\\xbrlReport\\SR\\XBRL\\PublicDoc\\jpcrp030000-asr-001_E02770-000_2019-03-31_01_2019-06-21.xbrl', 'C:\\Users\\XXX\\Desktop\\xbrlReport\\SR\\XBRL\\PublicDoc\\jpcrp030000-asr-001_E04510-000_2019-03-31_01_2019-06-27.xbrl']

```

`xbrl_files`を使ってループ処理を行い、各インスタンスファイル毎に、ArelleのmodelManager.load(xbrl_file)でXBRLデータの読み込みを行います。結果、XBRLデータ情報を保持するArelleのmodel_xbrlオブジェクトを取得できます。

```python:Code2
 for index, xbrl_file in enumerate(xbrl_files):

    ctrl = Cntlr.Cntlr()
    model_manager= ModelManager.initialize(ctrl)
    model_xbrl= modelManager.load(xbrl_file)

```

```:Result2

<arelle.ModelXbrl.Model_xbrl object at 0x0EF6F350>
```

次に、インスタンスに含まれる全`fact`（要素の数値など実データ）がリスト形式で定義された`model_xbrl.facts`を使ってループ処理を行い、各`fact`の要素名（`fact.concept.qname.localName`）を取得します。要素名が、今回抽出対象としている要素名であった場合に、当該要素の数値（`fact.value`）を取得する仕組みです。EDINETCODEを処理する際には、`EdinetcodeDlInfo.csv`から企業の業種を特定するためのロジックを組み込んでおきます。従業員数については、同じ要素名が複数存在するため、`context`と呼ばれる期間属性で、当期（`CurrentYearInstant`）を指定しています。

```python:Code3
for fact in model_xbrl.facts:
    if fact.concept.qname.localName == 'EDINETCodeDEI':
        edinet_code = fact.value

        for code_name in edinet_info_list:
            if code_name[0] == edinet_code:
                industry_code = code_name[1]
                break

 elif fact.concept.qname.localName=='FilerNameInJapaneseDEI':
 elif fact.concept.qname.localName=='AverageAnnualSalaryInformationAboutReportingCompanyInformationAboutEmployees':
 elif fact.concept.qname.localName=='AverageLengthOfServiceYearsInformationAboutReportingCompanyInformationAboutEmployees':
 elif fact.concept.qname.localName=='AverageLengthOfServiceMonthsInformationAboutReportingCompanyInformationAboutEmployees':
 elif fact.concept.qname.localName=='AverageAgeYearsInformationAboutReportingCompanyInformationAboutEmployees':
 elif fact.concept.qname.localName=='AverageAgeMonthsInformationAboutReportingCompanyInformationAboutEmployees':
 elif fact.concept.qname.localName=='NumberOfEmployees':
     if fact.contextID == 'CurrentYearInstant_NonConsolidatedMember':

```

その後、取得してきた各要素（`edinet_code`、`filer_name_jp`、`industry_code`、`salary_info`、`service_years`、`age_years`、`number_of_employees`）の情報を`company_info_list`に格納していきます。このときに、平均勤続年数（月）(`service_months`）と平均年齢（月）（`age_months`）が存在していたら、年換算する処理を組み込みました。その後、`edinet_company_info_list`にEDINETCODE単位（インスタンスファイル単位）で`company_info_list`を格納すれば、各企業の従業員の情報を保持したリストが完成します。

```python:Code4
    company_info_list.append(edinet_code)
    company_info_list.append(filer_name_jp)
    company_info_list.append(industry_code)
    company_info_list.append(salary_info)

    if len(service_months) != 0:
        service_years_decimal= round(int(service_months)/12,1)
        service_years = int(service_years) + service_years_decimal
        service_years = str(service_years)

    company_info_list.append(service_years)

    if len(age_months) != 0:
        age_years_decimal= round(int(age_months)/12,1)
        age_years = int(age_years) + age_years_decimal
        age_years = str(age_years)

    company_info_list.append(age_years)
    company_info_list.append(number_of_employees)

    edinet_company_info_list.append(Company_info_list)

```
`edinet_company_info_list`は以下のようになります。

```:Result4
[['E00112', '東鉄工業株式会社', '建設業', '8547489', '13.8', '41.2', '1673'], ['E01422', '不二サッシ株式会社', '金属製品', '5527000', '20.7', '44.7', '850'], ['E02770', '株式会社ミスミグループ本社', '卸売業', '', '', '', '1293'], ['E04510', '電源開発株式会社', '電気・ガス業', '7980312', '19.6', '40.9', '2445']・・・]

```

最後に、取得してきた`edinet_company_info_list`をCSV形式で出力します。以上で、必要な要素について抽出が完了しました。

```python:Code5
employee_frame = pd.DataFrame(edinet_company_info_list,
                         columns=['EDINETCODE', '企業名', '業種', '平均年間給与（円）', ' 平均勤続年数（年）', '平均年齢（歳）', '従業員数（人）'])
employee_frame.to_csv("C://Users//xxx//Desktop//xbrlReport//xbrl_qiita.csv", encoding='cp932')
```
ちなみに、プログラムを実行すると、以下のようなCSVファイルが最終的に出力されます。企業開示の仕方により、平均年間給与の単位が何種類（円、千円など）か存在します。また、2019年4月以降のデータを対象にしていますが、各社の決算期の関係上、まだタグ付けがされておらず、給与関連のデータを取得できないものもあります。
※数千件のXBRLデータを処理すると、今回の方法では非常に時間がかかることが想定されます。次の記事で、全件処理済みのデータを開示しますので、ここではお試し程度の件数で処理することをオススメします。
<img width="889" alt="スクリーンショット 2019-12-01 22.06.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500494/0494b192-16e6-a8bf-0bc6-9ed8ea71ae3e.png">


# 2. ソースコード

```python

# -*- coding: utf-8 -*-

from arelle import ModelManager
from arelle import Cntlr
import os
import zipfile
import glob
import pandas as pd


def make_edinet_info_list(edinetcodedlinfo_filepath):
    edinet_info = pd.read_csv(edinetcodedlinfo_filepath, skiprows=1,
                                 encoding='cp932')
    edinet_info = edinet_info[["ＥＤＩＮＥＴコード", "提出者業種"]]
    edinet_info_list = edinet_info.values.tolist()
    return edinet_info_list

def unzip_file(zip_dir,xbrl_file_expressions):
    zip_files = glob.glob(os.path.join(zip_dir, '*.zip'))

    number_of_zip_lists = len(zip_files)
    print("number_of_zip_lists：", number_of_zip_lists)

    for index, zip_file in enumerate(zip_files):
        print(zip_file, ":", index + 1, "/", number_of_zip_lists)
        with zipfile.ZipFile(zip_file) as zip_f:
            zip_f.extractall(zip_dir)
            zip_f.close()

    xbrl_files = glob.glob(xbrl_file_expressions)
    return xbrl_files

def make_edinet_company_info_list(xbrl_files,edinet_info_list):
    edinet_company_info_list = []
    for index, xbrl_file in enumerate(xbrl_files):
        edinet_code = ""  # EDINETCODE
        filer_name_jp = ""  # 企業名
        industry_code = ""  # 業種
        salary_info = ""  # 平均年間給与（円）
        service_years = ""  # 平均勤続年数（年）
        service_months = ""  # 平均勤続年数（月）
        age_years = ""  # 平均年齢（歳）
        age_months = ""  # 平均年齢（月）
        number_of_employees = ""  # 従業員数（人）
        company_info_list = []  # 企業情報

        ctrl = Cntlr.Cntlr()
        model_manager = ModelManager.initialize(ctrl)
        model_xbrl = model_manager.load(xbrl_file)

        print(xbrl_file, ":", index + 1, "/", len(xbrl_files))

        for fact in model_xbrl.facts:

            if fact.concept.qname.localName == 'EDINETCodeDEI':
                print("EDINETコード", fact.value)
                edinet_code = fact.value

                for code_name in edinet_info_list:
                    if code_name[0] == edinet_code:
                        print("業種",code_name[1])
                        industry_code = code_name[1]
                        break

            elif fact.concept.qname.localName == 'FilerNameInJapaneseDEI':
                print("企業名", fact.value)
                filer_name_jp = fact.value

            elif fact.concept.qname.localName == 'AverageAnnualSalaryInformationAboutReportingCompanyInformationAboutEmployees':
                print("平均年間給与（円）", fact.value)
                salary_info = fact.value

            elif fact.concept.qname.localName == 'AverageLengthOfServiceYearsInformationAboutReportingCompanyInformationAboutEmployees':
                print("平均勤続年数（年）", fact.value)
                service_years = fact.value

            elif fact.concept.qname.localName == 'AverageLengthOfServiceMonthsInformationAboutReportingCompanyInformationAboutEmployees':
                print("平均勤続年数（月）", fact.value)
                service_months = fact.value

            elif fact.concept.qname.localName == 'AverageAgeYearsInformationAboutReportingCompanyInformationAboutEmployees':
                print("平均年齢（歳）", fact.value)
                age_years = fact.value

            elif fact.concept.qname.localName == 'AverageAgeMonthsInformationAboutReportingCompanyInformationAboutEmployees':
                print("平均年齢（月）", fact.value)
                age_months = fact.value

            elif fact.concept.qname.localName == 'NumberOfEmployees':
                if fact.contextID == 'CurrentYearInstant_NonConsolidatedMember':
                    print("従業員数（人）", fact.value)
                    number_of_employees = fact.value

        print("")
        company_info_list.append(edinet_code)
        company_info_list.append(filer_name_jp)
        company_info_list.append(industry_code)
        company_info_list.append(salary_info)

        if len(service_months) != 0:
            service_years_decimal = round(int(service_months) / 12, 1)
            service_years = int(service_years) + service_years_decimal
            service_years = str(service_years)

        company_info_list.append(service_years)

        if len(age_months) != 0:
            age_years_decimal = round(int(age_months) / 12, 1)
            age_years = int(age_years) + age_years_decimal
            age_years = str(age_years)

        company_info_list.append(age_years)
        company_info_list.append(number_of_employees)

        edinet_company_info_list.append(company_info_list)

    return edinet_company_info_list

def write_csv_of_employee_info(edinet_company_info_list):

    employee_frame = pd.DataFrame(edinet_company_info_list,
                         columns=['EDINETCODE', '企業名', '業種', '平均年間給与（円）', ' 平均勤続年数（年）', '平均年齢（歳）', '従業員数（人）'])

    print(employee_frame)
    employee_frame.to_csv("C://Users//xxx//Desktop//xbrlReport//xbrl_qiita.csv", encoding='cp932')


def main():
    edinetcodedlinfo_filepath = 'C://Users//xxx//Desktop//xbrlReport//EdinetcodeDlInfo.csv'
    edinet_info_list = make_edinet_info_list(edinetcodedlinfo_filepath)

    zip_dir = 'C://Users//xxx//Desktop//xbrlReport//SR//'
    xbrl_file_expressions = 'C://Users//xxx//Desktop//xbrlReport//SR//XBRL//PublicDoc//*.xbrl'
    xbrl_files = unzip_file(zip_dir,xbrl_file_expressions)

    edinet_company_info_list = make_edinet_company_info_list(xbrl_files,edinet_info_list)
    print(edinet_company_info_list)

    write_csv_of_employee_info(edinet_company_info_list)
    print("extract finish")

if __name__ == "__main__":
    main()


```

# 3. 従業員の情報以外を抽出する方法
特に説明もせず、`XBRLで定義された要素名`（ex.EDINETCodeDEI）を記載していましたが、これらは全てEDINETのサイトで公開されています。2019年12月時点での最新版は、[2020年版EDINETタクソノミの公表について](https://www.fsa.go.jp/search/20191101.html)の`タクソノミ要素リスト`と`勘定科目リスト`から確認ができます。今回抽出した科目は、タクソノミ要素リスト（`1e_ElementList.xlsx`）に記載があります。詳しくは当該ファイルの[企業内容等の開示に関する内閣府令 第三号様式 有価証券報告書  (jpcrp030000-asr)] - [9]シート - 212000b 従業員の状況 ( jpcrp_212000-002_2019-11-01_pre.xml )の箇所を確認してください。これらリストを活用すれば、`従業員の情報以外の様々な科目を抽出することが可能`になります。なお、`法令及び会計基準の改正等に対応`するため、EDINETタクソノミが更新されることもあります。そのため、インスタンスを処理する際には、EDINETタクソノミのバージョンが正しいかも事前に確認しましょう。

# 4. 問合せ先
本記事に関する問い合わせは、以下のメールアドレスまでお願いします。
e-mail：xbrl-tech-qa@xbrl.or.jp
（もちろん、qiita上でのコメントも歓迎します）

本メールアドレスは、qiitaの記事を執筆しているXBRLJapanの[開発委員会](https://www.xbrl.or.jp/modules/pico3/index.php?content_id=10)の問合せ窓口になります。
そのため、組織に関する一般的な問合せなどは内容によって回答できかねますが、XBRLに関する技術的な質問、意見、要望、助言等はお気軽にご連絡ください。
なお、委員会メンバが有志で対応しているため、回答に時間がかかることもありますが、ご了承ください。