LoginSignup
5
4

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-29

1.整形処理のスクリプト(改

前回の続き。上場会社全銘柄を1301から9997まで通しで実行して、金融庁に提出された有価証券報告書のXBRLファイル(feat.有報キャッチャー)から「従業員の状況」の箇所の文章を読み込んで保存したcsvファイルを元に、平均年間給与の数値等の抽出および整形を試みた。

以下は、前回のスクリプトを少し改良したもの↓

cleanse.py
!pip install mojimoji

##########################

import pandas as pd

import requests
from bs4 import BeautifulSoup
import unicodedata
import re
import mojimoji


fn = 'emp1000.csv'
df = pd.read_csv(fn , header=None, error_bad_lines=False)
lst_f  = []

def fn_modvle1(s):
    ptn = '[\[[\(\(\)\)\〔\〕\)]'
    if str(s).find('円')>-1:
       # 単位の整形
       s = re.sub(ptn ,"", str(s) )
       s = s.strip()
       s =str(s).replace('平均年間給与','')

    if str(s).find('_')>-1:  s = s.replace('_','',2).replace('\u3000','',2) 
    if str(s).find('、')>-1:  s = s.replace('、','') 

    s = re.sub('[\u3000|\xa0|\n|,]' ,"", str(s) )      
    return s

def fn_modvle2(s):


    if str(s).find('月')>-1:
      if re.search(r'[年|才|歳]', str(s))is not None:

          # 平均年齢と平均勤続年数の整形
          s = mojimoji.zen_to_han( s )
          obj = re.search(r'[年|才|歳]', str(s))
          u = obj.start() 
          if u>0: v = s[u+1:] 
          if u>0: e = s[:u].strip() 
          v = re.sub('[ヵ|か|カ|ケ|ヶ|カ|ケ|月|年|才|歳]' ,"", str(v) )
          ####s = e
          e,v = int(e), int(v)
          w = e + round(v/12,1) #小数部分を10進法に変換
          s = s +'/'+ str(w)
    elif re.search(r'[年|才|歳]', str(s))is not None:
       s = s +'/'+ re.sub('[年|才|歳]' ,"", str(s) )
    return s


def fn_cleanse_emp(k):

      lst , i = [] , 0 

      txt=df.iloc[k,3]
      sic , ecode =df.iloc[k,1] , df.iloc[k,5]
      ed , adrs =df.iloc[k,4] , df.iloc[k,6].replace('\n','_')
      ####emp_nc , emp_c =df.iloc[k,7] , df.iloc[k,8]
      #
      emp_nc =df.iloc[k,7]
      pdf=df.iloc[k,2]

      tds = []
      soup = BeautifulSoup( txt ,"html.parser")
      tbls = soup.find_all("table")

      #「給与」の文字が記載されているテーブルの特定
      for tbl in tbls:
            if str(tbl).find('給与')>-1 and str(tbl).find('勤続')>-1:
                tds = tbl.find_all("td")

      l=len(tds)
      if l>0:
         for td in tds: 
            #「給与」の文字が記載されているtdタグの特定
            if str(td).find('給与')>-1 :
                u = 8 if l-i >=8 else l-i 

                #年間平均給与/平均勤続年数/平均年齢等の値の取得
                for j in range(1,u):
                      f1 = tds[i+j].text
                      if f1.find('\n')>-1: f1=f1.replace('\n','')
                      if f1.find('\u3000')>-1: f1=f1.replace('\u3000','')
                      if f1.find('\xa0')>-1: f1=f1.replace('\xa0','')
                      if f1.find(',')>-1: f1=f1.replace(',','')
                      #print(f1 )

                      ecd=str(f1).encode('unicode-escape')
                      if str(ecd).find('3014')==-1 and str(f1).find('[')==-1 and str(f1).find('〔')==-1 and str(f1).find('(')==-1 :
                         if str(f1).find('(')==-1 and str(f1).find('[')==-1 :lst.append( fn_modvle2(f1))

                #年間平均給与の単位をセット
                f1=td.text.split('給与')[1]
                if f1.find('円')>-1: f1=f1[:f1.find('円')+1]
                f1=f1.replace('\n','')
                #print(f1)

                x = lst.append( fn_modvle1(f1) ) 

            i=i+1
      else:           
        x = [lst.append('nan') for j in range(3)]

      #リストを左右逆転させる
      lst=list(reversed(lst))

      if len(lst)<4: lst.insert(2,'')
      if lst[0]=='' and lst[1].find('円')>-1: lst[0]=re.sub('[0-9]','',lst[1])
      if lst[1].find('円')>-1: lst[1]=re.sub('[^0-9]','',lst[1])
      #lst_f = [k,sic ,ed,pdf, emp_nc,emp_c,fn_modvle1( adrs ) ] + lst 
      lst_f = [k,sic ,ed,pdf, emp_nc, fn_modvle1( adrs ) ] + lst 

      if lst_f[-1]==emp_nc :lst_f[-1] =''

      print(lst_f) 

# main
if __name__ == '__main__':

    st,ed=0 ,len(df) #rec_id

    #fn_cleanse_emp( 1089 ) #レコードIDを指定 
    x=[fn_cleanse_emp(k) for k in range(st ,ed)] 

上記スクリプトの実行結果
epm9.png
※Google Colaboratoryで上記スクリプトを実行

上記スクリプトの前回からの主な修正点
・臨時従業員の数値は今回は不要なので、カッコの含まれている項目はappend()で取得しないようにした。
・平均年間給与の単位(円、千円、百万円)の前後のカッコは不要なので、append()で取得する際に別途ファンクションfn_modvleX()にて除去処理をしている。
・平均年齢と平均勤続年数は、「○○年XXヶ月」であったり「「○○歳XXカ月」というような表記があるので、漢字部分を除去しつつ、小数点の部分を抽出して、12進法表記を10進法に変換(12で割る)した上で、小数表記(XX.X)に直している(上記では変換が妥当かどうか確認するために、置き換えではなく追記している)
・平均年齢と平均勤続年数の12進法表記の「XXヶ月」は、いろんなパターンがあって、「1ヶ月」「1ヵ月」「1か月」「1カ月」「1ケ月」「1カ月」「1ケ月」、それと「1月」などの表記が見られた。これらはパターンが多いので、正規表現でsub()で一括除去している。
・文字列検索(find()search())と文字列置換(replace()sub())を雑然と実行していて、まだまだ洗練されていないのは自分でも承知していて、もっときれいに文字列検索と文字列置換ができるはず。
・文字列検索と文字列置換の関数を色々と試してみて分かったのは、正規表現との関係性は以下のイメージかと↓

文字列検索 文字列置換    正規表現利用可能有無
find() replace() できない
search() sub() できる

・python初心者の自分にとってはfind()replace()がパッと頭に浮かび、思わずreplace()を多用しがちなのだが、正規表現が使えるsearch()sub()を使いこなせるようになると、上記スクリプトはもっとすっきりした記述になるかと思われ。
・平均年間給与の単位の部分が''もしくは'円'となっていて、平均年間給与の数値が「5000000円」となっているレコードがあるので、リストで平均年間給与の単位と平均年間給与の額を確認して、平均年間給与の額の箇所に記載のある単位を平均年間給与の単位の箇所に再セットして、平均年間給与の箇所に記載のある単位は除去している。
・連結従業員数の項目は今回は割愛した。最悪バフェット・コードで従業員数は閲覧取得できるので、今回は平均年間給与と平均勤続年数、平均年齢に注力。
・有価証券報告書の提出者の最寄りの連絡場所(≒本社住所)は、本社住所を移転しました的な文章を載せているケースが数件確認できた。一応邪魔にならないように「、」を除去している。

2.従業員の表の様式についての特殊?なパターン例(抜粋)

(1)従業員の表がないケース
例)2702:日本マクドナルドHD
emp1.png
 持株会社で従業員がいなくて、数値の記載がないケースはありますよね。(実際の持株会社の総務や経理部門の処理はホールディングス子会社の総務部門や経理部門にかけもちで処理させて、持株会社は役員のみ置いて従業員置かない的なイメージ?)
 上記スクリプトでは、tableのtdタブをbeautifulsoupで読みこんで取得が0件でなかった場合のみ整形処理をするようにして、0件の場合は処理しないようにした。

