Day22 財務分析

・type2に指定して、EDINETにアクセスすると返ってくるのは以下になる。

{
 "metadata": {
     "title": "提出された書類を把握するための API", 
     "parameter":
      {
       "date": "2019-04-01", 
       "type": "2"
      }, 
     "resultset":
      {
       "count": 2
      },
     "processDateTime": "2019-04-01 13:01", 
     "status": "200",
     "message": "OK"
 }, 

"results": [
   {
     "seqNumber": 1,
     "docID": "S1000001",
     "edinetCode": "E10001",
     "secCode": "10000",
     "JCN": "6000012010023",
     "filerName": "エディネット株式会社", 
     "fundCode": "G00001", 
     "ordinanceCode": "030",
        "formCode": "04A000",
     "docTypeCode": "030",
     "periodStart": "2019-04-01",
     "periodEnd": "2020-03-31",
     "submitDateTime": "2019-04-01 12:34",
     "docDescription": "有価証券届出書(内国投資信託受益証券)",
       "issuerEdinetCode": null,
     "subjectEdinetCode": null,
   
     "subsidiaryEdinetCode": null, 
     "currentReportReason": null, 
     "parentDocID": null, 
     "opeDateTime": null, 
     "withdrawalStatus": "0", 
     "docInfoEditStatus": "0", 
     "disclosureStatus": "0", 
     "xbrlFlag": "1",
     "pdfFlag": "1", 
     "attachDocFlag": "1", 
     "englishDocFlag": "0"
   },
    {
     書類情報の繰り返し 
   }
]

のようにリストで与えられるから、それを

doc_list['result']

とかでとってくる。

Day21

@charset.setter
def charset(self, value):
    self._charset = value

 def serialize_headers(self):
        """HTTP headers as a bytestring."""
        def to_bytes(val, encoding):
            return val if isinstance(val, bytes) else val.encode(encoding)

        headers = [
            (to_bytes(key, 'ascii') + b': ' + to_bytes(value, 'latin-1'))
            for key, value in self.headers.items()
        ]
        return b'\r\n'.join(headers)

    __bytes__ = serialize_headers

def serialize_headers(self)の中に、この関数の中だけで使用する関数を書いている。
それがdef to_bytes(val, encoding)
・ return val if isinstance(val, bytes) else val.encode(encoding)がreturn のところに一行でif-else文をかけるのが凄い。

headers はリスト。その中で、上記で書いた関数を足し合わせている。リストの中に、リスト内包表記。
返り値がheaderのリストを改行して返す。

__があるのは特殊メソッド。よーわからん。このserialize_headersを特殊メソッドにしちゃうよって感じかな?わっかんね。

Pythonのif __name__ == "__main__" とは何ですか?への回答 - Python学習チャンネル by PyQ

プログラミングの学習プロセスの見直し

コンピュータの理解は深まったが、コードが書けるようになっているとはとても思えないので学習プロセスを見直す。


現状、コードのコピペを解説している。それをブログに掲載している。

プログラミングをする理由はプロダクトを作るため。
なので、言語の使い方がプロダクト作りに直結するやり方を模索してみる。


①コードをたくさん読む。そしてわからない箇所、使い方があれば解説する。
②理解してからコピペをする。(それを使用する場合。ブログはコピペでオーケー。)
③わからない使い方があったら徹底的にそれを解説しているコードを書いて、解説する。そして使えそうな具体例も想像でいいから考えてみる。
④0からコードを書く。ただしコメントから。そして小さい機能でよし。例えばファイルをオープンするなど。
⑤繰り返す。

これでやってみる。

Day20 django/http/response.py HttpResponseBase

class HttpResponseBase(------------):

@property
def reason_phrase(self):
    if self._reason_phrase is not None:
        return self._reason_phrase
    # Leave self._reason_phrase unset in order to use the default
    # reason phrase for status code.
    return responses.get(self.status_code, 'Unknown Status Code')

@propatyとは値を変更することはできないが、使うこと(読み取ること)はできるようにするものらしい。
if self._reason_phraseがあれば、その値を返す
そのままであれば、httpのresponseからstatus_codeをgetしてstatus_codeと 'Unknown Status Code'を返す。

@reason_phrase.setter
def reason_phrase(self, value):
    self._reason_phrase = value

新たな値を設定したいときにその手続きが行われる関数。上記のpropatyデコレータとセットで使用する。

@property
def charset(self):
    if self._charset is not None:
       return self._charset
    content_type = self.get('Content-Type', '')
    matched = _charset_from_content_type_re.search(content_type)
    if matched:
         # Extract the charset and strip its double quotes
         return matched['charset'].replace('"', '')
     return settings.DEFAULT_CHARSET

・charsetが存在したら、charseインスタンスを返す
content_typeはContet-Typeをとって代入する
machedにはcharset_from_content__のsearchで上記のcontent_typeを投入
もしmatchedしたらmatchedの'charset'の部分の""を''で置き換える。
そして何にも当てはまらなかったらdefault_charsetを返す。

pythonのプロパティのあれこれ - Qiita

Day19はdjango/response.pyのclass HttpResponseBase

class HttpResponseBase:
    """
    An HTTP response base class with dictionary-accessed headers.
    This class doesn't handle content. It should not be used directly.
    Use the HttpResponse and StreamingHttpResponse subclasses instead.
    """

    status_code = 200

・status_codeはhttpにおける状態のこと、200 OK。

    def __init__(self, content_type=None, status=None, reason=None, charset=None, headers=None):
        self.headers = ResponseHeaders(headers or {})
        self._charset = charset

・クラスにおけるインスタンスそのものについて書かれている。ResponseHeadersはこのクラスに書かれている上にあるクラスを用いている(この記事では表示していない)。その中に書かれているheadersか{}(辞書)を投入する。charsetはそのまま。

        if content_type and 'Content-Type' in self.headers:
            raise ValueError(
                "'headers' must not contain 'Content-Type' when the "
                "'content_type' parameter is provided."
            )

・content_typeと'Content-Type'が入っていれば、ValueErrorを表示する。中身は"--"

        if 'Content-Type' not in self.headers:
            if content_type is None:
                content_type = 'text/html; charset=%s' % self.charset
            self.headers['Content-Type'] = content_type
        self._resource_closers = []
        # This parameter is set by the handler. It's necessary to preserve the
        # historical behavior of request_finished.
        self._handler_class = None
        self.cookies = SimpleCookie()
        self.closed = False

・'Content-Type' がインスタンスのheadersになかったら、
  content-typeがNoneだったら、content-typeにtext/htmlとcharsetを入れ、
  それをheaderインスタンスの'Content-Type'にcontent_typeを入れる
self._resource_closersという空のリストを用意
self._handler_class = None
self.cookies = SimpleCookie()
self.closed = False
の各インスタンス自身を作成


        if status is not None:
            try:
                self.status_code = int(status)
            except (ValueError, TypeError):
                raise TypeError('HTTP status code must be an integer.')

もしstatusがなかったら
例外処理でself.status_codeに整数statusを代入
できなかったらTypeErrorを表示

            if not 100 <= self.status_code <= 599:
                raise ValueError('HTTP status code must be an integer from 100 to 599.')
        self._reason_phrase = reason

・status_codeが100以上599ではなかったら、ValueErrorを表示
・self._reason_phraseインスタンスを用意する

以上がHttpResponseBaseの読み。これがHttpResponseに使用される。明日はHttpResponseを読む。

class HttpResponseBase:
    """
    An HTTP response base class with dictionary-accessed headers.
    This class doesn't handle content. It should not be used directly.
    Use the HttpResponse and StreamingHttpResponse subclasses instead.
    """

    status_code = 200

    def __init__(self, content_type=None, status=None, reason=None, charset=None, headers=None):
        self.headers = ResponseHeaders(headers or {})
        self._charset = charset
        if content_type and 'Content-Type' in self.headers:
            raise ValueError(
                "'headers' must not contain 'Content-Type' when the "
                "'content_type' parameter is provided."
            )
        if 'Content-Type' not in self.headers:
            if content_type is None:
                content_type = 'text/html; charset=%s' % self.charset
            self.headers['Content-Type'] = content_type
        self._resource_closers = []
        # This parameter is set by the handler. It's necessary to preserve the
        # historical behavior of request_finished.
        self._handler_class = None
        self.cookies = SimpleCookie()
        self.closed = False
        if status is not None:
            try:
                self.status_code = int(status)
            except (ValueError, TypeError):
                raise TypeError('HTTP status code must be an integer.')

            if not 100 <= self.status_code <= 599:
                raise ValueError('HTTP status code must be an integer from 100 to 599.')
        self._reason_phrase = reason

習慣について

読書:
最初の空白のページになぜその本を読むのか理由を3つぐらい書く。
読みながらその本の全体像を図として書き出す。
最後によかった点3つとそのうち1つについて考えたことを後ろのページに書いとく。

以上!

Day18 財務分析

先人の方達が書いてくれたものを自分なりにまとめてみました。ありがとうございます。
以下は書かれているものを写経したものです。私が実践して得たものではなく、記事を読んだものをただただまとめただけです。
私はこれをもとに実践するつもりです。それではいきまそいきまそ。

EDINET APIを使用する財務分析は、単純な機能のプログラムの集まり。


証券コードリスト取得 → ②XBRL取得 → ③XBRLの読み込み → ④データ抽出(分量が多い) → ⑤グラフ化 → ⑥分析
XBRLの取得、XBRLの読み込み、勘定科目の抽出、グラフ化、時系列分析。


これらの機能を一つずつ小さく小さく作成していく。
ただXBRLを取得して、実際に銘柄分析に役立てるまでたどり着くのは時間がかかる。


XBRLの取得方法
XBRLとは財務情報や投資情報などの事業報告用の情報作成・流通・利用できるように標準化されたxmlベースの言語。

EDINETコードリスト(CSV形式)を使用してXBRLを取得する。
EDINETコードリストはZIP形式で圧縮されている。解凍すると、中身はCSV形式。文字のエンコーディングはcp932。文字化けする時はcp932を指定する。


① まずこのEDINETコードリスト(CSV)をpandasライブラリで読みこむ。

EDINETコードリストをpandasで読み込む方法
pandas.read_csv()で読み込む。
データは文字列で(str)として読み込むのが良い。
最初はいらない行なので1行分だけスキップ。
スキップしてから最初の行(0行目)がラベル行。

import pandas as pd
def get_edinet_codes_df(file):
    """EDINETコードリストのデータフレーム取得"""
    # データフレーム生成
    df = pd.read_csv(
        file,
        encoding='cp932',
        dtype='str',
        skiprows=1, # 最初の行をスキップ
        header=0,
        )
    return df


ここから上場企業の情報だけを抽出したりする。
以下抽出例。データフレームはリストに変換することもできる。

 # 上場企業だけを抽出
    df = df[df['上場区分']=='上場']

    # 必要な列だけ抽出
    df = df.loc[:, [
        'EDINETコード', '上場区分',
        '提出者名', '提出者名(ヨミ)', '提出者名(英字)',
        '提出者業種', '証券コード', '提出者法人番号',
        ]]

    # データフレームをリストに変換
    datas = df.values.tolist()


作成したデータフレームの保存と読み込みをする。データベース保存にも使える。
インデックス列も保存したいときは「index=None」を削除する。
CSV保存:pandas.DataFrame.to_csv()
CSV読み込み:pandas.read_csv()

    # データフレームをCSVで保存
    df.to_csv(symbols_csv, encoding='utf-8', index=None)

    # CSVから読み込み
    df = pd.read_csv(symbols_csv, encoding='utf-8')

②そしてこのコードリストを使用してXBRLを取得する。

サンプルコード:
SUMMARY_TYPE = 2
def download_all_documents(date, save_path, 
                           doc_type_codes=['120', '130', '140', '150', '160', '170']):
    params = {'date': date, 'type': SUMMARY_TYPE}
    doc_summary = get_submitted_summary(params)
    df_doc_summary = pd.DataFrame(doc_summary['results'])
    df_meta = pd.DataFrame(doc_summary['metadata'])
     
    # 対象とする報告書のみ抽出
    if len(df_doc_summary) >= 1:
        df_doc_summary = df_doc_summary.loc[df_doc_summary['docTypeCode'].isin(doc_type_codes)]
 
        # 一覧を保存
        if not os.path.exists(save_path + date):
            os.makedirs(save_path + date)
        df_doc_summary.to_csv(save_path + date + '/doc_summary.csv')
 
        # 書類をを保存
        for _, doc in df_doc_summary.iterrows():
            download_document(doc['docID'], save_path + date + '/')
            open_zip_file(doc['docID'], save_path + date + '/')
        return df_doc_summary
     
    return df_doc_summary 


XBRLを読み込み方。zipファイルをそのまま読み込む方法、beautiful soupで読み込む方法、lxmlで読み込む方法の三つぐらいあるらしい。

今回はzipファイルをそのまま読み込む方法をとる。
XBRLのファイル群はzipでひとつにまとめて公開されている。
これは解凍せずに読み込むことができる。
Python のzipモジュールにXBRLのzipファイルパスを渡すと読み込む事ができる。
そうするとバイナリデータが返ってくるので、それをそのままBeautifulSoup()やlxmlに渡せば解析してくれる。

zipを展開せずに直接読み込む方法[python, XBRL]
つまり、Python で zipを解凍せずに直接読み込む方法

zipファイルの中身を読み込む時は、いったんHDD か SSD に解凍(展開)するのが一般的であるが、zipから直接読み込むこともできる。

やり方は、「標準のzipfileモジュール」を使用する。
zipの中身を解凍しなくて良いため、大量のXBRLを読み込む時に、容量を省く事ができる。

zipfileモジュールでzipファイルを開いたら、
中身のファイルリストを取得するメソッド「.infolist()」と
データを読み込むメソッド「.read()」を使用する。

使い方:

zipファイルを開く:class zipfile.ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True)

