有報? 何それ? 美味しいの?

有価証券報告書、通称「有報」が、株式投資にどれだけ役に立つのかを検証したいと思います。


有報についての簡単な説明

「有報」と呼ばれるものがある。正確には「有価証券報告書」。「有価証券」とは、企業が発行している株式とか債券とかだが、ここでは主として株式。株式を発行している企業が、これを買ってくれた人たち、通常「投資家」と呼ばれる人たちのために、「今、うちの会社の状況はこんな感じです」と報告する書類が、「有価証券報告書」。投資家はこれを見て、株式の配当がちゃんと出るか、今後株価がどう動くだろうか、などを考える。株式を上場している企業は必ず出さなければならないと法律で決められた書類である。
この書類、多くの投資家が気にするのは数字ばかりであるが、実は文章も多い。この文章の中にも投資のヒントがあるのではないか、と思いつつ、長ったらしい、面白くもない内容である。こんなものをしっかり読み込めるのはウォーレン・バフェット氏ぐらいであろう。ということで、AIに読ませてみよう。もしかして、人がザッと呼んだぐらいではわからないことを、何か見つけてくれるかもしれない。

どの有報、どの会社にするか

有報の文章分析の方法はもちろんたくさんある。まず、どの有報を対象とするか。日本の上場企業約4,000社すべての有報を対象とすることも、やってやれないことはないだろうが、話が大きすぎて、漠然としたものしか出てこないような気がする。同一業種の中で企業間の違いを比べてみるのも興味深い。ただ、まずは手始めに、一つの企業について、数年分の変化を見比べるのが良いのではないか、そのほうがその企業の業績、株価との比較もできる、と考えた。
企業はアンリツにした。通信機器などの電子計測機器などの製造・販売を行う会社であるが、特に深い理由はない。年間の売上高は1,000億円程度、2022年9月末の株式時価総額は2,000億円ちょっとぐらいと、規模が大きすぎず、小さすぎずで、いいだろう。また事業内容があまり多岐にわたっていない会社、という程度の条件で、最初に目についたのがたまたまこの会社だった。

有報を手に入れてみた

さっそく始めよう。まずは、有報を手に入れるところから。金融庁が用意してくれているEDINETなるところから持ってくるのが王道、ということだが、これ、意外と使いづらい。第一に、書類管理番号(docid)を取得しなければいけない。しかもこれ、公表日ベースでしかとれない、らしい。つまり、ある1企業が出した有報、という選び方はできず、何年何月何日に公表された有報すべてのdocidをとってきて、そこから目指す企業のものを探し出す、ということになる。まあ、そこはコンピュータにやってもらえばさほどの手間ではない。最大5年分しかないのだが、公表日をいちいち調べるのも面倒なので、公開企業4,000社、全部で2万超のdocidを全部取得する。そのデータの中に、銘柄コードも入っているので、それだけを取り出す。アンリツの銘柄コードは6754。新たに発行される株式などではない、普通の株式ということで、最後に「0」をつけて5桁にする。

import datetime
import requests
import pandas as pd

start_date = datetime.date(2018, 4, 1)
end_date = datetime.date(2022,9,30)

period = end_date - start_date
period = int(period.days)
day_list = []
for d in range(period):
    day = start_date + datetime.timedelta(days=d)
    day_list.append(day)
    
day_list.append(end_date)

#結果を格納するための空のリストを用意
report_list =[]
#日付リストの期間に提出された書類のメタデータを取得してjson形式に変換
for day in day_list:
    url = "https://disclosure.edinet-fsa.go.jp/api/v1/documents.json"
    params = {"date": day, "type": 2}
    res = requests.get(url, params=params)
    try:
      json_data = res.json()

      for num in range(len(json_data['results'])):
          ordinance_code= json_data['results'][num]["ordinanceCode"]
          form_code= json_data['results'][num]["formCode"]
          
    #ordinance_code=010かつform_code=030000が有価証券報告書
          if ordinance_code == "010" and  form_code =="030000" :
              company_name=json_data["results"][num]["filerName"]
              edi={ '会社名':company_name,
                          '書類名':json_data["results"][num]["docDescription"],           
                          'docID':json_data["results"][num]["docID"],
                          '証券コード':json_data["results"][num]["secCode"],
                          '日付': day             }
              report_list.append(edi)
    except:
       continue