(2)従業員の表が変則的のケース
a)従業員の表にセグメントの名称の列があるパターン
例)5013:ユシロ化学工業_2018年3月期有報
emp2.png
 その会社の主要なセグメント、もしくは主要な地域(日本)における項目を記載しているパターン。このパターンの会社は他にも何社かあったのだけれども、上記のスクリプトでは普通に項目の取得はできる。が、主要なセグメントや地域の記載であって全社の項目値ではないので、値をどう扱うかは要検討。

b)縦形式の表で記載しているパターン
例)7267:ホンダ_2018年3月期有報
emp3.png
前年度と今年度の数値を表現するために縦形式の表で従業員数等を記載しているパターン。これは上記のスクリプトでは想定外なので、狙った項目を取得できない。ただ縦形式の表で記載している会社は、ホンダ以外なかったので、後で有報のpdf見て追記修正するでもいいかも。

c)平均年齢と平均勤続年数が整数部分と小数点部分と列が分離しているパターン
例)4912:ライオン_2017年12月期有報
emp4.png
 平均年齢と平均勤続年数の項目で、整数部分(歳/年)と小数点部分(月)が別の列になっているパターン。
さもありなむ。

(5)複数の項目・分類に分けて記載しているケース
 部署毎だったり、複数の事業毎だったり、雇用体系の異なる職種毎だったりと、複数に分けて表記しているパターン。上記スクリプトを実行すると、日本語の項目をそのまま取得しに行くので気づくのだが、こういうパターンは上記スクリプトでは処理できないので、後で人間が有報pdfを見て再確認する必要がある。
 抜粋してみたところ以下のような会社が分類分けをして複数表記している模様。(そんな多い訳ではない)↓ 