中身のファイルリストを取得:ZipFile.infolist()

名前を指定して読み込む:ZipFile.read(name, pwd=None)


zipを読み込む
zipを解凍せずに中身を見るには、zipfileモジュールのZipFile()を使用する

読み込み手順
拡張子「.xbrl」のファイルだけを読み込んで辞書に入れる例。
zipfile.ZipFile()でzipファイルを開いてから、zip_data.infolist()でファイルリストを取得する。

ファイルリストの要素にはinfo.filenameという属性があるので、これをzip_data.read()に渡すと、データが読み込める。

info.filenameは、実際には以下のような「ファイル構造を含んだファイルパス」

'S100AM8K/XBRL/PublicDoc/jpcrp030000-asr-001_E00518-000_2017-03-31_01_2017-06-26.xbrl'

そのため、ファイル名を使用して読み込む・読み込まないを判定する時は、このファイルパスを意識して
判定部分を作ることになる。

コード例
まずzipfileモジュールを使用する部分

import zipfile
from collections import OrderedDict
import traceback

def read_file_from_zip(zip_file, re_match):
    """zipからファイル名を指定して読み込む"""
    file_datas = OrderedDict()

    try:
        with zipfile.ZipFile(zip_file, 'r') as zip_data:
            # ファイルリスト取得
            infos = zip_data.infolist()

            for info in infos:
                # ファイルパスでスキップ判定
                if re_match(info.filename) is None:
                    continue

                # zipからファイルデータを読み込む
                file_data = zip_data.read(info.filename)

                # ファイルパスをキーにして辞書に入れる
                file_datas[info.filename] = file_data

    except zipfile.BadZipFile:
        print(traceback.format_exc())

    return file_datas