df = pd.DataFrame(report_list)
df

docid =df[df['証券コード'] == '67540']['docID']
docid

取得し終わるまで1時間半かかった。コードがわかったところで、有報を手に入れる。有報全部である必要は無く、文字情報だけでいい。

import requests
import zipfile
import pandas as pd
import itertools
from tqdm.notebook import tqdm

# codelist_df = pd.read_csv("/content/drive/MyDrive/YuhoEdiIDDenkikiki.csv")

docid_list = ['S100CY8A', 'S100FYRT', 'S100IW80', 'S100LJOA', 'S100OFC5']

#docid_list = codelist_df["CODE"].values.tolist()
# docid_list = itertools.chain.from_iterable(docid_list0)

filename_list = []

# print(codelist_df)
# print(docid_list0)
print(docid_list)

for docid in tqdm(list(docid_list)):
    # 書類取得APIのエンドポイント
    # print(docid)
    url = "https://disclosure.edinet-fsa.go.jp/api/v1/documents/" + docid
    
    # 書類取得APIのリクエストパラメータ
    params = {
        "type" : 1}
        
    # 出力ファイル名
    filename_list.append(docid + ".zip")

    # 書類取得APIの呼び出し
    res = requests.get(url, params=params, verify=False)
    # ファイルへ出力
    print(res.status_code)
    if res.status_code == 200:
        with open(docid+".zip", 'wb') as f:
            for chunk in res.iter_content(chunk_size=1024):
                f.write(chunk)

# zipファイル解凍
for i, docid in enumerate(docid_list):
    # print(filename_list[i])
    with zipfile.ZipFile(filename_list[i]) as zip_f:
      zip_f.extractall(docid)

#ライブラリーのインポート
from edinet_xbrl.edinet_xbrl_parser import EdinetXbrlParser
import glob
from bs4 import BeautifulSoup
import pandas as pd

#インスタンス化
parser = EdinetXbrlParser()

for _docid in tqdm(docid_list):
    #xbrlファイルのパスを指定
    xbrl_path = '/content/' + _docid + '/XBRL/PublicDoc/*.xbrl'
    print(i)
    xbrl_path = glob.glob(xbrl_path)[0]
    print(xbrl_path)
    edinet_xbrl_object = parser.parse_file(xbrl_path)
    
    #keyとcontext_refを指定。keyが経営方針と事業等のリスクでは異なる
    #経営方針、経営環境および対処すべき課題等
    key_houshin= 'jpcrp_cor:BusinessPolicyBusinessEnvironmentIssuesToAddressEtcTextBlock'
    
    # 事業等のリスク
    key_risk ='jpcrp_cor:BusinessRisksTextBlock'

    # context_refは経営方針と事業等のリスクともに共通
    context_ref='FilingDateInstant'
    # 例外処理
    
    try:
    
        data_houshin = edinet_xbrl_object.get_data_by_context_ref(key_houshin, context_ref).get_value()
        soup_houshin = BeautifulSoup(data_houshin,'html.parser').get_text(strip=True)
        
        data_risk = edinet_xbrl_object.get_data_by_context_ref(key_risk, context_ref).get_value()
        soup_risk = BeautifulSoup(data_risk,'html.parser').get_text(strip=True)
        


        tmp_se = pd.DataFrame([[_docid], [soup_houshin],[soup_risk]], index=df.columns).T
        df = df.append(tmp_se)
    
    except AttributeError as e:
        print(e)

df.to_csv("/content/drive/MyDrive/YuhoEdiID400Denkikiki.csv", index=False )