証券コード 社名    memo
1380 秋川牧園 「月給社員」と「日給社員」
1381 アクシーズ 「社員」と「従業員」
2162 NMSホールディングス 「一般社員」と「合計又は平均」
2398 ツクイ 「常勤従業員」と「非常勤従業員」と「合計」
2406 アルテサロンHD 「本部」と「合計」
2427 アウトソーシング 「内勤社員」と「外勤社員」と「合計又は平均」
2831 はごろもフーズ 「職員」と「月業員」と「合計又は平均」
4290 プレステージ・インターナショナル  「社員」と「地域限定社員」と「契約社員」
7181 かんぽ生命保険 「内務職員」と「営業職員」
7211 三菱自動車工業 「事務技術系」と「技能系」
7270 SUBARU 「男性」と「女性」と「合計」 ※2018年3月期
8377 ほくほくフィナンシャルG HDと傘下子会社銀行の2表あり
9009 京成電鉄 従業員数の列が「運輸」「不動産」「全社共通」「合計」の4列あり
9104 商船三井 「陸上従業員」と「海上従業員」と「合計」
9127 玉井商船 「陸上従業員」と「海上従業員」と「合計」
9206 スターフライヤー 「一般従業員」と「運航乗務員」と「客室乗務員」
9962 ミスミグループ本社 「正社員」と「有期雇用社員」

(6)外国会社など
9399:ビートホールディングス・リミテッド(旧新華ファイナンス)
→ 有報は提出している模様。だが従業員の項目がないので取得あたわず。
4875:メディシノバ・インク
→ 有報は提出している模様。だが、取得スクリプトがこの銘柄でエラーで止まった。
8421:信金中央金庫
→ 有報は提出している模様。信金中央金庫は、東証のエクセルファイルで、sectorCode列の値が「-」となっているので、銘柄一覧から除外されており、XBRLファイルを読みに行っていない。
8301:日本銀行
→ 有報キャッチャーでは検索すらできず。なお日本銀行は、東証のエクセルファイル上では、sectorCode列の値が「-」となっているので、銘柄一覧から除外されており、XBRLファイルを読みに行っていない。

バフェット・コードでもこれらの銘柄は対象外にしているので、まあ項目値が取得できなくても良しとしますか。

(5)記載ミスのパターン
2291:福留ハム_2016年3月期有報、平均年間給与の単位
emp6.png
「4,878百万円」って...自分もこれくらい(48億円)お給料欲しいものです(違

(6)従業員の表の数字が変則的なケース
a)3536:アクサスHD_2016年8月期有報、平均勤続年数
emp5.png
 「4.3ヶ月」は「4年3ヶ月」とも「[4.3]ヶ月(≒4ヶ月ちょい)」ともどちらにも意味が取れる可能性が...一応経年比較すると翌年と翌々年が7年くらいなので、おそらく「4年3ヶ月」でしょうね。。。