次に、上記の関数を使用する部分。
メイン関数。
zipから読み込んだデータは、そのままlxml.etree.fromstring()に渡す事ができる。
データの中身はバイナリデータ。

import re
from lxml.etree import fromstring as etree_fromstring

def main():
    """メイン関数"""
    # zipファイルの場所
    zip_file = r'*****\S100AM8K.zip'

    # 拡張子でファイルを選択してみる
    re_match = re.compile(r'^.*?\.xbrl$').match

    # zipからファイルデータを読み込む
    files = read_file_from_zip(zip_file, re_match)

    # 読み込んだXBRLファイルを1つずつ解析
    for (file, data) in files.items():
        # ファイル名
        print('file: %s' % file)

        # XML解析 lxml.etree
        root = etree_fromstring(data)
        print('root.tag: %s' % root.tag)

        print('')
    return

そしてメイン関数を呼び出す。
if __name__=='__main__':
    main()

zipから読み込んだXMLデータをBeautifulSoupで読み込む場合。
バイナリデータをそのままわたす。単に文字列に変換したい場合は、適当なエンコーディングを指定して『.decode()』のメソッドを呼び出す。
バイトオーダーマーク (BOM) 付きの UTF-8 をデコードするときは、'utf-8-sig' を指定する。
'utf-8' でデコードしたら、先頭にパイトオーダーマークの文字列が残ってしまう。