と、いうことで、手に入ったのだが、5年分というのはやはり少ないのではないか。過去5年のこの会社の業績をみると、好調な時から不調への変化はあった。しかし、もう5年、前にさかのぼると、不調から好調への変化の時期があった。その時、有報はどう変わったかも見てみたい。では、どうやってさらに5年分の有報を手に入れるか。スクレイピングでもできないことは無いのだろうが、結局、この会社のホームページから、一つ一つ手でPDFファイルをダウンロードした。このくらいの分量なら、これが一番速い。さらにPDFをtxtファイルにして、「経営方針」と、「事業等のリスク」の部分をコピペして、すでに入手している5年分と合わせ、10年分のデータファイルを作った。
ファイルは、dfyufo.csv という名前でMyDriveに保存。pandas で読み込むと、こんな形になっている。


「経営方針」と「事業等のリスク」の両方を分析した方がいいのだろうが、どうも10年間の途中で「経営方針」の方は書き方が変わっているみたいなので、今回は「事業等のリスク」だけを取り出して分析対象とする。

どんな言葉が使われているか調べてみた

いよいよ自然言語処理っぽいことをしてみる。まずは、必要そうなライブラリーなどのインストール。

# インストール

# 形態素分析ライブラリーMeCab と 辞書(mecab-ipadic-NEologd)のインストール 
!apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab > /dev/null
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git > /dev/null 
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n > /dev/null 2>&1
!pip install mecab-python3 > /dev/null

# シンボリックリンクによるエラー回避
!ln -s /etc/mecabrc /usr/local/etc/mecabrc

!echo `mecab-config --dicdir`"/mecab-ipadic-neologd"

!pip install janome
!apt-get -y install fonts-ipafont-gothic

!pip install mplfinance
!pip install japanize-matplotlib

それから、どんな言葉が多く使われているか、ワードクラウドを作ってみた。

2012年度   2015年度
2018年度   2021年度

しかし、、、あまりに当たり前の言葉ばかり出てくる。10年分、ほとんど変わりがない。「当社」なんて、普通の一人称だ。まあ、そりゃそうだ。業績に関する文章なんて、ほぼ型が決まっており、変化なんかほとんどない。「いつも出てくる単語」ばかりなんだから。そこで、ストップ・ワーズなるものを設定して、あまりにも「いつも出てくる単語」を省いてみた。その結果。ストップ・ワーズは、Stop_words.csv という名前で MyDrive に保存。

# ワードクラウド
from janome.tokenizer import Tokenizer
from wordcloud import WordCloud
import matplotlib.pyplot as plt 
import pandas as pd
import csv

t = Tokenizer()
nn = 0

with open('/content/drive/MyDrive/Stop_words.csv', encoding = "shift-jis") as f:
  reader = csv.reader(f)
  stop_words = [row for row in reader]

stop_words =stop_words[0]

data= pd.read_csv('/content/drive/MyDrive/dfyufo.csv')

texts = data['リスク'].to_list()


for text_file in texts:
  tokens = t.tokenize(text_file)

  word_list=[]
  for token in tokens:
      word = token.surface
      partOfSpeech = token.part_of_speech.split(',')[0]
      partOfSpeech2 = token.part_of_speech.split(',')[1]
      
      if partOfSpeech == "名詞":
          if (partOfSpeech2 != "非自立") and (partOfSpeech2 != "代名詞") and (partOfSpeech2 != "数"):
              word_list.append(word)
  
  words_wakati=" ".join(word_list)
  # print(words_wakati) 
  # print(stop_words) 
 
  fpath = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf'  # 日本語フォント指定

  save_filename = "/content/drive/MyDrive/word_cloudBB" + str(nn) + ".png"

  wordcloud = WordCloud(
      font_path=fpath,
      width=400, height=200,   # default width=400, height=200
      background_color="white",   # default=”black”
      stopwords=set(stop_words),
      max_words=200,   # default=200
      min_font_size=4,   #default=4
      colormap='Set3',  # 文字色のカラーマップ指定
      collocations = False   #default = True
      ).generate(words_wakati)
  
  plt.figure(figsize=(15,12))
  plt.imshow(wordcloud)
  plt.axis("off")
  # plt.savefig(save_filename)
  plt.show()

  nn = nn +1

