akatak blog

プログラム初心者の50代ビジネスマンがセカンドキャリアを目指して働きながらPythonを中心に独学していましたが、昨年IT系企業に転職。新規事業開発の仕事をすることになりました。自らの覚え書きや成果物、感じたことなどを脈絡もなく書き連ねるブログです。

Pythonによる不動産情報のデータ取得&分析(2)【賃貸物件/データ前処理編】

こんにちは。 皆さん、賃貸物件のダウンロードはうまくいきましたでしょうか。

以前はうまくいっていたのですが、最近、BeautifulSoupのhtml.parserがうまく機能していないのか、ダウンロードの度に、一定数のページでGoogleChromeの検証での表示と異なる状況となり、結果としてエラーが発生してしまいます。再度、同じスクリプトを走らせると、今度は前回と異なるページでエラーが発生したりして、挙動が一定しません。全体の2〜3割がうまくダウンロードできませんが、データ数としては全体で15万件ほどと十分確保できましたので、このまま進めたいと思います。

賃貸物件データの統合

前回と同様に他の22区(+興味のある三鷹市武蔵野市)についてもデータをダウンロードしておきます。これらのファイルをpandasを利用して読み込み、1つのファイルの統合し、csvファイルとして書き出します。

import pandas as pd

# dataフォルダに保存した各区の賃貸物件データの読み込み。i
df_adachi = pd.read_csv('data/suumo_adachi.csv', sep=',', index_col=0)
df_arakawa = pd.read_csv('data/suumo_arakawa.csv', sep=',', index_col=0)
df_bunkyo = pd.read_csv('data/suumo_bunkyo.csv', sep=',', index_col=0)
df_chiyoda = pd.read_csv('data/suumo_chiyoda.csv', sep=',', index_col=0)
df_chuo = pd.read_csv('data/suumo_chuo.csv', sep=',', index_col=0)
df_edogawa = pd.read_csv('data/suumo_edogawa.csv', sep=',', index_col=0)
df_itabashi = pd.read_csv('data/suumo_itabashi.csv', sep=',', index_col=0)
df_katsushika = pd.read_csv('data/suumo_katsushika.csv', sep=',', index_col=0)
df_kita = pd.read_csv('data/suumo_kita.csv', sep=',', index_col=0)
df_koto = pd.read_csv('data/suumo_koto.csv', sep=',', index_col=0)
df_meguro = pd.read_csv('data/suumo_meguro.csv', sep=',', index_col=0)
df_minato = pd.read_csv('data/suumo_minato.csv', sep=',', index_col=0)
df_nakano = pd.read_csv('data/suumo_nakano.csv', sep=',', index_col=0)
df_nerima = pd.read_csv('data/suumo_nerima.csv', sep=',', index_col=0)
df_ota = pd.read_csv('data/suumo_ota.csv', sep=',', index_col=0)
df_setagaya = pd.read_csv('data/suumo_setagaya.csv', sep=',', index_col=0)
df_shibuya = pd.read_csv('data/suumo_shibuya.csv', sep=',', index_col=0)
df_shinagawa = pd.read_csv('data/suumo_shinagawa.csv', sep=',', index_col=0)
df_shinjuku = pd.read_csv('data/suumo_shinjuku.csv', sep=',', index_col=0)
df_suginami = pd.read_csv('data/suumo_suginami.csv', sep=',', index_col=0)
df_sumida = pd.read_csv('data/suumo_sumida.csv', sep=',', index_col=0)
df_taito = pd.read_csv('data/suumo_taito.csv', sep=',', index_col=0)
df_toshima = pd.read_csv('data/suumo_toshima.csv', sep=',', index_col=0)
df_mi_mu = pd.read_csv('data/suumo_mi_mu.csv', sep=',', index_col=0)  # 三鷹市と武蔵野市の物件データ

read_csvメソッドの引数としてindex_col=0を設定していますが、これは、元のcsvファイルの最初の列(0列)をpandasデータフレームのindex列に指定するものです。 一旦全ての区のデータをデータフレームとして取り込んだ後、concatメソッドを利用して、これらのデータフレームを一つに統合します。各区のデータを列に追加していきますので、axis=0とします。また、各データフレームのindexを新たな通し番号として設定するために、ignore_index=Trueとしておきます。

df = pd.concat([df_adachi, df_arakawa, df_bunkyo, df_chiyoda, df_chuo, df_edogawa, df_itabashi, df_katsushika, df_kita, df_koto, df_meguro, df_minato, df_nakano, df_nerima, df_ota, df_setagaya, df_shibuya, df_shinagawa, df_shinjuku, df_suginami, df_sumida, df_taito, df_toshima, df_mi_mu], axis=0, ignore_index=True)

これで、東京23区の賃貸物件データがひとつのデータフレームになりました。

この統合されたデータフレームを、後からの作業をしやすくするために、一旦csvファイルに保存しておきましょう。

df.to_csv('data/suumo_tokyo.csv', sep=',')

データ前処理

さて、分析をしやすくするために、データを加工していきましょう。 まず、先ほど保存したデータフレームの全体像を見ると以下のようになります。

df.info()

f:id:akatak:20180818113630p:plain

また、最初の4行を見ておきましょう。

df.head(4)

f:id:akatak:20180818113637p:plain

データの前処理は以下の通り行うことにします。データの前処理は、アウトプットやエラー等を確認しながら行う必要があり、Jupyter Notebookを利用すると良いかと思います。

  • 住所から「都県」欄・「市区」欄を新設する
  • 築年数を「文字」から「数値」に置き換える
  • 家賃を「文字」から「数値」(円単位)にする
  • 管理費を「文字」から「数値」(円単位)にする
  • 「家賃合計」欄を新設する(家賃合計=家賃+管理費)
  • 「家賃単価(円/平米)」(=「家賃合計」÷「面積」)を計算する
  • 間取りから「部屋数」欄を作成する

最後に、

  • 立地1・立地2・立地3を「路線」「最寄駅」「手段」「時間」に分解する

「都県」欄・「市区」欄を新設