from bs4 import BeautifulSoup

# XML解析 BeautifulSoup(HTMLパーサー)
soup = BeautifulSoup(data, features='lxml')

# XML解析 BeautifulSoup(XMLパーサー)
soup = BeautifulSoup(data, features='xml')

# バイナリをデコードして文字列に変換
text = data.decode('utf-8-sig')

参考:https://srbrnote.work/archives/1297





その勘定科目の抽出には2種類ある。

XBRLを一気に読み込んでから抽出するアプローチ。(今回はこっち)
XBRLを解析しながら狙った科目だけ抽出するアプローチ

XBRLを一気に読み込んでCSVに保存しておくと、内容を一望でき、タグを探すのが楽になる。
CSVは企業ごとに全ての決算を連結していったものを作成し、
これを一つ開くだけでその企業の何年分というタグを一覧する事ができる。

そして、「タグの読み込み」と「勘定科目の抽出」を分けたら、プログラムの分割がうまくいく。

PythonXBRLの読み込みに専念して、時系列分析のための表作成はpandasデータフレームの機能に任せる。
できる人であれば、専用のデータベースに取り込んで、時系列をリクエストするのが良い。


参考:https://srbrnote.work/archives/1100
参考:https://srbrnote.work/archives/1136
参考:https://srbrnote.work/archives/1123


あとは抽出してグラフ化するだけ。