akatak’s blog

プログラム初心者の50代ビジネスマンがセカンドキャリアを目指して働きながらPythonを中心に独学しています。自らの覚え書きや感じたことなどを脈絡もなく書き連ねるブログです。

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は威力を発揮します。

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