# 文字列を複数文字で split
df['都県'] = df['住所'].apply(lambda x: re.split('[都県]', x)[0]) 
df['市区'] = df['住所'].apply(lambda x: re.split('[都県市区]', x)[1]) 

複数の文字(この場合、「都」「県」「市」「区」)が合った場合に、その文字の前と後に分解するreモジュールのsplitを利用します。1行目により、「都県」の前後に分解し1番目の文字列を「都県」欄に格納します。また、2行目により、「都県市区」文字の前後に分解し、2番目の文字列を「市区」欄に格納します。

築年数を「文字」から「数値」に返還

まず、

df['築年数'].unique()

にて、築年数の種類を確認しておくと処理しやすいでしょう。結果は以下の通りとなります。

array(['築21年', '築2年', '築18年', '築17年', '築25年', '築27年', '築8年', '築6年', '築49年',
       '築1年', '築23年', '築13年', '築19年', '築11年', '築28年', '築5年', '新築', '築29年',
       '築4年', '築12年', '築3年', '築15年', '築24年', '築32年', '築54年', '築22年', '築7年',
       '築31年', '築16年', '築9年', '築10年', '築20年', '築30年', '築42年', '築34年',
       '築26年', '築14年', '築43年', '築40年', '築38年', '築48年', '築39年', '築41年',
       '築56年', '築33年', '築52年', '築35年', '築46年', '築45年', '築51年', '築36年',
       '築37年', '築53年', '築55年', '築44年', '築0年', '築47年', '築58年', '築50年',
       '築59年', '築60年', '築72年', '築65年', '築67年', '築57年', '築66年', '築74年',
       '築83年', '築82年', '築99年', '築64年', '築70年', '築61年', '築69年', '築63年',
       '築84年', '築62年', '築93年', '築68年', '築79年', '築75年', '築87年', '築78年',
       '築71年', '築77年', '築94年', '築97年', '築80年', '築73年'], dtype=object)

これらを見ると、
1. データの両端から「築」「年」を取り除く。
2. 「新」を「0」に置き換える。
3. 文字列を数値に変換する。
ことを行えば良いことが分かります。

df['築年数'] = df['築年数'].str.strip('築年')       # 両端から'文字'を削除
df['築年数'] = df['築年数'].str.replace('新', '0') # '1文字目'を'2文字目'で置換
df['築年数'] = pd.to_numeric(df['築年数'])      # 文字列を数値に変換

これで、築年数が無事に数値に変換できました。

家賃を「文字」から「数値」(円単位)に

まず、以下を計算することで「万円」が含まれていないデータがないことを確認しておきます。

sum(df['家賃'].apply(lambda x: '万円' not in x))

0であればOKです。 続いて、「万円」を「」で置き換えて、floatに変換後、10000を掛けます。小数点が表示されるのが煩わしいので、結果を整数に変換しておきます。

df['家賃'] = df['家賃'].str.replace('万円', '').apply(lambda x: int(float(x) * 10000))

管理費を「文字」から「数値」(円単位)に

さて、管理費です。これもdf['管理費'].unique()で内容を見ておきましょう。 管理費がないか不明なものは「-」と表示されている以外は、特段留意するところはなさそうです。

df['管理費'] = df['管理費'].str.replace('-','') # '-'を''で置換
df['管理費'] = df['管理費'].str.replace('円','') # '円'を''で置換
df['管理費'] = pd.to_numeric(df['管理費'], downcast='integer') #文字列を整数に変換
df['管理費'] = df['管理費'].apply(lambda x: 0 if np.isnan(x) else x) # NaNのデータの場合に0で置き換えます。

4行目を入れる代わりに、1行目の'-'の''への置換を'0円'への置換としておいた方がすっきりするかもしれません。

「家賃合計」欄を新設

「家賃単価(円/平米)」を計算

更に家賃合計と㎡当たりの家賃単価も計算しておきましょう。

df['家賃合計'] = df['家賃'] + df['管理費']
df['家賃単価(円/平米)'] = df['家賃合計'] / df['面積']                     

間取りから「部屋数」欄を作成

さて、間取りです。 これも同様にdf['間取り'].unique()にて、どんなデータがあるか見ておきましょう。

array(['3LDK', '1SLDK', '2LDK', 'ワンルーム', '1LDK', '4LDK', '1K', '3DK',
       '1DK', '2DK', '2K', '3K', '5SLDK', '2LK', '2SLDK', '2SK', '1SK',
       '4SK', '3SDK', '4DK', '2SDK', '5LDK', '4SLDK', '6LDK', '4K', '6DK',
       '3SLDK', '5K', '5DK', '5SDK', '1SDK', '6K', '4SDK', '1LK', '1SLK',
       '5SK', '7LDK', '8LDK', '9LDK', '6SDK', '6SLDK', '3SK', '7DK', '9DK',
       '3SLK', '3LK', '22LDK', '11SLDK', '7SLDK', '2SLK', '18DK', '32SLDK'], dtype=object)

部屋数を計算しやすくするために、「ワンルーム」との表示を「1RM」とでもしておきましょう。

df['間取り'] = df['間取り'].str.replace('ワンルーム', '1RM')

各間取りの数値部分が部屋数を示していますので、数値部分のみ取り出したいと思います。

df['部屋数'] = df['間取り'].apply(lambda x: int(re.search('[0-9]+', x).group(0)))

立地1・立地2・立地3を「路線」「最寄駅」「手段」「時間」に分解

さて、いよいよ、立地1〜3をそれぞれ「路線」「最寄駅」「手段」「時間」に分解していきましょう。入力が必ずしも、ルールに基づいていない場合もあり、エラーが起きやすい箇所です。ひとつひとつの作業を丁寧に確認しながら行っていくことが必要です。 まず、立地1から分解していきます。

df['立地1_路線'] = df['立地1'].apply(lambda x: x.split('/')[0])
df['立地1_駅'] = df['立地1'].apply(lambda x: x.split('/')[1].split(' ')[0])
df['立地1_手段'] = df['立地1'].apply(lambda x: x.split('/')[1].split(' ')[1][0])
df['立地1_分'] = df['立地1'].apply(lambda x: re.search('[0-9]+',x.split('/')[1].split(' ')[1]).group(0))

