Advent Calendar 5回目の投稿では、EDINET APIで取得してきたXBRLデータから欲しい情報を抽出してみます。今回は、就職活動などでも良く目にする企業の平均給与や平均年齢などを対象にしてみました。
(本記事のプログラムは、一切の保証なく、現状で提供されるものであり、XBRLJapanは、本プログラムの利用に伴って発生した不利益や問題について、原因のいかんを問わず、一切の責任を負わないものとします。)
1. XBRLデータから特定の情報を抽出する
1.1 プログラム概要
EDINET APIで取得したzipファイル(XBRLデータ含む)を解凍・解析し、有価証券報告書から給与等の情報を抽出した後、CSVとして必要な情報を出力するPython言語のプログラムです。(「3. ソースコード」に全コード記載)
XBRLデータから抽出する項目は以下表のとおりです。
項目 | XBRLで定義された要素名 | 取得例 |
---|---|---|
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で新たに追加された項目のため、ひとまずは、2019年4月1日以降に開示されたXBRLデータをダウンロードしましょう。
1.2.2 XBRLParserのインストール
Arelleと呼ばれるXBRLParserをインストールします。Arelle
は、インスタンスを読み込むと外部のタクソノミ情報なども含めてDTSセットで解析を行います。そのため、高度なXBRLデータの活用には向いていますが、インスタンスのみ使用する場合などは、処理に時間がかかるためOSSのXMLライブラリや、みなさんが開発されているパーサーで代替することも検討しましょう。
※2024/04/09:arelleインストールの補足
正式なarelleプロジェクトがpypiにリリースされています。
arelleのgithubからでもよいですが、公式のプロジェクトを利用するようにしましょう。
https://pypi.org/project/arelle-release/
qiitaの記事に、Arelleのインストール方法や、その他開発しているパーサーの情報もありますので、一部をここに記載しておきます。
・ゼロから始めないXBRL解析(Arelleの活用)
・EDINETのXBRL用のPythonライブラリを作った - Parser編
・UFOキャッチャーからXBRLをダウンロード&パースするクラスを作った
・上場会社の有価証券報告書XBRLを取得して値を読み込む(←主に名前空間の取得の部分を改良
1.2.3 EDINETコードリストの準備
EDINETサイトの[ダウンロード]タブ遷移後の、ページ下段にあるEDINETコードリスト
よりダウンロードが可能です。このリストは、EDINETCODEと業種が紐付いて記載
されています。そのため、本リストを活用することで、企業の業種も併せて取得することができます。
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解凍あたりの処理は説明を割愛しました)
xbrl_file_expressions = glob.glob('C:\\Users\\xxx\\Desktop\\xbrlReport\\SR\\XBRL\\PublicDoc\\*.xbrl')
['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オブジェクトを取得できます。
for index, xbrl_file in enumerate(xbrl_files):
ctrl = Cntlr.Cntlr()
model_manager= ModelManager.initialize(ctrl)
model_xbrl= modelManager.load(xbrl_file)
<arelle.ModelXbrl.Model_xbrl object at 0x0EF6F350>
次に、インスタンスに含まれる全fact
(要素の数値など実データ)がリスト形式で定義されたmodel_xbrl.facts
を使ってループ処理を行い、各fact
の要素名(fact.concept.qname.localName
)を取得します。要素名が、今回抽出対象としている要素名であった場合に、当該要素の数値(fact.value
)を取得する仕組みです。EDINETCODEを処理する際には、EdinetcodeDlInfo.csv
から企業の業種を特定するためのロジックを組み込んでおきます。従業員数については、同じ要素名が複数存在するため、context
と呼ばれる期間属性で、当期(CurrentYearInstant
)を指定しています。
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
を格納すれば、各企業の従業員の情報を保持したリストが完成します。
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
は以下のようになります。
[['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形式で出力します。以上で、必要な要素について抽出が完了しました。
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データを処理すると、今回の方法では非常に時間がかかることが想定されます。次の記事で、全件処理済みのデータを開示しますので、ここではお試し程度の件数で処理することをオススメします。
2. ソースコード
# -*- 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コード", "提出者業種"]]
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タクソノミの公表についてのタクソノミ要素リスト
と勘定科目リスト
から確認ができます。今回抽出した科目は、タクソノミ要素リスト(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の開発委員会の問合せ窓口になります。
そのため、組織に関する一般的な問合せなどは内容によって回答できかねますが、XBRLに関する技術的な質問、意見、要望、助言等はお気軽にご連絡ください。
なお、委員会メンバが有志で対応しているため、回答に時間がかかることもありますが、ご了承ください。