2012年度   2015年度
2018年度   2021年度

もっと省くべき単語があるのかもしれないけれど、とりあえず、これで。

しかし、ワードクラウドは、視覚的に目を引くのだろうが、詳細を見るには向いていないような気がする。いくつかのキーワードに注目して、出る頻度の変化を調べてみる。キーワードは「リスク」、「経営」、「顧客」としてみた。単純に出る回数ではなく、単語総数に対する出現回数の比率を10年分見てみた。

# 単語出現回数
import MeCab
import pandas as pd
import csv
import collections
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import japanize_matplotlib

np.set_printoptions(precision=2)

m = MeCab.Tagger ('-Ochasen')

risuku = []
keiei = []
kokyaku = []

def count_word(textB):
  node = m.parseToNode(textB)

  words=[]
  while node:
    hinshi = node.feature.split(",")[0]
    if hinshi in ["名詞"]:
      origin = node.feature.split(",")[6]
      words.append(origin)
    node = node.next

  changed_words = [word for word in words if word not in stop_words]
  
  #単語の数カウント
  c = collections.Counter(changed_words)
  c_most_common = c.most_common(30)

  rrr = 0

  cmclist = list(c_most_common)
  for q in cmclist:
    # タプルで出てくるので、一つ一つリストにしなければならない、、、
    qq = list(q)
    cmclist[rrr] = qq
    rrr += 1
 
  rrrr = 0
  for mc in cmclist:
    rr = round(mc[1]*100/len(changed_words), 2)
    mc.append(rr)
    cmclist[rrrr] = mc
    rrrr += 1

  rrrr = 0
  for mc2 in cmclist:
    if mc2[0] == 'リスク':
      risuku.append(mc2[2])
    if mc2[0] == '経営':
      keiei.append(mc2[2])
    if mc2[0] == '顧客':
      kokyaku.append(mc2[2])

for textB in texts:
  count_word(textB)

fword = [risuku, keiei, kokyaku]
fwordj = ['リスク', '経営', '顧客']

ff = 0
for f in fword:
  # 表示するデータ
  Y = f
  X = data['FY']

  # グラフの大きさ指定
  plt.figure(figsize=(10, 5))

  # 棒グラフを中央寄せで表示
  plt.bar(X, Y, align="center", width=0.8)

  # グラフのラベル
  plt.xticks(X, data['FY'])

  # XとYのラベル
  plt.xlabel("決算期")
  plt.ylabel("出現比率 %")

  # タイトル表示
  plt.title(fwordj[ff], fontsize=15)
  ff += 1

  # グリッド線を表示
  plt.grid(True)

結果。

19年3月期までと、20年3月期からとで違いがある。
実は、頻出単語だけでなく、総単語量も変っていて、19年3月期までは400~450語(stop_wordsを除く)程度だったのが、20年3月期以降は1,200語前後と、3倍近くに膨らんでいる。

どんな言葉が重要なのか調べてみた

ただ数多く使われている、だけじゃなく、どの程度重要か、を見てみよう。tf-idfを試してみる。特定の文書中にのみ多く出現し、他の文書ではあまり出現しないような、 出現の分布に偏りのある単語の重要度が高くなるんだそうな。

# tf-idf
import numpy as np
from gensim import corpora
from sklearn.feature_extraction.text import TfidfVectorizer


# 表示するときに有効数字2桁で表示する
np.set_printoptions(precision=3)

docsL = []

def count_word(textB):
  node = m.parseToNode(textB)

  words=[]
  while node:
    hinshi = node.feature.split(",")[0]
    if hinshi in ["名詞"]:
      origin = node.feature.split(",")[6]
      words.append(origin)
    node = node.next

  changed_words = [word for word in words if word not in stop_words]
  changed_words2 = " ".join(changed_words)
  docsL.append(changed_words2)

for textB in texts:
  count_word(textB)

docsN = np.array(docsL)