うまくいきました。続いて、立地2です。立地2がない場合(NaN)があり、その場合、「-」に変換しておきます。 また、立地1とは「−」の場合の処理が異なります。

df['立地2'].fillna('-',inplace=True)
df['立地2_路線'] = df['立地2'].apply(lambda x: x.split('/')[0])
df['立地2_駅'] = df['立地2'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地2_駅'] = df['立地2_駅'].apply(lambda x: x.split(' ')[0])
df['立地2_手段'] = df['立地2'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地2_手段'] = df['立地2_手段'].apply(lambda x: x.split(' ')[1][0] if x != '-' else x)
df['立地2_分'] = df['立地2'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地2_分'] = df['立地2_分'].apply(lambda x: x.split(' ')[1] if x != '-' else x)
df['立地2_分'] = df['立地2_分'].apply(lambda x: re.search('[0-9]+', x).group(0) if x !='-' else x)

これもうまくいきました。いよいよ立地3に参りましょう。処理は立地2と同様です。

df['立地3'].fillna('-',inplace=True)
df['立地3_路線'] = df['立地3'].apply(lambda x: x.split('/')[0])
df['立地3_駅'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_駅'] = df['立地3_駅'].apply(lambda x: x.split(' ')[0])
df['立地3_手段'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_手段'] = df['立地3_手段'].apply(lambda x: x.split(' ')[1][0] if x != '-' else x)

うーん。やはりエラーがでました。

f:id:akatak:20180818131035p:plain

以下のスクリプトをを動かして、エラーが発生した列を探しましょう。

errors = []
for i, item in enumerate(df['立地3_手段']):
    try:
        if item != '-':
            item.split(' ')[1]
        else:
            item
    except Exception as e:
        errors.append([e, i])
print(errors)

すると、以下のように

[[IndexError('list index out of range',), 103908]]

と表示されました。103908行のデータが何かおかしそうです。5列目(立地3)の中身を見るには、

df.iloc[103908,5]

とします。すると、

'自01/02自由が丘駅-駒大/深沢小学校 歩1分'

と表示されました。「路線/最寄駅 手段○分」というルールに従っていないようですので、修正しておきましょう。

df.iloc[103908, 5] = '東急バス(自由が丘駅-駒大)/深沢小学校 歩1分'

とでも入力して、以下の再度スクリプトを回してみると、今度はうまく行きました。

df['立地3_路線'] = df['立地3'].apply(lambda x: x.split('/')[0])
df['立地3_駅'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_駅'] = df['立地3_駅'].apply(lambda x: x.split(' ')[0])
df['立地3_手段'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_手段'] = df['立地3_手段'].apply(lambda x: x.split(' ')[1][0] if x != '-' else x)
df['立地3_分'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_分'] = df['立地3_分'].apply(lambda x: x.split(' ')[1] if x != '-' else x)
df['立地3_分'] = df['立地3_分'].apply(lambda x: re.search('[0-9]+', x).group(0) if x != '-' else x)

なお、次回の分析のために、物件の「階数」と物件がある「物件階」もデータを加工しておきます。

df['階数'] = df['階数'].str.replace('平屋','1階建')
df['階数'] = df['階数'].apply(lambda x: re.search('[0-9]+階建', x).group(0))
df['階数'] = df['階数'].apply(lambda x: int(x.strip('階建')))

にて、建物の階数を数字で確保します。 その他、「物件階」も個別に作業した上で、数値化して保存します。

修正後のデータを保存

最後に

df.to_csv('app/data/suumo_tokyo_mod.csv')

として、加工後のデータを保存しておきましょう。

いかがでしたでしょうか。人が作ったデータですので、間違って入力している場合もあります。この過程は、ひとつひとつの作業を、結果を見ながら、適宜、修正しながら行っていく必要があるかと思います。その場合に、Jupyter Notebookは威力を発揮します。

皆さんも是非試してみてはいかがでしょうか。 長くなりましたが、本日はこの辺で失礼します。

Pythonによる不動産情報のデータ取得&分析(1)【賃貸物件/Webスクレイピング編】

暑い日が続きますね。皆さんはいかがお過ごしでしょうか。

我が家では、不動産の賃貸で住み替えか?購入か?を検討しなくてはいけない局面が生じたので、何かデータを取得して分析できないかな、とネットサーフィンをしていたところ、発見!

【データで見る世界】機械学習を使って東京23区のお買い得賃貸物件を探してみた 〜スクレイピング編〜

ありがとうございます。このような先人の方がいらっしゃると、真似をしつつ、何とかやってみようという気がしてきます。

さて、まずは、東京23区の賃貸物件について、「データで見る世界」さんのブログを参考にして、Pythonを使って取得してみることにしましょう。「データで見る世界」さん同様にSUUMOさんからデータを取得していくにあたり、まずはSUUMO規約を確認しましょう。

SUUMO(スーモ)規約

私的利用を超えて使用してはいけないことは書かれていますが、それ以外に、スクレイピングに関する制限はなさそうですね。どうやらスクレイピングをしても大丈夫そうです。

不動産情報を取得する準備

まず、いつものようにrequestsモジュールとBeautifulSoupモジュールをimportしましょう。それ以外にもtimeモジュールとpandasモジュールもimportしておきます。

import requests
from bs4 import BeautifulSoup
import time
import pandas as pd

SUUMOさんのホームページから「関東」「賃貸物件」と選択していくと、「関東の賃貸住宅[賃貸マンション・アパート情報探し]」のページとなり、「沿線・エリアから探す」よう選択が求められますので、「東京都」「エリア」を選びます。そうすると、「東京都ー市区郡を選択」するページが表示されます。ここで東京23区を全て選択してもいいかと思いますが、全部で20万件程度のデータになるかと思います。時間もかかるし、実行中にエラーとなる可能性も高くなりますので、区毎にデータを取得していきます。

最初は、足立区を選択してみましょう。足立区の賃貸物件検索ページのアドレスをコピペし、urlに格納します。

# SUUMO 賃貸 東京都足立区 検索
url = 'https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=030&bs=040&ta=13&sc=13121&cb=0.0&ct=9999999&et=9999999&cn=9999999&mb=0&mt=9999999&shkr1=03&shkr2=03&shkr3=03&shkr4=03&fw2=&srch_navi=1'

いつものようにurlの情報を取得して、htmlで構文解析を行い、結果をsoupオブジェクトに格納します。そして、そのsoupオブジェクトを通じて、基本的にデータを取得していくことになります。

result = requests.get(url)
c = result.content
soup = BeautifulSoup(c, "html.parser")

実際にホームページをご覧になると分かると思いますが、物件データは数十ページに跨がっています。また、データ数によって日々、ページ数は変わってきますので、まずはページ数を取得して、その後1ページずつデータしていきます。

ページ数・urlの取得

# ページ数を取得
body = soup.find("body")
pages = body.find_all("div", {'class':'pagination pagination_set-nav'})
pages_text = str(pages)
pages_split = pages_text.split('</a></li>\n</ol>')
num_pages = int(pages_split[0].split('>')[-1])

「データで見る世界」さんとは、自分自身で理解しやすいように、スクリプトは変更しています。 実際にデータを取得していく際には、かなり変更している部分がありますので、参考にしていただければと思います。

さて、次に、urlsリストに1ページずつ、urlを加えていきます。ここはそのまま使わさせていただいています。

# 全てのページのURLを作成
# urlを入れる箱(リスト)を設定
urls = []

# 1ページ目を格納
urls.append(url)

# 2ページ目以降を格納
for i in range(num_pages-1):
    pg = str(i+2)
    url_page = url + '&pn=' + pg
    urls.append(url_page)

スクレイピングによるデータ取得の準備

まずは、全データを格納するリストdataとエラーが出たときにその内容を格納するリストerrorsを用意します。賃貸物件の件別データ(1つの物件でも複数の部屋の物件が出ている場合は、その件別も含む)を1つ1つ取り込みます。その場合の件別データそのものもリスト形式なので、dataはリストのリストになります。

data = []
errors = []

さて、スクレイピングの本体部分に入っていきます。urlsに格納したurlを一つずつ取り出して、1ページ毎にスクレイピングしていきます。

for url in urls:

スクレイピングの最中にエラーが出ても、スクリプトが動き続けるように、try〜exceptを入れます。ついでにerrorsにエラーの内容、エラーが出たときのurl、何番目のデータかを保存しておきます。

    try:
    
    # ここにスクリプトのエンジン部分を書きます。
    
    except Exception as e:
        errors.append([e, url, len(data)])
        pass    

いよいよ、スクレイピングのエンジン部分に入っていきましょう。url毎にsoupオブジェクトを作り、soupオブジェクトから各種情報を取り出すようにします。

        result = requests.get(url)
        c = result.content
        soup = BeautifulSoup(c, "html.parser")

Google Chromeの検証機能を使って探していくと、ページの中で件別データ全体を選択するのはdivタグのうち、id='js-bukkenList'のものになります。findメソッドの引数でこれを指定して、summaryに情報を格納します。更に、summaryに格納された情報の中で、各件別データを選択するのは、divタグのうち、class='cassetteitem'ですので、find_allメソッドの引数でこれを指定して、件別データをcassetteitemsに格納します。

        summary = soup.find("div",{'id':'js-bukkenList'})
        cassetteitems = summary.find_all("div",{'class':'cassetteitem'})

スクレイピングのエンジン部分

その後は、cassetteitemsから件別データ(cassetteitem)毎に情報を取り出していきます。この段階でもエラー処理を行えるようにしております。

        for cas in cassetteitems:
            try:
              # 情報取得用の箱を準備します。
                new = ''  # 新着
                subtitle = '' # 物件名
                location = '' # 住所
                station_list = []  # 最寄駅(リスト)
                yrs = ''           # 築年数
                heights = ''      # (建物の)階数
                floor = ''         # (物件のある)階数
                rent = ''      # 家賃
                admin = ''         # 管理費
                others = ''     # その他(敷金/礼金等)
                floor_plan = ''   # 間取り
                area = 0       # 面積

                # 物件名
                subtitle = cas.find("div",{"class":"cassetteitem_content-title"}).string

                # 住所
                location = cas.find("li",{"class":"cassetteitem_detail-col1"}).string

                # 最寄駅
                sta = cas.find("li", {"class":"cassetteitem_detail-col2"})
                stas = sta.find_all("div", {"class":"cassetteitem_detail-text"})
                for s in stas:
                    station_list.append(s.string)

                # 築年数、階数
                col3 = cas.find("li",{"class":"cassetteitem_detail-col3"})
                yrs = col3.find_all('div')[0].string
                heights = col3.find_all('div')[1].string

                tbodies = cas.find_all('tbody')

                for tbody in tbodies:
                    cols = tbody.find_all('td')
                    for i, col in enumerate(cols):
                        if i == 0:
                            try:
                                new = col.span.string.strip('\r\t\n')
                            except:
                                new = ''
                        if i == 2:
                            floor = col.string
                        if i == 3:
                            rent = col.string
                        if i == 4:
                            admin = col.string
                        if i == 5:
                            others = col.string
                        if i == 6:
                            floor_plan = col.string
                        if i == 7:
                            area = float(col.contents[0].split('m')[0])
                    data.append([new, subtitle, location, station_list[0], station_list[1], station_list[2], yrs, heights, floor,
                                 rent, admin, others, floor_plan, area])
            except Exception as e:
                errors.append([e, url,len(data)])
                pass

        time.sleep(1) # スクレイピングする際の礼儀として、1秒待ちましょう

これで全ての物件データが取得でき、dataとerrorsに格納されました。

pandasを使ってデータをcsvファイルとして保存

さて、これらのデータをpandasのメソッドを利用してDataFrameオブジェクトに変換、その後、csvファイルとして保存します。

# data listを DataFrameに変換
df = pd.DataFrame(data, columns=['新着','物件名','住所','立地1','立地2','立地3','築年数','階数','物件階','家賃','管理費','敷金礼金','間取り','面積'])

# csvファイルとして保存
df.to_csv('data/suumo_adachi.csv', sep = ',',encoding='utf-8')

# ついでに errors fileも保存
df_errors = pd.DataFrame(errors)
df_errors.to_csv('data/errors_adachi.csv', sep = ',', encoding='utf-8')

以上でデータが保存できました。念のため、errorsを開いて、エラーがないことを確認しましょう。たまに、通信環境等によってか、エラーが発生することがありますが、少ない時は無視します。エラーが多い時だけ、時間を空けて再度トライしてみます。これでうまくいくことが多いので、エラーが発生した場合には、時間をおいてみましょう。

次回以降、取得したデータの加工をしていきます。

なぜ、Pythonプログラミングの勉強を始めたのか。

そういえば、なぜ、セカンドキャリアを目指すのにプログラミングなのか。なぜ、プログラミング言語の中でPythonを選んだのか、についてまだお知らせしていませんでした。今後プログラミングを始めてみたいという方の参考になればと思い、今回は、プログラミングを始めた理由、Pythonを選んだ理由について、書きたいと思います。

なぜプログラミングなのか

  • 人生100年ライフ時代を迎えて、長生きリスクがクローズアップされてきました。年金も減る可能性が高く、生きている限り、働き続けることも念頭に置く必要がある時代の到来。自分自身に照らすと、あと数年で会社を退職したら、何をするのか。逆に、いつまでも働き続けるためには、何らかのスキルが必要ではないのか思っていたところ、経済産業省が「2030年にはIT人材が60万人不足する」との調査結果を発表してました。やはり不足人材が多いほど、それだけニーズがあるのだから、きっと年をとっても仕事があるはず、との期待から、プログラミングをやってみようと思った次第です。甘いかも、ですが。

    (経済産業省)IT人材の最新動向と将来推計に関する調査結果

  • それと、私自身が中高生のころ、ちょうどパソコンが世の中に出始めた頃ですが、BASICでプログラムを組んでみたり、好きだった記憶があります。この年になって、プログラミングをいざ始めて見ると、すごく楽しいんですよね。子供の頃、好きだったことは、年をとっても変わらないということですかね。これから年をとっても続けて行くには、やはり好きなことが良いのではないか、と思ったことも理由のひとつです。

なぜPythonなのか

プログラミング言語 2018 2013 2008 2003 1998 1993 1988
Java 1 2 1 1 16 - -
C 2 1 2 2 1 1 1
C++ 3 4 3 3 2 2 5
Python 4 7 6 11 23 18 -
C# 5 5 7 8 - - -
Visual Basic .NET 6 12 - - - - -
JavaScript 7 10 8 7 20 - -
PHP 8 6 4 5 - - -
Ruby 9 9 9 18 - - -
R 10 23 46 - - - -
Perl 12 8 5 4 3 11

(出所)TIOBE(the software quality company)

  • Pythonは、1991年に開発されましたので、歴史は長い言語です。近年、人工知能分野でよく使われるため、注目を集めるようになってきています。
  • また、Pythonは文法がシンプル、かつコードの分量が少なく記述でき、読みやすいという特徴があります。そのため、私のような初心者には非常に学びやすい言語だと思います。また、様々なライブラリが無料で公開されており、汎用性が高いことも特徴の一つです。
  • 更に、人工知能分野だけでなく、Webアプリケーションや組み込みアプリケーションでも使われており、代業的なサービスとしては、YouTubeやインスタグラム、Evernoteがあるようです。
  • 加えて、アメリカでは中高生のプログラミングの授業でPythonが教えられるようになっており、ExcelでもPythonを使えるようにすることを検討中との話もあります。

以上のようなことを総合的に考えて、Pythonの勉強を始めることにしました。 参考になりましたでしょうか?

今後、どこかのタイミングで、どのように学んでいるかについても書きたいと思います。 それでは。

個人投資家のためのWebスクレイピング(5)〜 Pythonで「通貨インデックス」を作成してみよう

さて、個人投資家向けのスクレイピングシリーズが続いていますが、今回は、Webスクレイピングを利用して「通貨インデックス」を作成してみましょう。

日経ヴェリタス」の記事の中に、毎週連載されている「世界通貨番付」というものがあります。最近、身近に日経ヴェリタスがないので、「世界通貨番付」と同じように、各通貨が今強くなっているのか弱くなっているのか、通貨全体の中での強弱が分かるものが作れたらなと思っておりました。そうしないと、ドル円やユーロ円だけを見がちなので、全体感が掴めないからです。さらに、過去の推移も分かるともっといいですよね。

そこで、またまたまたPythonの出番です!

日経ヴェリタスを見てみると、「日経通貨インデックス 日本経済新聞社が算出する実効為替レートの指標。25通貨が対象。2008年=100」と記載されています。

どうやら実効為替レートを見れば良いみたい。

日銀のホームページによれば、実効為替レートとは、

実効為替レートは、特定の2通貨間の為替レートをみているだけでは捉えられない、相対的な通貨の実力を測るための総合的な指標です。具体的には、対象となる全ての通貨と日本円との間の2通貨間為替レートを、貿易額等で計った相対的な重要度でウエイト付けして集計・算出します。

とのことです。また、

最新の値は、国際決済銀行(Bank for International Settlements、BIS)公表の、Broadベースの実効為替レートを利用しています。同系列の作成方法やカバレッジ、ウエイト等の詳細については、BISのホームページを参照してください。

とありますので、早速、BISホームページに行ってみましょう。

https://www.bis.org/statistics/eer.htm

Daily data(更新は週次のようです)のCSVファイルがありますので、これを取得して展開していけば良いことが分かります。CSV horizontalとCSV verticalがありますが、時系列データなので縦に時系列データが蓄積されているCSV verticalを取得します。

import requests
import zipfile, io
import os

# BISホームページで実効為替レートが表示されているページのCSV verticalを指定します。
url = 'https://www.bis.org/statistics/full_webstats_eer_d_dataflow_csv_row.zip'

# requestsライブラリのgetメソッドを使って、ファイル情報をResponseオブジェクトとして取得
res = requests.get(url)
# Responseオブジェクトからバイナリデータを取り出し、ZipFileオブジェクトに変換し、解凍します。
z = zipfile.ZipFile(io.BytesIO(res.content))
z.extractall()

# 解凍して得られたcsvファイルを、BISフォルダの下に移動します。
os.rename('WEBSTATS_EER_D_DATAFLOW_csv_row.csv', 'BIS/WEBSTATS_EER_D_DATAFLOW_csv_row.csv')

その後は、PandasライブラリのDataFrameを使って、データを加工していきます。

import pandas as pd

# csvファイルを取り込み、DataFrameオブジェクトに転換します。その際に、余分な行をskipします。
df = pd.read_csv('BIS/WEBSTATS_EER_D_DATAFLOW_csv_row.csv', skiprows=[0,1,2,4])

# データのうち、日付データとBisがBroadデータと呼んでいる61通貨のヒストリカルデータのみ抽出
# そして、データがない行を削除したものをfx_broadとします。
fx_broad = df.iloc[:, :62].dropna()

この段階でfx_broadを表示させると以下の通り。

f:id:akatak:20180721122622p:plain

日付は現在Stringオブジェクトになっていますので、Datetimeオブジェクトに変換した上で、indexとして設定しましょう。 ついでにindex名をDateにしておきます。

df['Reference area'] = pd.to_datetime(df['Reference area'], format='%Y-%m-%d')
fx_broad.set_index(keys='Reference area', inplace=True)
fx_broad.index.name = 'Date'

現時点でのデータ系列は実効為替レートそのものなので、これをインデックス化します。世界通過番付における日経通貨インデックスが2008年を100としていますので、今回は2008-01-01が100となるように計算してみましょう。また、日経通貨インデックスが25通貨なので、同様に25通貨のDataFramefx_index_mainを作成します。

fx_index = fx_broad / fx_broad.loc['2008-01-01',:] * 100
main_fx_list = ['AE:United Arab Emirates','AU:Australia','BR:Brazil','CA:Canada','CH:Switzerland',
               'CN:China','DK:Denmark','GB:United Kingdom','HK:Hong Kong SAR',
               'ID:Indonesia','IN:India','JP:Japan','KR:Korea','MX:Mexico',
               'MY:Malaysia','NO:Norway','NZ:New Zealand','RU:Russia','SA:Saudi Arabia',
               'SE:Sweden','SG:Singapore','TH:Thailand','TW:Chinese Taipei',
                'US:United States','XM:Euro area']
fx_index_main = fx_index[main_fx_list]

さらに、pct_changeメソッドで週次、半年、2年、5年のインデックスの変化率を計算し、その結果を表示しましょう。

wk_chg = fx_index_main.pct_change(periods=5).iloc[-1]* 100
hy_chg = fx_index_main.pct_change(periods=130).iloc[-1] * 100
y2_chg = fx_index_main.pct_change(periods=520).iloc[-1] * 100
y5_chg = fx_index_main.pct_change(periods=1300).iloc[-1] * 100

table = pd.DataFrame([fx_index_main.iloc[-1], wk_chg, hy_chg, y2_chg, y5_chg], index=['FX_index', 'weekly-change', 'half-year-change', '2-year-change', '5-year-change'])
table_T = table.T.sort_values(by='weekly-change', ascending=False)

table_Tを表示すると
f:id:akatak:20180721133048p:plain

となります。日経通貨インデックスとは若干FX_indexの数値もその他変化率の数値も異なっていますが、全体感や方向感は良いようです。数値が異なっているのは、日経通貨インデックスが2008年=100(年平均値?)としているのに対し、ここでは2008年1月1日のデータを100としていることによるものかなぁと勝手に推測しています。

今後いろいろと修正が必要かと思いますが、取り敢えず「通貨インデックス」もどきが出来ました。

なお、変化幅を5年の変化率をもとにソートして、描画してみましたので、アップします。 また、今回利用したJupyter Notebookもアップしておきますね。

import matplotlib.pyplot as plt

plt.style.use('ggplot')
table_T.sort_values(by='5-year-change', ascending=True).drop('FX_index',axis=1).plot(kind='barh',fontsize=12, figsize=(8,20), colormap='viridis')

f:id:akatak:20180721133842p:plain

実効為替レートから「通貨インデックス」を作成する

個人投資家のためのWebスクレイピング(4)〜 Pythonを使って、東証「空売り比率」を取得し、グラフ化してみよう【下】

前回積み残してしまった過去12ヶ月のバックデータを取り込むようにスクリプトを修正しましたので、アップデートしておきます。

  空売り集計のトップページ(https://www.jpx.co.jp/markets/statistics-equities/short-selling/index.html)の上部右側にバックデータの年月を選択できるので、選択して表示させてみると、アドレスは以下のようになっています。「.html」の左側の数字だけが変化しているだけなので、簡単に取り込めそうです。

https://www.jpx.co.jp/markets/statistics-equities/short-selling/00-archives-01.html
https://www.jpx.co.jp/markets/statistics-equities/short-selling/00-archives-02.html
...
https://www.jpx.co.jp/markets/statistics-equities/short-selling/00-archives-12.html

これらのアドレスをurlsにリストとして保存しておいて、その後、ひとつひとつurlとしてアクセスし、pdf_listに保存場所を抽出していきます。

urls = []
url = 'https://www.jpx.co.jp/markets/statistics-equities/short-selling/index.html'
urls.append(url)

# urlsにバックデータのurlを蓄積
for i in range(12):
    if i <= 8:
        urls.append("https://www.jpx.co.jp/markets/statistics-equities/short-selling/00-archives-0" + str(i+1) + ".html")
    else:
        urls.append("https://www.jpx.co.jp/markets/statistics-equities/short-selling/00-archives-" + str(i+1) + ".html")

# urlsからひとつひとうつurlを取り出し、url毎に必要なpdf保存場所をpdf_listに抽出
for url in urls:
    res = requests.get(url)
    # 東証のホームページだとres.encoding = 'ISO-8859-1'となり、res.textが文字化けするため、
    # 以下の行を入れる。そうすると、res.encoding = 'utf-8'となる。
    res.encoding = res.apparent_encoding
    soup = BeautifulSoup(res.text, 'html.parser')
    s = soup.find('div', {'class': 'component-normal-table'})
    a_tags = s.find_all('a')

    for a_tag in a_tags:
        if a_tag.get('href')[-5] == 'm':
            pdf_list.append(a_tag.get('href'))

そして、抽出したpdf_listをもとに、pdf_ファイルを取得して、tempフォルダ下に保存していきます。

base_url = 'https://www.jpx.co.jp'

# tempフォルダ下にpdfファイルを取得する
for i, x in enumerate(pdf_list):
    url = base_url + x
    urllib.request.urlretrieve(url,'temp/shortselling'+ str(i) + '.pdf')

後は、前回と一緒です。ただし、Errorが発生してもスクリプトが止まらないようにtry〜exceptを加えておきます。また、傾向を掴むために、期間中の平均値を点線で加えておきます。 そのスクリプトを実行した結果はこちらになります。それっぽいグラフになりました。

f:id:akatak:20180717220333p:plain

今回のスクリプトもこちらにアップしておきます。ご参考まで。

[東京証券取引所空売り比率」推移(過去12ヶ月)](https://gist.g

akatak.hatenadiary.jp

akatak.hatenadiary.jp

個人投資家のためのWebスクレイピング(4)〜 Pythonを使って、東証「空売り比率」を取得し、グラフ化してみよう【中】

さて、前回に続いて、東証空売り比率」のデータを取得して、グラフ化してみましょう。

前回、東証ホームページからPDFファイルを取得しましたので、次にそれらPDFファイルからテキストを抽出してみます。

PDFファイルからのテキスト取得については、以下のサイトで多少お世話になったPyPDF2を使ってみました。
Automate the Boring Stuff with Python

本サイトの元の英語本は、日本語に翻訳されています(こちら↓)。

ここに記載の通り、以下の通り試してみました。

import PyPDF2
# Open a PDF file.
fp = open('temp/shortselling0.pdf', 'rb')
pdfReader = PyPDF2.PdfFileReader(fp)
pageObj = pdfReader.getPage(0)
pageObj.extractText()

ですが、表示されたのは、文字化け。

'\'5Gˆ0£˚˜\n>Þ>Ü>Ý>ä>Û>ã>Û>Ý>ß\n0ɢd\nFÇ\n2ˇ5 \n>Ô>Õ\n˚ı"á\n>Ô>Õ>Û>Ô>Õ\n2ˇ5 \n>Ô>Õ\n˚ı"á\n>Ô>Õ>Û>Ô>Õ\n2ˇ5 \n>Ô>Õ\n˚ı"á\n>Ô>Õ>Û>Ô>Õ\n>Þ>Ü>Ý>ä>ã˙v>Ý>߈¥>Ý>Ø>ä>Ü>Ý>Ø>Þ>Ý>Þ>â>Ü>Ú>Þ>Ñ>å>ä>Þ>Ø>Ý>à>â>ß>Þ>Ú>ä>Ñ>Þ>Ü>â>Ø>â>â>Þ>â>Ú>å>Ñ>Þ>Ø>å>å>Ü>Ø>Ü>Þ>Ü\nFÿ˝4\'¼G"0£ˆX˛+\'ìF÷FÒG˙F¹\nGˆFþFûFÿ#ÝG"F¹\n˙vˆ¥\n˜@ˆe\'5Gˆ˝A0dFÒGˆ\'5Gˆ˝A0dFúFç\n0£\n>Ô>Õ\n'

こちら↓によると、PyPDF2のextractTextは貧弱な機能のようです。こちらに記載のようにpdfminer(python3系ではpdfminer3k)を使ってみることにしました。

teratail.com

pdfminer3kに関しては、こちらのブログ記事のスクリプトをほぼそのまま使わせていただきました。

cartman0.hatenablog.com

from pdfminer.pdfparser import PDFParser
from pdfminer.pdfparser import PDFDocument
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.pdfparser import PDFPage
from pdfminer.pdfdevice import PDFDevice
from pdfminer.converter import PDFPageAggregator
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.layout import LTTextBoxHorizontal

# Open a PDF file.
fp = open('temp/shortselling0.pdf', 'rb')

# Create a PDF parser object associated with the file object.
parser = PDFParser(fp)
document = PDFDocument()
parser.set_document(document)

# Create a PDF document object that stores the document structure.
# Supply the password for initialization.
password=""
document.set_parser(parser)
document.initialize(password)

# Create a PDF resource manager object that stores shared resources.
rsrcmgr = PDFResourceManager()

# Set parameters for analysis.
laparams = LAParams()

# Create a PDF page aggregator object.
device = PDFPageAggregator(rsrcmgr, laparams=laparams)
interpreter = PDFPageInterpreter(rsrcmgr, device)

pages = list(document.get_pages())
page_1 = pages[0] # 1st page
page_1

# interpreter page1
interpreter.process_page(page_1)

# receive the LTPage object for the page.
# layoutの中にページを構成する要素(LTTextBoxHorizontalなど)が入っている
layout = device.get_result()
# print(layout)

text = []

for l in layout:
#     print(l) # l is object
    if isinstance(l, LTTextBoxHorizontal):
        text.append(l.get_text())

print(text)

これを実行してみると、以下の通り、きれいに出力されました。

['空売り集計(日次)\n',
 '年月日\n',
 '実注文\n',
 '空売り(価格規制あり)\n',
 '空売り(価格規制なし)\n',
 '売買代金\n(a)\n',
 '比率\n(a)/(d)\n',
 '売買代金\n(b)\n',
 '比率\n(b)/(d)\n',
 '売買代金\n(c)\n',
 '比率\n(c)/(d)\n',
 '2018/7/13\n(株)東京証券取引所\n',
 '【単位:百万円】\n',
 '合計\n(d)\n',
 '2018年7月13日\n',
 '1,801,212\n',
 '60.2%\n',
 '982,146\n',
 '32.8%\n',
 '206,662\n',
 '6.9%\n',
 '2,990,020\n',
 '(注1)数値は外国株券等を含む合計数値(概算)である。\n(注2)空売りの中には信用取引を含む。\n']

上記のテキストデータから、12番目(日付)と17番目(実取引の割合)を抽出して加工すれば、必要なデータが得られます。 日付については、"\n"でsplit。その最初のテキストを更に"/"でsplit。それらをyear, month, dayに格納し、datetimeオブジェクトに変換します。

year, month, day = text[11].split('\n')[0].split('/')
datetime.datetime(int(year), int(month), int(day))

次に、空売り比率ですが、実取引の割合をテキストから小数に変換し、100から引けば算出できます。

100 - float(text[16].rstrip('%\n'))

これらを先ほどのスクリプトに加えて、取り込んだ複数のファイルを処理できるように forループにします。そこで得られたデータ系列をもとに、matplotlibで描画すれば出来上がり。

f:id:akatak:20180716090648p:plain

PDFファイルからテキストを取得するのに、多少苦労しましたが、何とかグラフ化までこぎ着けました。 参考までに最後にスクリプトを掲載しておきます。

今回のデータ系列は少しでしたが、バックデータは過去12ヶ月分あるようなので、それらも取り込めるように工夫することも必要かもしれません。

東京証券取引所「空売り比率」推移

akatak.hatenadiary.jp

akatak.hatenadiary.jp

個人投資家のためのWebスクレイピング(4)〜 Pythonを使って、東証「空売り比率」を取得し、グラフ化してみよう【上】

先日の日経新聞のマーケット欄に出ていました東証株式市場の空売り比率。これも今後の株式市場を占うのに大事な指標かと思いますので、Pythonを利用して、東証のホームページからWebスクレイピングしてみましょう。

東証 空売り比率」で、ググってみると、すぐに見つかりました。

https://www.jpx.co.jp/markets/statistics-equities/short-selling/index.html

ホームページを見ると、日付毎にPDFが張り付いているので、これらを一旦、作業ファイルに保存しておくことにしようと思います。そして、後でPDFからテキストデータを取り出せばいいと思いつつも、PDFからテキストデータをうまく取り出せるかな、とちょっと心配。

取り敢えず、まずは、いつものようにrequestsモジュールとBeautifulSoupモジュールを使ってスクレイピングを行っていきます。

import requests
from bs4 import BeautifulSoup

# urlとして、先ほど検索した東証「空売り集計」のトップページを指定。
url = 'https://www.jpx.co.jp/markets/statistics-equities/short-selling/'

res = requests.get(url)

# 東証HPだとデフォルトではres.encoding = 'ISO-8859-1'となり、res.textの漢字が文字化けしてしまう。
# そこで、以下の行を加えると、res.encoding = 'utf-8'となり、文字化けしなくなる。
res.encoding = res.apparent_encoding

# BeautifulSoupオブジェクトを生成。解析を行うパーサーとして'html.parser'を指定。
soup = BeautifulSoup(res.text, 'html.parser')

さて、ここで、Google Chromeの検証機能を使って、PDFファイルが格納されているディレクトリのリンクを特定しましょう。

以下の画像の通り、<div>タグのうち、<class="component-normal-table">となっているタグをまず指定し、その後、find_all()メソッドを使い、<a>タグを全て取得します。そして、get()メソッドの引数を"href"とすることで、リンクを取得します。

f:id:akatak:20180714195113p:plain

ただし、ホームページをよく見ると「空売り集計」欄だけでなく、「業種別集計」欄もあります。検証機能でリンクのPDFファイル名をよく見ると、「空売り集計」欄のファイル名の末尾は***-m.pdfとなっており、「業種別集計」欄の末尾は***-g.pdfとなっています。

今回は「空売り集計」欄のPDFだけ取得したいので、リンクの後から5文字目が"m"となっているリンクのみ取得するようにします。

# <class="component-noramal-table>となっている<div>タグで囲まれた範囲の情報を取得
s = soup.find('div', {'class': 'component-normal-table'})

# その範囲で、更に<a>タグで囲まれた情報を全て取得
a_tags = s.find_all('a')

pdf_list = []

# それらの<a>タグで囲まれた情報を一つ一つ取り出す。
for a_tag in a_tags:
    # get('href')メソッドで、hrefで指定しているリンクを取り出し、後ろから5文字目が"m"のリンクのみ取得して、pdf_listに追加する。
    if a_tag.get('href')[-5] == 'm':
        pdf_list.append(a_tag.get('href'))

これで、無事、リンクを取得できました。

['/markets/statistics-equities/short-selling/nlsgeu0000037ver-att/180713-m.pdf',
 '/markets/statistics-equities/short-selling/nlsgeu0000037t4d-att/180712-m.pdf',
 '/markets/statistics-equities/short-selling/nlsgeu0000037pl7-att/180711-m.pdf',
 '/markets/statistics-equities/short-selling/nlsgeu0000037nj9-att/180710-m.pdf',
 '/markets/statistics-equities/short-selling/nlsgeu0000037jr9-att/180709-m.pdf',
 '/markets/statistics-equities/short-selling/nlsgeu0000037h3w-att/180706-m.pdf',
 '/markets/statistics-equities/short-selling/nlsgeu0000037efl-att/180705-m.pdf',
 '/markets/statistics-equities/short-selling/nlsgeu00000379yi-att/180704-m.pdf',
 '/markets/statistics-equities/short-selling/nlsgeu000003764s-att/180703-m.pdf',
 '/markets/statistics-equities/short-selling/nlsgeu0000037115-att/180702-m.pdf']

このリンクは、東証トップページからの相対位置を示していますので、絶対位置に変換して、作業フォルダの下にtempフォルダを作り、そこに保存します。

import os
import urllib.request
import time

base_url = 'https://www.jpx.co.jp'

for i, x in enumerate(pdf_list):
    url = base_url + x
    urllib.request.urlretrieve(url,'temp/shortselling'+ str(i) + '.pdf')
    time.sleep(1)  # 礼儀として1秒待つ

ここまで、お疲れ様でした。さて、いよいよ次は、これらのPDFファイルから、必要なテキストを取得して、データ系列を作成し、可視化していきましょう。ただし、PDFファイルを取り扱うモジュールも必要になり、長くなってしまうので、次回にしたいと思います。それでは。

akatak.hatenadiary.jp

akatak.hatenadiary.jp