keisukeのブログ

***乱雑です!自分用のメモです!*** 統計や機械学習の勉強と、読み物を書く練習と、備忘録用のブログ

webページからデータを引き抜いて時系列プロット【yahooファイナンス】

時系列プロットの基本のための練習。
時系列プロットなら株価の動きが定番かなと思い、ターゲットをそれに決定。

ということで、Yahooファイナンスのページから日経平均株価のデータを引き抜いてプロットします。
ちょっと長ったらしいコードですが:

import sys
import urllib.request, urllib.error
import bs4
import datetime
import matplotlib.pyplot as plt
import re
import numpy as np

NOF_ITEMS_PER_PAGE = 50


def main(sy, sm, sd, ey, em, ed, nof_ticks=13, figurename=None):
    # create url
    # url = 'http://info.finance.yahoo.co.jp/history/?code=998407.O&sy=2014&sm=2&sd=13&ey=2014&em=3&ed=15&tm=d'
    url = 'http://info.finance.yahoo.co.jp/history/?code=998407.O&sy=%d&sm=%d&sd=%d&ey=%d&em=%d&ed=%d&tm=d' % (sy,sm,sd,ey,em,ed)
    if figurename is None:
        figurename = '%02d%02d%02d-%02d%02d%02d.png' % (sy,sm,sd,ey,em,ed)

    # create array
    # data = [date_list, start_price_list, max_price_list, min_price_list, end_price_list]
    data = create_array(url)

    # plot
    line_names = ['start', 'max', 'min', 'end']
    plot_data(data, figurename, line_names, nof_ticks)


def plot_data(data, figurename, line_names, nof_ticks=13):
    plt.figure()
    plt.hold(True)
    # for line in data[1:]:
    #     plt.plot(range(len(line)), line)

    # plt.legend(line_names, loc='best')
    # plt.xlim((0, len(data[0])))
    start_price = np.array(data[1])
    max_price = np.array(data[2])
    min_price = np.array(data[3])
    end_price = np.array(data[4])

    ave_price = (max_price+min_price)/2

    plt.errorbar(range(len(ave_price)), ave_price, yerr=np.vstack((max_price-ave_price, ave_price-min_price)), fmt=None, capsize=0)
    plt.plot(range(len(ave_price)), ave_price, color='r')

    if nof_ticks > 31:  # too long
        nof_ticks = 31
    interval = len(data[0]) // (nof_ticks-1)
    ticks = range(0, len(data[0]), interval)
    plt.xticks(ticks, [data[0][i].strftime('%Y/%m/%d') for i in ticks], rotation='vertical')
    plt.tight_layout()
    plt.grid()

    plt.savefig(figurename)


def create_array(url):
    req = urllib.request.Request(url)
    try:
        res = urllib.request.urlopen(req)
    except urllib.error.URLError as e:
        print(e)
        sys.exit(1)
    html_doc = res.read()
    soup = bs4.BeautifulSoup(html_doc)

    # get the number of items
    nof_items_string = soup.find('span', 'stocksHistoryPageing yjS').string
    nof_items = int(re.findall(r'\d+', nof_items_string)[2])
    # get the number of pages
    nof_pages = int(np.ceil(nof_items/NOF_ITEMS_PER_PAGE))

    date_list = list()
    start_price_list = list()
    max_price_list = list()
    min_price_list = list()
    end_price_list = list()

    list_list = [date_list, start_price_list, max_price_list, min_price_list, end_price_list]
    func_list = [j_strptime, float_wrapper, float_wrapper, float_wrapper, float_wrapper]

    for page_num in reversed(range(1, nof_pages+1)):
        current_url = url + ('&p=%d' % page_num)
        current_req = urllib.request.Request(current_url)
        try:
            current_res = urllib.request.urlopen(current_req)
        except urllib.error.URLError as e:
            print(e)
            sys.exit(1)
        current_html_doc = current_res.read()
        current_soup = bs4.BeautifulSoup(current_html_doc)

        target_table = current_soup.find('table', 'boardFin yjSt marB6')
        tr_tags = target_table.find_all('tr')

        # convert html table to python's list
        value_list = html_table_to_list(tr_tags)

        for row in reversed(value_list[1:]):  # first row is skipped
            for idx, val in enumerate(row):
                list_list[idx].append(func_list[idx](val))

    return(list_list)


def j_strptime(datestring):
    return(datetime.datetime.strptime(datestring, '%Y年%m月%d日'))


def float_wrapper(floatstring):
    return(float(floatstring.replace(',','')))


def html_table_to_list(table):
    ret_list = list()
    for tr in table:
        columns = list()
        for column in tr.stripped_strings:
            columns.append(column)
        ret_list.append(columns)
    return(ret_list)


if __name__ == '__main__':
    main(sy=2013, sm=3, sd=14, ey=2014, em=3, ed=14, nof_ticks=13)

YahooファイナンスのURLは、http://info.finance.yahoo.co.jp/history/?code=998407.O&sy=2014&sm=2&sd=13&ey=2014&em=3&ed=15&tm=d みたいな構造をしてるので、sy(おそらくstart yearの略)、sm(start month)、sd(start day)、同じくey(おそらくend yearの略)、em、edにそれぞれ変数を代入できるようにしています。
得られたHTMLをパースするために、BeautifulSoup(上に載せたコード中ではbs4としてimportしています)というサードパティ提供の強力なHTMLパーサライブラリ*1を使います。
BeautifulSoupを使って、先ほどゲットしたYahooファイナンスのページから今回のターゲットコンテンツである株価の値動きが書いてあるテーブルを抽出します。
そのテーブルをさらにパースして数値型や日付型にして、あとはmatplotlibに投げるだけです。
plt.xtickesは、x軸を日本語の表記にするため(matplotlibのデフォルトだとFeb.1 2014みたいな英語の表記になります)の指定です。
plt.tight_layoutは、図の下側が切れてしまうための措置。


最終的にはこういった図が得られます:f:id:kaisk:20140316185759p:plain


課題は、x軸(日付)の間隔の調整です。
例えば今回載せた図は1年間の値動きなので、n月1日の日付だけをx軸に置けたり、柔軟に設定できるようになると良いですね・・・
今回は2014年1月1日までの1年間をゲットしたのですが、2013年10月18日からのデータしか入っていないですね。日付のプロットがギリギリ入るところまでで自動的にカットされてしまったのでしょうか。x軸の調整は必須ですね。あとで修正をします。
日付表示のラベルの数を指定できるように修正しました。しかし、データがすべて入っていないのはYahooファイナンスの1ページあたりの表示量が50件分しかないのが原因のようですので、
少し大掛かりな修正を加えて対応しました(上のソースコードは修正済みです)。
まず、総件数をチェックして最終的なページ数を計算し、それぞれのページにデータを取りに行きます。


あとはmax(高値)とmin(安値)をエラーバー風にプロットしたい。これはそんなに難しくないでしょうけど。
実装しました。赤線が高値と安値の平均*2、青のバーの上側が高値、下側が安値です。


ちなみに、土日祝日は取引がお休みでデータ欠損が起こるため、得られた図の日付はたとえ一年間の指定をしても365日分のデータは得られません(取引所が開いている日の分だけしか得られない)。


上に載せたコードに車輪の再発明がどれだけ含まれてるか考えると悲しくなりますが、練習が目的なので今回は許してください*3

*1:BeautifulSoup4からPython3に対応した模様

*2:株価の指標として高値と安値の平均が意味を成すのか不明ですが。おそらく意味のない値でしょう。普通は何を使うの?

*3:そもそもYahooファイナンス公式にチャート表示機能あるし