b)4217:日立化成_2018年3月期有報
従業員の表の数値がすべて、全角数字(カンマや小数点もすべて全角)になっている。
emp7.png
...堪忍してつかぁさい(泣
ってか、全角を半角に直すためにライブラリ「mojimoji」を入れて変換かましているつもりだったのだけれども、変換できていないっすね。
(なお全項目を全角で表記しているのは、上場企業ではこの会社だけでしたです、はい)

3.pandasでmap()と無名関数lamba()を利用する

test.py
#pandasでmap()とlamba()を利用する練習
import pandas as pd

def fn_hoge(x): 
    return x*200

fn_test = lambda x: fn_hoge(x)

df1 = pd.DataFrame([['A','B','C'],[25,10,55],[40,15,-15]]).T
df2 = df1[1].map(fn_test)


df0 = pd.concat([df1,df2], axis=1)
#df[ df.columns[df.columns!='not_this_column'] ]
df0

 今回はリスト内包表記でリスト内でforループを記述して、上から1レコードずつ値をfn_cleanse_emp()というファンクションを実行して整形処理をしているが、上記のスクリプト(test.py)のようにpandasのdataframeの各項目に対してmap()applymap()などの高階関数を使って、無名関数lamda()を介してfn_cleanse_emp()を実行する形でも処理は可能ですね。
(リスト内包表記で記述した後に、map()lamda()を組み合わせた書き方があるのだということを発見したのと、今回扱うデータの量がそれほど多くないため処理速度がそれほど変わらなかったというのがあって、今回はリスト内包表記でループ処理をする書き方で通しました。次に機会があるときに、map()lamda()を使った書き方を試してみようかと思います。)

botu.py
    #i=100
    #fn_test = lambda x: fn_cleanse_emp2(x)
    #df[2] = df[3].head(i).map(fn_test)
    #df2 = pd.DataFrame(sr)
    #x = df.head(i) 

    #print(x)


4.[おまけ]未上場で有価証券報告書を提出している会社の平均年間給与について

private_company.py
import pandas as pd

def fn_foo():

    ### 未上場の有報提出会社
    df2 = pd.DataFrame([['E00738','E00718','E00699','E00698','E00697','E00169','E22559'
                     ,'E00141','E04379','E04396','E00706','E25321','E04413','E21951']
                     ,['日本経済新聞社','朝日新聞社','東洋経済新聞社','神戸新聞社','西日本新聞社'
                     ,'竹中工務店','サントリーHD','鴻池組','毎日放送','山陽放送'
                     ,'毎日新聞社','毎日新聞GH','東海テレビ','商工中金'
                     ]]).T

    print( df2 )

fn_foo()   

 有報キャッチャーは上場会社以外の未上場会社でEDINETに書類提出している会社も検索可能なので、未上場会社で有価証券報告書を提出している会社を抜粋(主に平均年収の高そうな規制産業?のマスメディアなどを狙い撃ち)してスクリプトを実行して、平均年間給与の値等の取得を試みた。

以下は、スクリプトを実行して取得したデータ↓

会社名 Edinetコード 決算期末 従業員数(単独) 従業員数(連結) 本社住所 単位 平均年間給与 平均年齢 平均勤続年数
日本経済新聞社 E00738 2017-12-31 2497 9406 東京都千代田区大手町一丁目3番7号 12216296 19年2ヵ月/19.2 43歳5ヵ月/43.4
日本経済新聞社 E00738 2016-12-31 2518 9413 東京都千代田区大手町一丁目3番7号 12549431 19年3ヶ月/19.2 43歳4ヶ月/43.3
日本経済新聞社 E00738 2015-12-31 2500 9411 東京都千代田区大手町一丁目3番7号 12626731 19年1ヶ月/19.1 43歳2ヶ月/43.2
朝日新聞社 E00718 2018-03-31 3933 7449 東京都中央区築地五丁目3番2号 12082396 20.5 44.7
朝日新聞社 E00718 2017-03-31 3948 7371 東京都中央区築地五丁目3番2号 12139686 20.2 44.4
朝日新聞社 E00718 2016-03-31 4178 7605 東京都中央区築地五丁目3番2号 12442844 20.2 44.3
東洋経済新聞社 E00699 2017-09-30 274 None 東京都中央区日本橋本石町一丁目2番1号 11736340 16.3 43.6
東洋経済新聞社 E00699 2016-09-30 269 None 東京都中央区日本橋本石町一丁目2番1号 11835523 16.4 44.0
神戸新聞社 E00698 2017-11-30 485 1355 神戸市中央区東川崎町一丁目5番7号 千円 7742 18.7 43.8
神戸新聞社 E00698 2016-11-30 486 1363 神戸市中央区東川崎町一丁目5番7号 千円 7672 18.0 43.6
神戸新聞社 E00698 2015-11-30 480 1345 神戸市中央区東川崎町一丁目5番7号 千円 7570 17.5 43.2
西日本新聞社 E00697 2018-03-31 667 1548 福岡市中央区天神一丁目4番1号 8864719 18.13 45.54
西日本新聞社 E00697 2017-03-31 719 1600 福岡市中央区天神一丁目4番1号 8740882 17.51 45.21
西日本新聞社 E00697 2016-03-31 742 1623 福岡市中央区天神一丁目4番1号 8616920 17.26 44.72
竹中工務店 E00169 2017-12-31 7400 12982 大阪市中央区本町四丁目1番13号 10013993 19.2 44.0
竹中工務店 E00169 2016-12-31 7307 12592 大阪市中央区本町四丁目1番13号 9569442 19.6 44.3
竹中工務店 E00169 2015-12-31 7195 12328 大阪市中央区本町四丁目1番13号 9211209 19.8 44.4
サントリーHD E22559 2017-12-31 449 37745 大阪市北区堂島浜二丁目1番40号 11266704 18.2 43.4
サントリーHD E22559 2016-12-31 438 38013 大阪市北区堂島浜二丁目1番40号 10657132 17.9 43.0
サントリーHD E22559 2015-12-31 442 42081 大阪市北区堂島浜二丁目1番40号 10407404 17.6 42.8
MBSメディアホールディングス E04379 2017-03-31 627 906 大阪市北区茶屋町17番1号 千円 13448 19.3 43.7
MBSメディアホールディングス E04379 2016-03-31 636 907 大阪市北区茶屋町17番1号 千円 13211 19.1 43.6
山陽放送 E04396 2018-03-31 144 268 岡山市北区丸の内二丁目1番3号 千円 8760 17.0 43.0
山陽放送 E04396 2017-03-31 149 268 岡山市北区丸の内二丁目1番3号 千円 8493 17.1 43.5
山陽放送 E04396 2016-03-31 143 260 岡山市北区丸の内二丁目1番3号 千円 8595 17.4 43.9
商工中金 E21951 2018-03-31 3765 4083 東京都中央区八重洲二丁目10番17号 千円 7830 16.4 39.3
商工中金 E21951 2017-03-31 3753 4080 東京都中央区八重洲二丁目10番17号 千円 7879 16.4 39.3
商工中金 E21951 2016-03-31 3773 4102 東京都中央区八重洲二丁目10番17号 千円 7900 16.8 39.7

 鴻池組や東海テレビなど一部取得できない銘柄があったが、これら銘柄は以前に金融庁に書類を提出していてEDINETコードは付与されているものの、最近は有価証券報告書を提出していないので取得できないというパターンで取れていないものである。あと毎日放送は、社名変更していて、現在はMBSメディアホールディングスとなっている模様。
 それにしても平均年間給与、たかっ!(驚

 某ネットメディアが「自分以外の会社員がどれだけお給料をもらっているのか知りたい」という人々の潜在的欲求につけ込んで?PV稼ぐ目的で頻繁に配信している年収ランキングの記事などは、上場企業限定でソーティングしているので、上場していない会社の年収はランキングには載らないのだが...

※例の年収ランキング記事のコメント欄↓
mijojo.png
 仮に上記のスクリプトで得られた未上場企業の年収データを、例の年収ランキング記事のランキング表に脳内で「UNION ALL」で連結して再ソーティングしてみると、例えば東京都トップ500社ランキングだと、このランキング記事を配信している運営会社や、東京の築地に本社がある某新聞社や大手町の経団連ビルの隣に本社がある某経済新聞社などは、確実に上位にランクインしますよね(白目

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4