こちらの記事をご覧いただきありがとうございます。
以前にSUUMOの物件データをスクレイピングした記事を投稿しました。そのデータを機械学習でモデルに学習させることができるように前処理をしましたので、今回はその過程を書きます。
↓が私が以前投稿したSUUMOの物件データをスクレイピングした記事です。よろしければ先にそちらをご覧ください。
注意書き
スクレイピングしたデータを公開するといろいろ問題になってしまうので、実際にデータを処理してこれがこうなったとお見せすることができません。実データではないサンプルを使ってどのような処理を行ったのかをお伝えしますが、実データの処理とはまた違うことをご了承ください。
前処理の過程
ここから私が実際に行った前処理の過程をご紹介します。
SUUMOの物件データ処理前と処理後
まず物件データが処理によってどのように変わったかをご紹介します。
※実際のデータをお見せできないので、具体例は私がテキトーに作りました。実在しない物件ですのでご注意ください。
処理前
名称 | 説明 | 具体例 |
---|---|---|
カテゴリ | 建物のカテゴリ | 賃貸マンション |
建物名 | 建物の名前 | 東京タワー |
住所 | 建物の住所 | 東京都港区芝公園4丁目2−8 |
最寄り駅1 | 建物の最寄り駅の1つ | 都営大江戸線/赤羽駅 歩5分 |
最寄り駅2 | 建物の最寄り駅の1つ | 東京メトロ日比谷線/神谷町駅 歩7分 |
最寄り駅3 | 建物の最寄り駅の1つ | 都営三田線/御成門駅 歩6分 |
築年数 | 建物が経ってからの年数 | 築63年 |
階数 | 建物の階数(地下を含む) | 333階建 |
階 | 部屋のある階 | 33階 |
賃料 | 1カ月の家賃 | 3.6万円 |
管理費 | 家賃と別に支払う物件の維持費用 | 5000円 |
敷金 | 契約時に支払う敷金 | 3.6万円 |
礼金 | 契約時に支払う礼金 | 3.6万円 |
間取り | 部屋の構造 | 33LDK |
専有面積 | 部屋の面積 | 4,120.34m2 |
url | 部屋の詳細情報が載っているページのurl | https://www.tokyotower.co.jp/price/ |
処理後
※★は何も処理していないところです。
名称 | 説明 | 具体例 |
---|---|---|
カテゴリ★ | 建物のカテゴリ | 賃貸マンション |
建物名★ | 建物の名前 | 東京タワー |
都道府県 | 建物の住所の都道府県の部分 | 東京都 |
市区町村 | 建物の住所の市区町村の部分 | 港区 |
市区町村以下 | 建物の住所の市区町村以下の部分 | 芝公園4丁目2−8 |
路線 | 最寄り駅1の路線 | 都営大江戸線 |
駅 | 最寄り駅1の駅 | 赤羽駅 |
歩 | 最寄り駅1の歩いてかかる時間 | 5 |
バス | 最寄り駅1のバスに乗る時間(書いてあれば) | 0 |
車 | 最寄り駅1の車でかかる時間(書いてあれば) | 0 |
築年数 | 建物が経ってからの年数 | 63 |
地上 | 建物の地上部分の階数 | 333 |
地下 | 建物の地下部分の階数 | 0 |
階数 | 地上+地下 | 333 |
階 | 部屋のある階 | 33 |
賃料 | 1カ月の家賃 | 3.6 |
管理費 | 家賃と別に支払う物件の維持費用 | 5000 |
敷金 | 契約時に支払う敷金 | 3.6 |
礼金 | 契約時に支払う礼金 | 3.6 |
間取り★ | 部屋の構造 | 33LDK |
専有面積 | 部屋の面積 | 4,120.34 |
url★ | 部屋の詳細情報が載っているページのurl | https://www.tokyotower.co.jp/price/ |
処理前後比較
処理前後がわかりやすいように横に並べます。
※処理前後で名称が変わっているものは()に処理後の名称を記載しました。
名称 | 処理前 | 処理後(名前 |
---|---|---|
カテゴリ★ | 賃貸マンション | 賃貸マンション |
建物名★ | 東京タワー | 東京タワー |
住所 | 東京都港区芝公園4丁目2−8 | 東京都(都道府県) |
港区(市区町村) | ||
芝公園4丁目2−8(市区町村以下) | ||
最寄り駅1 | 都営大江戸線/赤羽駅 歩5分 | 都営大江戸線(路線) |
赤羽駅(駅) | ||
5(歩) | ||
0(バス) | ||
0(車) | ||
最寄り駅2 | 東京メトロ日比谷線/神谷町駅 歩7分 | ※削除 |
最寄り駅3 | 都営三田線/御成門駅 歩6分 | ※削除 |
築年数 | 築63年 | 63 |
階数 | 333階建 | 333(地上) |
0(地下) | ||
333(階数) | ||
階 | 33階 | 33 |
賃料 | 3.6万円 | 3.6 |
管理費 | 5000円 | 5000 |
敷金 | 3.6万円 | 3.6 |
礼金 | 3.6万円 | 3.6 |
間取り★ | 33LDK | 33LDK |
専有面積 | 4,120.34m2 | 4,120.34 |
url★ | 部屋の詳細情報が載っているページのurl | https://www.tokyotower.co.jp/price/ |
変数ごとの処理
ここから、変数ごとに行った処理を簡単に解説します。
基本的に正規表現を使用して処理しています。正規表現の基本的な解説は省きますので、正規表現って何?という人はぜひ以下をご覧ください。
また、もっといい書き方はあると思いますので、ここはこう書いたほうがよくね?と思ったところは書きなおしていただいてよいと思います。ぜひ改善点のご意見をいただけますと幸いです。
ライブラリ
正規表現だけならこれだけあれば十分なはずです。
import pandas as pd
import re
複数の変数の処理で使用する関数
出番がいくらかあった関数を先に定義します。
# この文字列ありますか?関数
def search_object(search,object):
return bool(re.search(search,object))
# 文字+数字+文字 → 数字
def objnumobj_num(x):
return re.sub(r'\w+?([0-9]+)\w+',r'\1',x)
# 最後の数字列を取り出す
def lastnum(x):
return re.findall('[0-9]+',x)[-1]
# (文字)数字を取り出す
def get_objnum(x):
return re.search(r'([A-Z]?)([0-9]+)',x).group()
# ???万円→???
def drop_man(x):
return re.sub(r'([0-9]+)万円',r'\1',x)
# 数字+文字→数字
def numobj_num(x):
return re.sub(r'([0-9]+)\w+',r'\1',x)
# 英字以降を切り落とす
def drop_behind_alfa(x):
return re.sub('[a-z]\w+','',x)
築年数、賃料、管理費、敷金、礼金、専有面積
# 築年数(正規表現)
# 新築は築0年ということにする
suumo_tokyo.loc[suumo_tokyo['築年数']=='新築','築年数'] = '築0年'
# 築??年(以上)→??
# 99階以上は99階に圧縮されます
suumo_tokyo['築年数'] = suumo_tokyo['築年数'].map(lambda x: objnumobj_num(x))
# 整数型に変更
suumo_tokyo['築年数'] = suumo_tokyo['築年数'].astype(int)
スクレイピングしたデータはsuumo_tokyo
という変数にDataFrameで入っています。
簡単なところから説明します。
これらの変数は不要な文字を削除しただけです。
例えば築年数なら、基本的に 「築??年」 という形になっているので、築と年を削除します。
ただし、規則的でない例外が存在します。
築年数ならたまに 「新築」 というデータがありますので、先に 「築0年」 に変換しておきます。
(後の処理と合わせて結果的に0になります)
また、 「築99年以上」 というデータもありますが、「年」を消す処理ではなく、数字の後ろの文字列を全て削除する書き方にしたので、まとめて処理できます。
この場合、築99年以上は築99年と同じ扱いになります。
また、データが入っていないところ(-)は0に直します。
築年数を例にして解説しましたが、他の変数も同じような考えでだいたい処理できます。
(処理が終わってから改めてみると、\w
を使ってたんだなーと感じました。今書くなら.
にします。他にももっといい書き方が出来そうだなーと感じるところがいくらかありました。)
他の変数の処理も以下に載せます。
# 賃料変数(正規表現)
# 10000倍するかは諸説
# 賃料から万円を除去
suumo_tokyo['賃料'] = suumo_tokyo['賃料'].map(lambda x: drop_man(x))
# float型に変換
suumo_tokyo['賃料'] = suumo_tokyo['賃料'].astype(float)
# 管理費(正規表現)
suumo_tokyo['管理費']
# 管理費の'-'は0ということにする
suumo_tokyo.loc[suumo_tokyo['管理費']=="-",'管理費'] = suumo_tokyo.loc[suumo_tokyo['管理費']=="-",'管理費'].replace('-','0')
# ????円→????
suumo_tokyo['管理費'] = suumo_tokyo['管理費'].map(lambda x: numobj_num(x))
# 整数型に変更
suumo_tokyo['管理費'] = suumo_tokyo['管理費'].astype(int)
# 敷金変数(正規表現)
# 10000倍するかは諸説
# '-'は0万円ってことにする
suumo_tokyo.loc[suumo_tokyo['敷金']=='-','敷金'] = '0万円'
# 敷金から万円を除去
suumo_tokyo['敷金'] = suumo_tokyo['敷金'].map(lambda x: drop_man(x))
# float型に変換
suumo_tokyo['敷金'] = suumo_tokyo['敷金'].astype(float)
# 礼金変数(正規表現)
# 10000倍するかは諸説
# '-'は0万円ってことにする
suumo_tokyo.loc[suumo_tokyo['礼金']=='-','礼金'] = '0万円'
# 礼金から万円を除去
suumo_tokyo['礼金'] = suumo_tokyo['礼金'].map(lambda x: drop_man(x))
# float型に変換
suumo_tokyo['礼金'] = suumo_tokyo['礼金'].astype(float)
# 専有面積変数(正規表現)
# ????m2 → ????
suumo_tokyo['専有面積'] = suumo_tokyo['専有面積'].map(lambda x: drop_behind_alfa(x))
# float型に変換
suumo_tokyo['専有面積'] = suumo_tokyo['専有面積'].astype(float)
階数
# 地下変数(正規表現)
# 平屋は1階建ということにする
suumo_tokyo.loc[suumo_tokyo['階数']=='平屋','階数'] = '1階建'
# 地下何階までありますか?関数
def underground(x):
return search_object('地下',x)*objnumobj_num(x)+(1-search_object('地下',x))*'0'
# 地下がなければ0、あるなら何階まであるか
suumo_tokyo['地下'] = suumo_tokyo['階数'].map(lambda x: underground(x))
# int型に変換
suumo_tokyo['地下'] = suumo_tokyo['地下'].astype('int')
# 地上変数(正規表現)
suumo_tokyo['階数']
# 平屋は1階建ということにする
suumo_tokyo.loc[suumo_tokyo['階数']=='平屋','階数'] = '1階建'
# ??階建→??
suumo_tokyo['地上'] = suumo_tokyo['階数'].map(lambda x: lastnum(x))
# int型に変換
suumo_tokyo['地上'] = suumo_tokyo['地上'].astype(int)
# 階数変数は地下地上の合算にしておく
suumo_tokyo['階数'] = suumo_tokyo['地上'] + suumo_tokyo['地下']
階数変数は基本的に 「??階建」 と表記されています。
例外として、地下がある物件は 「地下??地上??階建」 、平屋の物件は 「平屋」 と表されています。
これを
- 地上:(地上)??階建の部分
- 地下:地下??の部分
- 階数:地上+地下
に直します。
また、平屋は1階建扱いとし、先に変換しておきます。
コードの解説をすると、
先に作っておいたsearch_object
関数は、文字列と検索したいワードを入れると、文字列が含まれていればTrue、なければFalseで返します。
str.contains
と似ていますが、正規表現を使った指定が可能です。
※地下変数を作るだけならstr.contains
で十分な気がします。
これで地下がある物件を絞り込み、「地下??」 の数字を取り出して地下変数に入れます。
地上変数は地下があってもなくても 「??階建」 の数字を取り出せばOKです。
地上と地下の変数を作った後、階数変数に地上+地下を入れます。
階
# 階変数(正規表現)
# - は1階ということにする
suumo_tokyo.loc[suumo_tokyo['階']=='-','階'] = '1階'
# 階の前処理 -があればその後ろ、なければ(文字)数字
def floor(x):
return search_object('-',x)*lastnum(x)+(1-search_object('-',x))*get_objnum(x)
# B? → -?,Bがなければそのまま
def basement_floor(x):
return search_object('B',x)*('-'+re.search(r'[0-9]+',x).group())+(1-search_object('B',x))*(x)
# M? → ?-0.5 Mがなければそのまま
def middle_floor(x):
return search_object('M',x)*str((int(re.search(r'[0-9]+',x).group())-0.5))+(1-search_object('M',x))*(x)
# 上の処理を適用する
suumo_tokyo['階'] = suumo_tokyo['階'].map(lambda x: floor(x)).map(lambda x: basement_floor(x)).map(lambda x: middle_floor(x))
# float型に変換
suumo_tokyo['階'] = suumo_tokyo['階'].astype(float)
階変数は、基本的に 「??階」 と表記されています。これも例外がいくらかあります。
まず、 「??-??階」 (1-10階など)と表記されているものがあります。
suumoのページをよく観察しましたが、1階から10階の間のどこかだろうという以上はわかりませんでした。
(物件を探している人にとって不親切では…?なぜ幅を持たせるような表現なのでしょうか…?)
これの処理に考えられるパターンはいくらか思いつきました。
- -の前(後ろ)の数字をそのまま使う
- -前後の数字の中間(平均)を使う
私は-の後ろの数字を取り出すことにしました。
(明確な根拠はありません。処理が簡単だからです。)
もう一つ例外パターンとして、 「B1階」 「M2階」 など、BやMがついていることがあります。
Bは割とよく見るのでご存じだと思いますが、地下を表す数字です。これは数字に-(マイナス)を付けました。
Mは中間を表す記号らしいです。 「M2階」 とあれば、1階と2階の中間です。数字部分を取り出して、いったんintに変換し -0.5をしてstrに戻します。
例えば、「M2階」→「1.5」に書き換わります。
Mについては以下を参照しました。ぜひご覧ください。
住所
# 住所変数(正規表現)
# ~都,~区,~ に分けたい
# ついでに[都道府県][市区町村][以下]に対応したい
x = suumo_tokyo.loc[0,'住所']
# 住所を(都道府県)(市区町村)(市区町村以下)に分けて取り出す
def split_address(x):
a,b,c = re.search('(...??[都道府県])(.+?[市区町村])(.+)',x).groups()
return a,b,c
# それぞれを変数に入れる
suumo_tokyo[['都道府県','市区町村','市区町村以下']] = suumo_tokyo.apply(lambda x:split_address(x.住所),axis=1,result_type='expand')
住所を切り分ける主な目的は、23区の違いを測ることができるようになることです。
その違いに興味がない場合は別に切り分けなくてもいいような気がします。
住所は (都道府県)(市区町村)(それ以下) の型が基本になります。
地域によってはこの型にはまらないところもありますが、拾ってきたデータならこの型で問題ありませんでした(私が見た限り)。
都道府県は東京都しかないので東京都を直接指定しても問題ありません。仮に他の都道府県まで範囲を広げた時に取り回しがいいってだけです。
(その理論なら市区町村ももっと細かい指定のほうがいいのですが)
住所の正規表現は以下を参考にしました。
最寄り駅
# 最寄り駅変数(正規表現)
# 最寄り駅から路線と駅を取り出す
suumo_tokyo[['路線','駅']] = suumo_tokyo.apply(lambda x:re.search('(.+)/(.+?)\s(.+)',x.最寄り駅1).groups()[0:2],axis=1,result_type='expand')
def how_to_station():
# 移動手段は3パターン
how_to_station = ['歩','バス','車']
for by in how_to_station:
# 移動手段変数を生成
suumo_tokyo[f'{by}'] = np.zeros(suumo_tokyo.shape[0])
# その移動手段があるなら移動手段変数に格納する
suumo_tokyo.loc[suumo_tokyo['最寄り駅1'].map(lambda x: search_object(f'{by}[0-9]+分',x)),f'{by}'] = suumo_tokyo.loc[suumo_tokyo['最寄り駅1'].map(lambda x: search_object(f'{by}[0-9]+分',x)),'最寄り駅1'].map(lambda x: re.search(f'{by}([0-9]+)分',x).group(1))
# 所要時間をint型に変換
suumo_tokyo[f'{by}'] = suumo_tokyo[f'{by}'].astype(int)
# 移動手段ごとに所用時間変数を作る
how_to_station()
最寄り駅変数は3つあるうちの最寄り駅1だけ使用しました。3つあってどう使うか、使い道がイマイチ浮かびませんでした。
路線ごと、駅ごと、移動手段と時間に分けて切り出すことで、後で調べるときに幅が出ます。
最寄り駅は基本的に 「(路線)/(駅名) 歩??分」 と表記されています。
(例えば 都営大江戸線/赤羽駅 歩5分 など)
路線と駅名の間に /(スラッシュ) があり、駅名と移動時間の間に空白があります。/ と空白を使って3つに分けました。
路線名だと〇〇線、駅名は〇〇駅だから、線や駅で区切って良くね?と一瞬思いましたが、このパターンだと例外がありました。
路線だと「日暮里・舎人ライナー」や「つくばエクスプレス」など、駅名だと「大泉風致地区」がありました。
(大泉風致地区って駅なのか…?と思ってGooglemapで調べたら駅名ではありませんでした。そらそう。)
路線と駅名は切り分けるだけでOKでした。移動時間はさらに処理します。
基本的には 「歩??分」 (歩3分 など)と表記されています。
例外として、物件→バス停→駅 の移動時間が記載されている場合があります。
その場合は 「バス??分 (バス停の名前) 歩??分」 (バス14分 (バス停)南大和 歩5分 など)となります。
車移動が記載されている場合もあり、 「車??分(??km)」 (車8分(2.7km) など)となります。
それぞれで処理が違うので、「歩??分」「バス??分」「車??分」のそれぞれがあるかどうかで処理を分岐させます。
分岐した後は数字の部分を取り出すだけなので簡単です。
数字を取り出した後は、対応する移動手段に応じた変数に入れます。
.unique()で表記ゆれみたいなものがないことも確認しました。
人が入力する以上は、例えば「阿佐ヶ谷」と「阿佐ケ谷」と「阿佐谷」の違いが出ることもあると思いましたが、そのような表記ゆれらしいものは見当たりませんでした。
SUUMOのシステムが素晴らしいんだろうなーと思いました。多分駅名リストとか路線名リストとか、そういうデータベースがあるんでしょうね。
緯度経度もあるとさらに出来ることが増える?
前処理が完了して、データ分析をしているうちに思いつきました。
緯度経度の情報があれば位置関係をグラフ上に描画することもできますし、住所ごとの距離を測ることもできます。
駅と物件の距離を計算して、移動時間変数の代わりに使うとかも出来そうですね。
今はまだ思いついただけなので、うまく行ったらまた記事にするかもしれません。
終わり。
前処理をやってみて感じたことは、機械に融通するのも大変だなぁと思いました。とはいえ、機械の力を借りたほうが人だけで作業するよりも断然早く済んで精度も高いので、積極的に機械に融通するんですけどね。機械様万歳。
…人の曖昧さを補正してくれるAIとか作れるんかな?自然言語処理とかそういう分野の知識が現状ないのでわかりませんが。そのあたりに手を出し始めたらやってみるかもしれません。もう既にある?私が思いつくくらいだから誰か開発してるか。
それと、この記事を書くにあたって、完成したコードについての解説よりも、このコードを書くに至った経緯?前処理の試行錯誤?とかその辺のほうが皆さん興味あるんかな?とか思っていました。ご希望などあればぜひご意見ください。記事を書く余裕があれば書きます。
この後は前処理したデータを使ってデータ分析を進めます。いよいよ機械学習らしくなってきますね。それもある程度進んだらまた記事にするかもしれません。
他のSUUMO記事
まとめ記事書いたのでぜひご覧ください。