有価証券報告書(XBRL)の「従業員の状況」から従業員数や平均年収の数値を取得し整形する試み(その1)


1.概要

 EDINETで閲覧できる有価証券報告書には「従業員の状況」のページがあって、XBRLファイル的にはタグ「jpcrp_cor:InformationAboutEmployeesTextBlock」の箇所に記載があるのだが、タグの中身はHTMLの記述がずらずらと格納されていて、簡単に従業員数や平均年齢、平均勤続年数、平均給与が取得できない。

以下はたまたま見かけたバフェットコードさんのツイート↓

 上記ツイートに対して激しく同意というか、「大株主の状況」だけでなく「従業員の状況」も一応はXBRLのタグの定義があるにはあるのだが、タグの中身はHTMLの記載があって、何のためのXML形式やねんと思わずツッコミしたくなる気持ちになるのはアレ。で、XBRLのタグの中で従業員や大株主の状況の箇所は後回しに(というか敬遠?)して、分かりやすいところで売上高やら純資産など財務項目の数値系の取得から始めようと考えるのがありがちなパターン。

 ただ従業員数や平均年齢、平均勤続年数、平均給与の記載は、いろいろな企業の有価証券報告書を見てると大抵表(HTMLテーブル)で記載しているっぽいので、ひょっとしたらPandasのRead_HTML()でその箇所を簡単に取得できるのではないか?という仮説のもとに試作してみたのが以下スクリプト↓

2.試作

ex1.rb
import requests
from bs4 import BeautifulSoup
import os
import zipfile
import re 
import xml.etree.ElementTree as ET
import pandas as pd

def fn_xbrl(sic,url):
       #XBRLダウンロード
       fn = str(sic) +".zip"
       os.system("wget -O " + str(fn) + " " + str(url))

       # ZIP解凍
       with zipfile.ZipFile( str(fn), 'r' )  as myzip:
          infos = myzip.infolist()
          for info in infos:
              base, ext = os.path.splitext(info.filename)
              if  str(base).find('Public')>0 and ext == '.xbrl':
                  myzip.extract(info.filename)
                  # XBRLパース 
                  print('■' + info.filename)
                  dict = fn_parse(sic , info.filename)

def fn_srch(sic):
        # 有報キャッチャー検索
        url='http://resource.ufocatch.com/atom/edinet/query/'+str(sic)+'0'
        r = requests.get(url)
        soup = BeautifulSoup( r.text,'html.parser')
        enty = soup.find_all("entry")

        for i in enty:
              ttl=i.find_all("title")
              # 有報のXBRL
              if str(ttl).find('有価証券報告書')>-1and str(ttl).find('訂正')==-1:
                  lnks=i.find_all("link")
                  print(ttl)
                  print(lnks[0])
                  print(lnks[1])
                  url =lnks[1].get('href')
                  fn = fn_xbrl(sic, url) if url !=''else 'hoge'

def fn_parse(sic , xbrl_path):

    ### タクソノミーURLの取得(using beautiful Soup)
    ff = open( xbrl_path , "r" ,encoding="utf-8" ).read() 
    soup = BeautifulSoup( ff ,"html.parser")

    x = soup.find_all("xbrli:xbrl" )
    x = str(x)[0:2000] 
    x = x.rsplit("xmlns")

    # 該当attr(crp)の取得
    crp  =['{' + i.replace( i[0:11] ,"").rstrip(' ').replace('"',"") + '}' for i in x[1:10] if i[0:11] == ':jpcrp_cor=']
    dei  =['{' + i.replace( i[0:11] ,"").rstrip(' ').replace('"',"") + '}' for i in x[1:10] if i[0:11] == ':jpdei_cor=']
    crp,dei = crp[0] ,dei[0]

    #df=pd.DataFrame(index=[], columns=[])
    #df.to_csv('test', header=False)

    # XML項目の取得(using elementTree)
    tree = ET.parse( xbrl_path ) 
    root = tree.getroot() 

    for c1 in root:
          if c1.tag==crp+'InformationAboutEmployeesTextBlock':
             tx = str(c1.text)
             df = pd.read_html( tx ,flavor='bs4' )
             for i in df:
                  if i.iloc[0, 0].find('従業員数')>-1 and i.iloc[0, 1].find('平均年齢')>-1 :
                     print( i )