vectorizer = TfidfVectorizer(use_idf=True, token_pattern="(?u)\\b\\w+\\b")
vecs = vectorizer.fit_transform(docsN)

# 列要素を取得
print(vectorizer.get_feature_names())

# tf-idf値を格納した行列を取得
vecstfidf = vecs.toarray()

print(vecstfidf.shape)

feature_names = vectorizer.get_feature_names()
# month_num = ['{0:02d}'.format(i) for i in range(1,11,1)]
df_score = pd.DataFrame(vecstfidf, columns = feature_names)

# print(df_score)

for i in range(0,10,1):
    # monthly_rank = []
    df_score_ = df_score[i:i+1].T
    df_score_sorted = df_score_.sort_values(i, ascending=False)
    print(df_score_sorted.head(10))

各年の上位10単語の結果はこんな感じ。(この表はExcelでつくりました)

tf-idf

年ごとにどの程度似ているのかを調べてみた

なんとなく、重要な言葉が変わってきているのはわかった。では、それらを合わせた文章全体では、それぞれの間に似ているとか、違っているとかあるだろうか。コサイン類似度を使ってみた。

# cos類似度

cosrl = []
# cosr = pd.DataFrame(columns=[X], index = [X]) # Xはdata['FY']

def cosine_similarity(v1, v2):
    cos_sim = np.dot(v1, v2)/(np.linalg.norm(v1)*np.linalg.norm(v2))
    return cos_sim

for i in vecstfidf:
  print()
  for j in vecstfidf:
    print(round(cosine_similarity(i, j),3))
    cosrl.append(round(cosine_similarity(i, j),3))

cosrn = np.array(cosrl).reshape(10,10)
cosr = pd.DataFrame(data=cosrn, index=X, columns=X, dtype='float')
print(cosr)
コサイン類似度

結論としては、まあ推測はできたことだが、13年3月期から19年3月期までのグループと、20年3月期から22年3月期までのグループにはっきり分かれた。

株価、業績はどんな動きだったか調べてみた

では、業績、株価とは関連があるのだろうか。
株価(週足)と四半期ベースの営業利益の推移は下図の通り。

アンリツ 株価推移

業績の傾向としては、
1)2012年度第1四半期~2016年度第1四半期:時々良かった時もありながら次第に低迷。
2)2016年度第2四半期~2019年度第4四半期:順調に回復
3)2020年度第1四半期~ :徐々に軟化
といった感じである。
株価の方は、2013年3月まで上昇、2016年10月まで下落、2021年1月まで上昇、その後下落、というトレンドである。株価の方がやや遅れているが、概ね営業利益と同じような動きといえるだろう。

まとめ

有報における「リスク」の文面を見ると、2012年度から2018年度までと、2019年度以降の二つの時期にしか分かれない。後の方がリスクに対する認識は強いが、1)から2)への変化は、有報には表れていない。2)から3)への変化について、1年ほど早めに認識され、有報での表現が変化した可能性はある。「リスク」なのであるから、悪いものが改善するより、好調なものが悪くなる際に警報を鳴らす方が適切とはいえる。業績の悪化を正確に見通して警告を出したのであれば、投資家にしてみれば利益の確定、または空売りのチャンスを提示しているといえるだろう。
もちろん、有報での書き方の変化の要因が、業績の見通しが変わったから、とは限らない。経営者の認識として、業績に関する懸念は全くないが、リスクについてもっと示すべし、という世間の流れに従っただけかもしれない。
これについては、同時期の同業他社、または異業種の動きなども調べてみる必要があるだろう。

感想

やはり、有報の文章部分はただの飾りではなく、しっかりと読み込む価値のあるものなのだろう。今回は1企業だけを取り上げたが、より多く見て行けば、さらにいろいろな情報が得られるだろう。特に、悪い状況からの好転の示唆が得られれば、株式投資としては非常に有用なものとなる。いくつかの分析手法の基礎を学んだので、それを応用し、分析を広げていきたい。






この記事が気に入ったらサポートをしてみませんか?