こちらの記事をご覧いただきありがとうございます。
以前にSUUMOの物件データをスクレイピングした記事と、スクレイピングデータを前処理した記事を投稿しました。そこからさらに前処理案を思いついて実践しましたので、解説します。
↓が、私が以前投稿したSUUMOの物件データをスクレイピングした記事と、スクレイピングしたデータを前処理した記事です。よろしければ先にそちらをご覧ください。
注意書き
スクレイピングしたデータを公開するといろいろ問題になってしまうので、実際にデータを処理してこれがこうなったとお見せすることができません。実データではないサンプルを使ってどのような処理を行ったのかをお伝えしますが、実データの処理とはまた違うことをご了承ください。
行った処理
- 間取りを部屋数+(S,L,D,K) があるかどうかに変換
- 住所から緯度経度を追加
- 緯度経度を使い、「最寄駅からの距離」「皇居からの距離」を追加
それぞれを以下で個別に解説します。
間取りを部屋数+(S,L,D,K) があるかどうかに変換
まずこれがどういうことか説明すると、例えば間取りが「 4LDK 」というデータがあったときに
- 部屋数 → 4
- 間取り_S → 0
- 間取り_L → 1
- 間取り_D → 1
- 間取り_K → 1
と変換します。アルファベットについてはいわゆるOneHotです。
これの何がうれしいかというと、機械学習をするときに
- 間取りが「4LDK」である
という情報が
- 部屋数が4つ
- S(サービスルーム)がない
- L(リビング)がある
- D(ダイニング)がある
- K(キッチン)がある
に変わります。
前者だと、機械学習モデルはその言葉の意味を解釈することができないので、「4LDK」と「4K」は完全に別者扱いされることになります。
後者だと、「4LDK」と「4K」には「4」と「K」という共通点を与えることができます。
若しくは、部屋数の違いでどう変わるのか、「K」の有無でどう変わるのか、などの変化の差を調べることも出来るようになります。
それと、間取りが「 ワンルーム 」ってパターンもありますが、これは
- 部屋数が1
- Sなし
- Lなし
- Dなし
- Kなし
ということにしました。
ワンルームの意味を考えるとこの処理には諸説あると思います。
ワンルームとは決して部屋1個だけで他の設備がないわけではなく、広い部屋にキッチン設備などを備えている部屋です。
ですので、SLDはともかくKをなしにするのは違和感を感じます。キッチン設備は基本どの部屋にもついてますからね。
とはいえ、SUUMOが「1K」と「ワンルーム」を明確に区別していることも事実です。SUUMOを見る人も、「1K」と「ワンルーム」で受ける印象が違うと思います。
ですので、「1K」と「ワンルーム」を区別できるように、先ほどのようなワンルームの扱いにしました。
「1K」と「ワンルーム」の差については以下を参考にしました。ぜひご覧ください。
以下、処理に使ったコードです。
# 間取り変数の変換(正規表現)
# ワンルームは1部屋ってことにする(かなり諸説)
condition = suumo_tokyo['間取り'] == 'ワンルーム'
suumo_tokyo.loc[condition,'間取り'] = '1'
# 数字部分を取り出して、部屋数変数にする
suumo_tokyo['部屋数'] = suumo_tokyo['間取り'].map(lambda x: re.search('([0-9]+)(.*)',x).group(1)).astype(int)
# S,L,D,K はあれば1でなければ0になる
list_alpha = ['S','L','D','K']
for alpha in list_alpha:
suumo_tokyo[f'間取り_{alpha}'] = 0
condition = suumo_tokyo['間取り'].map(lambda x: search_object(alpha,x))
suumo_tokyo.loc[condition,f'間取り_{alpha}'] = 1
住所から緯度経度を追加
住所の情報を使って緯度経度の情報を追加しました。
これのメリットは、見ていただいたほうが早いと思うので画像を出します。
これは23区ごとに物件賃料の平均値を比較する図です。
黒<赤<黄で賃料の優劣をつけています。
ぱっと見で、都心のあたりが高いんだなぁーとか、郊外は安いんだなぁーとか、そういうことが感じられると思います。ぱっと見でだいたいわかるって凄いですよね。
物件を借りる時に立地は絶対に気にする点だと思いますので、地図上で立地を確認できることはかなりありがたいです。
で、地図上で物件の位置を確認したいなぁーと思った時に、「緯度経度」が使えそうだと思ったので、住所から緯度経度を取り出しました。可視化の上ではかなり役に立ちました。
住所→緯度経度に変換する仕組み
さて、ここから緯度経度を取り出す過程の話です。
3つに分けて紹介します。まずは住所→緯度経度の変換です。
# 住所→緯度経度変換装置
import requests
import urllib
def translate_address_coordinates(address):
makeUrl = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="
s_quote = urllib.parse.quote(address)
response = requests.get(makeUrl + s_quote)
return response.json()[0]["geometry"]["coordinates"]
この住所→緯度経度の変換装置は、以下をコピペ参考にしました。
これがどういう仕組みか解説します。
まずdefの中にあるurlですが、国土地理院のAPIってやつらしいです。
詳しくは知りませんが、アドレス最後の?q=
の後ろに住所を入力するとその緯度経度情報を返してくれます。
urlを入れるだけだと[]が表示されるだけ
?q=(住所)とすると緯度経度を教えてくれる
※入力した住所は東京タワーです
これがjson形式?で表記されているので、returnでうまく取り出しています。
(この辺の仕組みは私はまだ良く知らないです。AWSのAPIもこんな感じだった記憶があります。知らんけど)
これを利用して、物件の住所を緯度経度に変換します。
物件の緯度経度を取得
# 住所のuniqueから緯度経度を取得
# csvに変換しておいたので2回目は不要
all_addresses = {}
running_times = []
i = 0
# 住所のユニークごとに処理を行う
for address in suumo_tokyo['住所'].unique():
start = time.time()
# 住所から緯度経度を取得
coordinate = translate_address_coordinates(address)
all_addresses[address] = coordinate
time.sleep(1)
finish = time.time()
# ここから下は処理時間の計測
running_time = finish - start
running_times.append(running_time)
all_count = suumo_tokyo['住所'].unique().shape[0]
print(f'{i}件目:{running_time}秒')
# 作業進捗
complete_ratio = round(i/all_count*100,3)
print(f'完了:{complete_ratio}%')
# 作業の残り時間目安を表示
running_mean = np.mean(running_times)
running_required_time = running_mean * (all_count - i)
hour = int(running_required_time/3600)
minute = int((running_required_time%3600)/60)
second = int(running_required_time%60)
print(f'残り時間:{hour}時間{minute}分{second}秒\n')
i += 1
※処理時間計測と表示の部分の解説は割愛します。
※今見返したら、わざわざカウンター用の i を用意してたんやな…って思いました。enumerate使った方が良くない?
物件データの住所のユニークごとにforループを実行し、住所から緯度経度を取得しています。
物件ごとにforループしたほうが良くね?と思われた方もいると思います。私も最初はそう思いましたし、コードを書くうえではその方がわかりやすくていいような気がします。
私がわざわざ住所のユニークを取ったのは、その方が処理時間が短いからです。
同じ建物で複数の部屋が掲載されていることは多々あるので、物件ごとに緯度経度を取得すると同じ住所を何回か処理することになります。
物件数は222923件取得していたのですが、住所のユニークの数は2952件でした。
最初はそんなに減るの!?と思いましたが、GoogleMapでSUUMOに掲載されている住所を検索するとわりかし広い範囲で表示されるのでそんなもんかーと納得しました。
適当な住所で検索したらわりと範囲が広かった
それはさておき、緯度経度の取得件数が 222923件→2952件 に減るのでかなり処理時間が圧縮されました。助かりました。
注意として、サーバにアクセスを掛けるので休憩時間を付けておきます。負荷がかからない配慮は大事です。
あとは住所と緯度経度のテーブルと物件データのテーブルをくっつけるだけです。
# 住所と緯度経度のDataFrame
# これもcsvに保存済なので2回目は不要
coordinates = pd.DataFrame(all_addresses).T
coordinates.reset_index(inplace=True)
coordinates.rename(columns={
'index':'住所',
0:'経度',
1:'緯度'
}, inplace=True)
coordinates.to_csv('./data/csv/coordinates.csv',index=False)
# 元データとくっつける
# 連打すると増殖するので注意
coordinates = pd.read_csv('./data/csv/coordinates.csv')
suumo_tokyo = pd.merge(suumo_tokyo, coordinates, on='住所', how='left')
先ほど取得した、住所と緯度経度の対応表をDataFrameに変えてからcsvで保存しています。やり直しになると面倒ですからね。
住所をキーにして物件データと緯度経度情報をくっつければ完成です。
緯度経度を使って地図を出力する
あとは物件の緯度経度を座標平面のxとyだと思って散布図に出力します。
xが経度、yが緯度ですね。
plt.figure(figsize=(8,8))
plt.scatter(suumo['経度'],suumo['緯度'])
plt.show()
東京23区らしい地図が出来上がる
23区で条件を絞って物件情報を取得したので、いかにも23区らしい地図になりました。
皇居や荒川も見てわかりますね。物件がないところは点が表示されないので、でかい施設や山川も見てわかるようになります。
お時間ある人は、SUUMOで日本中の物件を取得すれば日本地図(らしいもの)も出力できるのでぜひお試しください。私は処理時間がすこぶる長そうなのでやりません。
緯度経度を使い、「最寄駅からの距離」「皇居からの距離」を追加
先にこれを追加した理由の話をしておきます。長いので飛ばしても問題ないです。
先ほど23区と賃料による色分けの図を出しましたが、なんか都心のほうが賃料の相場が高そうじゃありませんでしたか?少なくとも私はそう思いました。
実際、私がSUUMOで引っ越し先を探したときも、勤務時間が短いほうがいいからなるべく都心に近いほうがいいなーと思っていましたが、都心になるほど家賃高いんですね。
ですので、機械学習で家賃に関わるなんやかんやをするときに、「都心からの距離」があると役に立つんじゃないかと考えた次第です。
でも「都心」てどこやねん、ってなったのですが、さっき出力した地図を眺めていると、どうも「皇居」でよくね?って気がしてきました。ので「皇居からの距離」を追加しました。
「皇居からの距離」は実質「都心からの距離」ということです。が「都心」=「皇居」と考えるのは諸説ある気がしているので、「都心」とは宣言していません。
もう一つ、やはり私が物件を探すときに、通勤時間が短いほうがいいからなるべく駅近にしたいなーという考えもありました。
駅近の物件が人気とはよく聞きますし、「最寄り駅までの距離」と家賃ってなんかあるんじゃないかなーと考えたので、追加しました。
ではここから変数を追加する過程を解説します。
皇居からの距離のつくり方
まず簡単なほうから、「皇居からの距離」変数ですが、これは皇居の緯度経度をもってきて、各物件との距離を計測するだけです。
皇居の住所はWikipediaから拾ってきました。
さて、距離の計算の仕方ですが、緯度経度から計算できます。
緯度経度を単純にxy座標平面で考えれば、三平方の定理を使うだけなので簡単です。
しかし、地球は丸いのでそう単純ではありませんでした。
以下サイトにある計算式を利用しました。
緯度経度からの距離計算の解説は以下で詳しく解説されていますので、そちらをご覧ください。
で、2点の緯度経度→2点の距離の関数で以下を作成しました。
# 座標から距離を計算
def distance(keido1,ido1,keido2,ido2):
# 出力は[m]
r=6378137
keido1 = math.radians(keido1)
ido1 = math.radians(ido1)
keido2 = math.radians(keido2)
ido2 = math.radians(ido2)
distance = r*np.arccos(round(np.sin(ido1)*np.sin(ido2)+np.cos(ido1)*np.cos(ido2)*np.cos(keido2-keido1),10))
return distance
緯度経度をラジアンに変換して、distance =
の後ろにある計算式に代入しています。
計算式の中にあるround()
ですが、計算の内部処理で発生する微小な誤差のせいでarccosがうまく機能しないことがあるらしいので、round()
で計算誤差をなかったことにしています。
以下を参考にしました。ぜひご覧ください。
距離計算をする関数が出来上がったので、あとは計算してもらうだけですね。
そんなに処理時間がかからないので物件ごとに計算してもらっています。先ほどと同様に住所ごとに計算するのもよいと思います。
# 皇居からの距離変数を生成
# 皇居の座標を獲得
koukyo = '東京都千代田区千代田1'
keido_k, ido_k = translate_address_coordinates(koukyo)
# 皇居(都心?)からの距離[m]
suumo_tokyo['皇居からの距離'] = 0
suumo_tokyo['皇居からの距離'] = suumo_tokyo.apply(lambda x: distance(x['経度'], x['緯度'], keido_k, ido_k), axis=1)
せっかくなので地図も出力します。
見ればわかりますが、黄<赤<黒 で皇居との距離をグラデーションしています。真夏の太陽みたいになりました。
最寄駅からの距離のつくり方
皇居からの距離と比較してこっちはまあまあ手間がかかりました。
最寄り駅からの距離を計算するために、以下の過程を踏みました。
- 都内の駅の住所を取得 → 駅名と住所の対応表を作成
- 駅の住所から緯度経度を取得 → 駅名、住所、緯度経度の対応表を作成
- 物件データと駅の緯度経度表を、駅名をキーにしてくっつける
- 物件の緯度経度と駅の緯度経度を利用して距離を計算
分けて考えると案外難しくなさそうじゃないですか?(そこそこ手間はかかるんですけど)
都内の駅の住所を取得 → 駅名と住所の対応表を作成
これはスクレイピングで情報を拾ってきます。
駅と住所の一覧が乗っているサイトを利用します。
以下のサイトから情報を拾ってきました。
以下は実際に使用したコードです。スクレイピングの解説は以前にも記事にしているので、コードの解説は割愛します。
# せっかくなので駅→住所→緯度経度変換できるようにしたい
# さきにページ情報だけ取得する
# csvに保存したので2回目は不要
url = 'https://www.navitime.co.jp/category/0802001/13/?page={}'
max_page = 44
running_times = []
soup_tank = []
for page in range(1,max_page+1):
start = time.time()
load_url = url.format(page)
html = requests.get(load_url)
soup = BeautifulSoup(html.content, 'html.parser')
soup_tank.append(soup)
time.sleep(1)
finish = time.time()
running_time = finish - start
running_times.append(running_time)
all_count = max_page
print(f'{page}件目:{running_time}秒')
# 作業進捗
complete_ratio = round(page/all_count*100,3)
print(f'完了:{complete_ratio}%')
# 作業の残り時間目安を表示
running_mean = np.mean(running_times)
running_required_time = running_mean * (all_count - page)
hour = int(running_required_time/3600)
minute = int((running_required_time%3600)/60)
second = int(running_required_time%60)
print(f'残り時間:{hour}時間{minute}分{second}秒\n')
# 回収したhtmlデータから駅と住所の対応を回収する
# csvに保存したので2回目は不要
all_sta_add = []
for page in range(max_page):
stations = []
for item in soup_tank[page].find_all(class_='spot-name-text'):
stations.append(item.text)
addresses = []
for item in soup_tank[page].find_all(class_='spot-detail-value-text'):
isaddress = re.match('東京都',item.text)
if isaddress :
addresses.append(item.text)
sta_add = np.c_[stations, addresses]
if page == 0:
all_sta_add = sta_add
else:
all_sta_add = np.r_[all_sta_add,sta_add]
# データをcsvに保存
pd.DataFrame(all_sta_add,columns=['駅','住所']).to_csv('./data/csv/station_address.csv',index=False)
※スクレイピングについて私が以前に開設した記事はこちらです。ぜひご覧ください。
駅の住所から緯度経度を取得 → 駅名、住所、緯度経度の対応表を作成
これは先ほどの住所→緯度経度変換を使用すればOKです。
以下コードです。
# 駅の住所→緯度経度の対応を回収したい
# csvに保存済なので2回目は不要
all_addresses = {}
running_times = []
i = 0
for address in all_sta_add['住所']:
start = time.time()
coordinate = translate_address_coordinates(address)
all_addresses[address] = coordinate
time.sleep(1)
finish = time.time()
running_time = finish - start
running_times.append(running_time)
all_count = all_sta_add['住所'].shape[0]
print(f'{i}件目:{running_time}秒')
# 作業進捗
complete_ratio = round(i/all_count*100,3)
print(f'完了:{complete_ratio}%')
# 作業の残り時間目安を表示
running_mean = np.mean(running_times)
running_required_time = running_mean * (all_count - i)
hour = int(running_required_time/3600)
minute = int((running_required_time%3600)/60)
second = int(running_required_time%60)
print(f'残り時間:{hour}時間{minute}分{second}秒\n')
i += 1
# 駅の住所と緯度経度のDataFrame
# csvに保存済なので2回目は不要
coordinates = pd.DataFrame(all_addresses).T
coordinates.reset_index(inplace=True)
coordinates.rename(columns={
'index':'住所',
0:'経度',
1:'緯度'
}, inplace=True)
coordinates.to_csv('./data/csv/station_coordinates.csv',index=False)
# 駅の住所と緯度経度対応表
# csvに保存したので2回目は不要
all_sta_add = pd.read_csv('./data/csv/station_address.csv')
station_coordinates = pd.read_csv('./data/csv/station_coordinates.csv')
station_coordinates = pd.merge(all_sta_add, station_coordinates, on='住所', how='left')
station_coordinates.to_csv('./data/csv/station_coordinates.csv', index=False)
物件データと駅の緯度経度表を、駅名をキーにしてくっつける
SQLでよくありそうな操作ですね。
くっつけようとしたときに素直にくっついてくれませんでしたので、少し前処理をしています。
素直にくっついてくれなかったパターンが以下でした。
- 駅名(東京都) って書いてある 例:自由が丘(東京都)
- 駅名[路線] って書いてある 例:両国[JR]
- ヶとケが違う 例:阿佐ヶ谷と阿佐ケ谷
- 23区内じゃない駅 例:谷塚(埼玉県の駅)
- そもそも駅じゃない 例:大泉風致地区(練馬区にある公園)
Navitimeなど駅一覧サイトでは、同じ駅名でも区別がつくように( )や[ ]でわかるようにしています。SUUMOでは( )も[ ]もついていません。ので、( )や[ ]は削除しました。
※都内23区なので( )の削除でOKですが、例えば日本全国で同じことをするならこの手法は使えません。SUUMOの最寄り駅情報の駅名を駅一覧サイトに合わせる必要があります。
ヶとケが違うパターンについて、Navitimeは「ヶ」で統一されているっぽいですが、SUUMOでは統一されていませんでした。どっちも「ヶ」があれば「ケ」に変換してくっつくようにしました。
※どっちに合わせるかは諸説ある気がしますが、くっつけるだけならどっちかに統一すればOKです。
23区じゃない駅について、SUUMOの最寄り駅で23区内の物件でも23区内じゃない駅を最寄にしているところが何件かありました。これらは駅一覧で都内に絞っている場合は当然くっつきません。
最寄り駅に駅じゃないものが乗っている物件も当然ながらくっつきません。
これらは例外処理の個別対応が大変になる気がしたので止めました。物件データは22万件くらいありましたが、駅の緯度経度が得られなかった物件は100件もなかったはずです。
あと、処理後に駅名が同じになるパターンがありますが、重複列は削除しました。くっつけるときに物件データが増えてしまうことを避けるためです。
その結果間違った駅とくっつく可能性がありますが、適切な駅とくっつけるための手順(特にSUUMOに掲載されている駅が同名駅のどの駅とくっつけるのが正しいのかを判断するところ)が思いつかなかったので諦めました。
以下くっつけるコードです。
# 適切に結合するための修正
# 修正を保存したので2回目は不要
station_coordinates = pd.read_csv('./data/csv/station_coordinates.csv')
# ()つきを圧縮する
station_coordinates.loc[station_coordinates['駅'].map(lambda x: search_object('(.*?)(\(.+?\))(.*?)',x)),'駅'] = station_coordinates.loc[station_coordinates['駅'].map(lambda x: search_object('(.*?)(\(.+?\))(.*?)',x)),'駅'].map(lambda x: re.sub('(.*?)(\(.+?\))(.*?)',r'\1\3',x) )
# 〔〕|[]つきを圧縮する
station_coordinates.loc[station_coordinates['駅'].map(lambda x: search_object('(.*?)(〔|\[)(.+)(〕|\])(.*?)',x)),'駅'] = station_coordinates.loc[station_coordinates['駅'].map(lambda x: search_object('(.*?)(〔|\[)(.+)(〕|\])(.*?)',x)),'駅'].map(lambda x: re.sub('(.*?)(〔|\[)(.+)(〕|\])(.*?)',r'\1\5',x) )
# ヶはケに変える
station_coordinates.loc[station_coordinates['駅'].map(lambda x: search_object('ヶ',x)),'駅'] = station_coordinates.loc[station_coordinates['駅'].map(lambda x: search_object('ヶ',x)),'駅'].map(lambda x: re.sub('ヶ','ケ',x))
# 重複列を削除する(ほぼ同じ住所になると信じることにする)
station_coordinates = station_coordinates[~station_coordinates['駅'].duplicated()]
# # 加工した状態で保存
station_coordinates.to_csv('./data/csv/station_coordinates.csv' ,index=False)
# 結合
station_coordinates = pd.read_csv('./data/csv/station_coordinates.csv')
# 名前が被らないように修正
station_coordinates.rename(columns={
'住所':'住所(駅)',
'緯度':'緯度(駅)',
'経度':'経度(駅)'
}, inplace=True)
# 〇〇駅に名称を変換
station_coordinates['駅'] = station_coordinates['駅'].map(lambda x: x+'駅')
# ヶはケに変える
suumo_tokyo.loc[suumo_tokyo['駅'].map(lambda x: search_object('ヶ',x)),'駅'] = suumo_tokyo.loc[suumo_tokyo['駅'].map(lambda x: search_object('ヶ',x)),'駅'].map(lambda x: re.sub('ヶ','ケ',x))
# 結合
suumo_tokyo = pd.merge(suumo_tokyo, station_coordinates, on='駅', how='left')
物件の緯度経度と駅の緯度経度を利用して距離を計算
これは皇居とほぼ同じ作業です。
各サンプルに物件の住所と最寄駅の住所があるので、それを利用して最寄り駅までの距離を計算します。
# 最寄駅からの距離変数を生成
# 最寄駅からの距離[m]
suumo_tokyo['最寄駅からの距離'] = 0
condition = ~suumo_tokyo['経度(駅)'].isnull()
suumo_tokyo.loc[condition,'最寄駅からの距離'] = suumo_tokyo[condition].apply(lambda x: distance(x['経度'], x['緯度'], x['経度(駅)'], x['緯度(駅)']), axis=1)
最寄り駅からの距離を地図に表す
最寄り駅からの距離の値によってグラデーションを付けて地図を出力します。
黒>赤>黄で距離が大きいです。黄色が駅近です。
せっかくなので山手線も一緒に出力しました。山手線上は黄色のはず…?
おそらく想定通りの変数が出来ています。
都心に近ければ基本どこでも駅があるので、だいたい黄色か赤になってますね。郊外になると黒がちらほら見えてきます。
※山手線の出力は、山手線の駅を駅番号順に並べて折れ線グラフを出力すると描けます。詳しい説明は割愛します。
終わり。
緯度経度情報は特に可視化の側面で大いに役立ってくれそうです。また家賃を予測するうえで有効そうな変数生成にも役立ちました。
機械学習に利用する目的で新しい変数を生成しましたが、目的が明確であればこそこのような有効そうな変数も思いつくよなぁとか感じました。何をするにしても目的は大事ですね。
変数生成もしたのでそろそろモデル分析を始めます。(というかしているところです。)
モデル分析で面白い結果になったらそれも記事にするかもしれません。
他のSUUMO記事
まとめ記事書いたのでぜひご覧ください。