# main
if __name__ == '__main__':

      sic=1301
      fn_srch(sic)

とりあえず1301極洋の有価証券報告書でトライしてみた。以下は実行結果↓

なんとなく取れてはいるっぽい...

(あと7203トヨタ自動車と2121ミクシイで実験してみたが、これもうまくいった。
ただ7974任天堂と6757ソニーだとうまくいかなかった。エラーでコケますたorz)

3.課題

で、全件取得にトライしてみたのだが、案の定会社によって従業員の状況の記述(表の様式等)がバラバラなので、整形の部分でたまに想定してないエラーで処理が止まる会社がある。(上記の1301極洋は、たまたまうまくいったケースです)

以下に、今後の課題をつらつらと記す↓
1.XBRL読込処理と整形処理の分離
XBRLのタグの抽出処理と、整形および数値の取得処理は別にしたほうが良さげ。今までトライした中では、XBRLのタグの取得でエラーになってはいないので、最初に全社のタグ取得をしてから、後でゆっくり平均年収等の数値のクレンジング整形及び値取得の部分をやったほうがいいかも。

2.年月の取得について
いつの従業員データかが分からないので、タグ「jpdei_cor:CurrentPeriodEndDateDEI」などで年月を取得するのを忘れずに。

3.平均年収の単位について
上記の1301極洋は「円」で1円まで数値を記載しているが、会社によっては、万円単位の表記であったり、あるいは千円単位の表記であったりするので、テーブル表のヘッダー部分の「平均年間給与(円)」の文字列は忘れずに取得を。

4.平均勤続年数について
上記の1301極洋は小数点表記だが、他の会社で「X年Xヶ月」という記載をしているケースがある。後で小数点表記に変換する必要あり。

5.従業員数の取得について
従業員数は、「jpcrp_cor:InformationAboutEmployeesTextBlock」の中にある表内の数値を取ろうと思った際、派遣社員や臨時従業員の数値の記載などが会社によってバラバラで、それによって表の列が変わる(のでpandasでdfを生成した際にきれいに揃わない)といったケースがある。従業員数は過去数年の経営成績の表に記載があるので、そちらのXBRLタグ「jpcrp_cor:NumberOfEmployees」で取得したほうが良さそう。
   連結従業員数:contextRef='CurrentYearInstant'
   単独従業員数:contextRef='CurrentYearInstant_NonConsolidatedMember'
なお単独決算会社には、当然ながら連結従業員数のタグはないので、最初の変数設定等では要注意を。

3.EDINETコードと本社住所の取得を忘れずに?
東証HPにある上場会社一覧エクセルファイルには、証券コードと社名、業種コードと業種名は載っているので、XBRLファイルで「提出会社の名前」は取得する必要はない。しかし東証HPにあるエクセルファイルやバフェットコードのcsvダウンロードでは「EDINETコード」と「本社住所」は取得できないので、XBRLを読み込む時は、ついでに取得しておいたほうが、のちのち使うかもしれないので、要検討を。
   EDINETコード:jpdei_cor:EDINETCodeDEI
   提出会社の最寄りの連絡場所:jpcrp_cor:NearestPlaceOfContactCoverPage
(注:某ネットメディアの年収ランキング記事で「近畿地方の年収トップ500社(もしくはワースト500)社ランキングみたいなのを見かけるが。本社住所でセパレートする意味がさっぱり分からんと思いつつも、同じようなランキング記事のデータを嫌がらせ?で作ることを想定して、XBRLの「提出会社の最寄りの連絡場所」のタグの取得は検討の余地あり。でも、例えば大阪に本社があるキーエンスの東京支社勤務の敏腕営業マンの年収2000万超みたいなケースもあるから、本社住所で分類したソーティングには、ほぼ意味がないというかナンセンスだなーと思うので、やっぱないわー)

6.BeautifulSoupの利用について
今回PandasのRead_HTML()を使ってみたが、BeautifulSoupでスクレイピングして整形したほうがいいのかも。会社によっては、きれいにテーブルとしてRead_HTML()で読み込みできたりできなかったりする場合がある。

...続きます。
(注:断念した場合は続きません)