動機
職場が移転し、今の家から遠くなるため、引っ越したい。しかしどのあたりの賃貸価格が安いのか、賃貸検索サイトなど見ても詳細な情報がない。駅別の賃貸価格が載っていたりするが別々のページに情報があり、統合して理解することが難しい。各地点での賃貸価格をmapで示すことができれば面白いなと思ったのがこの記事を書くことにしたきっかけです。
スクレイピング
賃貸検索サイトから都心部(中央区、千代田区、文京区、港区、新宿区)、南部(品川区、目黒区、大田区、世田谷区、渋谷区)の物件情報をスクレイピング。データベースに格納。集めた情報は以下。
- 賃貸価格
- 住所
- 緯度
- 経度
- 間取り
- バス・トイレが別か
- オートロックの有無
以下のようなデータモデルを定義しデータベースに格納。個人的な勉強のためdjangoでデータモデルを定義しています。今回の分析で使っていないアトリビュートもありますが無視してください。
class Rentproperty(models.Model):
property_id = models.AutoField(primary_key=True)
# 登録日
date = models.DateTimeField(blank=True, null=True, auto_now=True)
# 賃料
rent = models.FloatField(blank=True, null=True)
# 管理費
kanrihi = models.FloatField(blank=True, null=True)
# 敷金
sikikin = models.FloatField(blank=True, null=True)
# 礼金
reikin = models.FloatField(blank=True, null=True)
# 物件名
subtitle = models.CharField(max_length=50, blank=True, null=True)
# 住所
location = models.CharField(max_length=50, blank=True, null=True)
# 緯度
latitude = models.FloatField(blank=True, null=True)
# 経度
longititude = models.FloatField(blank=True, null=True)
# 最寄り駅までの徒歩での所要時間
close_station = models.IntegerField(blank=True, null=True)
# 間取り
floor_plan = models.CharField(max_length=50, blank=True, null=True)
# 面積
area = models.FloatField(blank=True, null=True)
# 築年数
age = models.FloatField(blank=True, null=True)
# 階数
floor = models.IntegerField(blank=True, null=True)
# 向き
orientation = models.CharField(max_length=5, blank=True, null=True)
# バストイレ別かどうか
bath_toilet = models.BooleanField(blank=True, null=True)
# オートロック
auto_lock = models.BooleanField(blank=True, null=True)
# URL
url = models.CharField(max_length=1000, blank=True, null=True)
パーサーは以下。パーサーも今回関係ない情報集めています。
URL = 'http://www.geocoding.jp/api/'
SUUMO_URL_DICT = {
'中央区': 'https://suumo.jp/chintai/tokyo/sc_chuo/',
'千代田区': 'https://suumo.jp/chintai/tokyo/sc_chiyoda/',
'文京区': 'https://suumo.jp/chintai/tokyo/sc_bunkyo/',
'港区': 'https://suumo.jp/chintai/tokyo/sc_minato/',
'新宿区': 'https://suumo.jp/chintai/tokyo/sc_shinjuku/',
'品川区': 'https://suumo.jp/chintai/tokyo/sc_shinagawa/',
'目黒区': 'https://suumo.jp/chintai/tokyo/sc_meguro/',
'大田区': 'https://suumo.jp/chintai/tokyo/sc_ota/',
'世田谷区': 'https://suumo.jp/chintai/tokyo/sc_setagaya/',
'渋谷区': 'https://suumo.jp/chintai/tokyo/sc_shibuya/'
}
class SuumoParser:
def __init__(self, url):
self.url = url
self.pages_num = self.get_pages()
self.urls = self.get_urls()
def get_pages(self):
result = requests.get(self.url)
content = result.content
soup = BeautifulSoup(content)
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>')
pages_num = pages_split[0][-3:].replace('>','')
pages_num = int(pages_num)
return pages_num
def get_summary(self, url):
results = requests.get(url)
content = results.content
soup = BeautifulSoup(content)
summary = soup.find("div", {"id":"js-bukkenList"})
return summary
def get_urls(self):
urls = []
urls.append(self.url)
for i in range(self.pages_num-1):
pg = str(i+2)
url = self.url + '?page=' + pg
urls.append(url)
return urls
def insert_db(self, url):
summary = self.get_summary(url)
cassetteitems = summary.find_all("div",{'class':'cassetteitem'})
for cassetteitem in cassetteitems:
try:
title = self._get_title(cassetteitem)
address = self._get_address(cassetteitem)
latitude, longititude = self._get_coordinate(address)
age = self._get_age(cassetteitem)
close_station = self._get_close_station(cassetteitem)
tables = cassetteitem.find_all('table')
for table in tables:
# date = datetime.now
rent = self._get_rent(table)
kanrihi = self._get_administration(table)
sikikin = self._get_sikikin(table)
reikin = self._get_reikin(table)
area = self._get_area(table)
floor = self._get_floor(table)
floor_plan = self._get_floor_plan(table)
detail_url = self._get_detail_url(table)
url_all = urllib.parse.urljoin(url, detail_url)
bath_toilet, auto_lock = self._get_details(url_all)
# print(url_all)
rp = Rentproperty(
# date=date,
rent=rent,
kanrihi=kanrihi,
sikikin=sikikin,
reikin=reikin,
subtitle=title,
location=address,
latitude=latitude,
longititude=longititude,
close_station=close_station,
floor_plan=floor_plan,
area=area,
age=age,
floor=floor,
bath_toilet=bath_toilet,
auto_lock=auto_lock,
url=url_all
)
if len(Rentproperty.objects.filter(url=url_all).all()) == 0:
rp.save()
else:
rp = Rentproperty.objects.filter(url=url_all).first()
rp.save()
except Exception as e:
print(e)
def _get_title(self, cassetteitem):
#マンション名取得
try:
subtitle = cassetteitem.find_all("div",{
'class':'cassetteitem_content-title'})
subtitle = str(subtitle)
subtitle = subtitle.replace('[<div class="cassetteitem_content-title">', '')
subtitle = subtitle.replace('</div>]', '')
except Exception as err:
print('_get_title: ', err)
return subtitle
def _get_address(self, cassetteitem):
#住所取得
try:
subaddress = cassetteitem.find_all("li",{'class':'cassetteitem_detail-col1'})
subaddress = str(subaddress)
subaddress = subaddress.replace('[<li class="cassetteitem_detail-col1">', '')
subaddress = subaddress.replace('</li>]', '')
return subaddress
except Exception as err:
print('_get_address: ', err)
def _get_age(self, cassetteitem):
try:
col3 = cassetteitem.find("li",{'class':'cassetteitem_detail-col3'})
col = col3.find('div')
age = col.find(text=True)
age = age[1:-1]
except Exception as err:
print('_get_age: ', err)
try:
age = float(age)
except Exception as e:
age = None
return age
def _get_rent(self, table):
try:
rent = table.find("span", {"class":"cassetteitem_price cassetteitem_price--rent"})
rent = rent.text
if '万円' in rent:
rent = str(rent)[:-2]
rent = float(rent)
else:
rent = None
return rent
except Exception as err:
print('_get_rent :', err)
def _get_administration(self, table):
try:
rent = table.find("span", {"class":"cassetteitem_price cassetteitem_price--administration"})
rent = rent.text
if '円' in rent:
rent = str(rent)[:-1]
rent = float(rent)
rent /= 10000
else:
rent = 0
return rent
except Exception as err:
print('_get_administration: ', err)
def _get_sikikin(self, table):
try:
rent = table.find("span", {"class":"cassetteitem_price cassetteitem_price--deposit"})
rent = rent.text
if '万円' in rent:
rent = str(rent)[:-2]
else:
rent = 0
return rent
except Exception as err:
print('_get_sikikin: ', err)
def _get_reikin(self, table):
try:
rent = table.find("span", {"class":"cassetteitem_price cassetteitem_price--gratuity"})
rent = rent.text
if '万円' in rent:
rent = str(rent)[:-2]
rent = float(rent)
else:
rent = 0
return rent
except Exception as err:
print('_get_reikin: ', err)
def _get_area(self, table):
try:
area = table.find("span", {"class":"cassetteitem_menseki"})
area = area.text
if 'm2' in area:
area = area[:-2]
area = float(area)
else:
area = None
return area
except Exception as err:
print('_get_area: ', err)
def _get_close_station(self, cassetteitem):
try:
station_list = cassetteitem.find_all("div", {"class":"cassetteitem_detail-text"})
station_list_min = []
for station in station_list:
station = station.text
if '歩' in station and '分' in station:
start = station.find(' 歩')
end = -1
station_min = station[start+2:end]
station_min = int(station_min)
station_list_min.append(station_min)
if len(station_list_min):
return min(station_list_min)
else:
return None
except Exception as err:
print('_get_close_sation: ', err)
def _get_floor_plan(self, table):
try:
floor_plan = table.find("span", {"class":"cassetteitem_madori"})
floor_plan = floor_plan.text
return floor_plan
except Exception as err:
print('_get_floor_plan: ', err)
def _get_floor(self, table):
try:
contents = table.find_all("td")
floor = contents[2].text
floor = floor[:-1]
floor = int(floor)
return floor
except Exception as err:
print('_get_floor :', err)
def _get_detail_url(self, table):
try:
url = table.find("td", {"class":"ui-text--midium ui-text--bold"}).find("a").get("href")
return url
except Exception as err:
print('_get_detail_url: ', err)
def _get_details(self, detail_url):
"""detail url1を受け取り、「部屋の特徴・設備」の中身を返す"""
try:
results = requests.get(detail_url)
content = results.content
soup = BeautifulSoup(content, features="html.parser")
summary = soup.find("div", {"class":"section l-space_small"}).find("li").text
if summary == "":
return None, None
else:
bath_toilet = 'バストイレ別' in summary
auto_lock = 'オートロック' in summary
return bath_toilet, auto_lock
except Exception as err:
print('_get_details :', err)
def _get_coordinate(self, address):
try:
if len(AddressCoordinate.objects.filter(address=address)):
instance = AddressCoordinate.objects.get(address=address)
time.sleep(10)
return instance.latitude, instance.longititude
else:
latitude, longititude = _coordinate(address)
ac = AddressCoordinate(address=address,
latitude=latitude,
longititude=longititude)
ac.save()
return latitude, longititude
except Exception as err:
print('_get_coordinate: ', err)
def _coordinate(address):
payload = {'q': address}
html = requests.get(URL, params=payload)
soup = BeautifulSoup(html.content, "html.parser")
if soup.find('error'):
raise ValueError(f"Invalid address submitted. {address}")
latitude = soup.find('lat').string
longitude = soup.find('lng').string
time.sleep(10)
return latitude, longitude
if __name__ == '__main__':
for city_name, city_url in SUUMO_URL_DICT.items():
sp = SuumoParser(city_url)
urls = sp.get_urls()
for i, url in enumerate(urls):
try:
print(city_name, i, url)
sp.insert_db(url)
except Exception as err:
print(err)
DBからデータの取り出し
各住所での平均の家賃を取り出したいので、基本のクエリ文はSELECT AVG(rent), location FROM rentproperty GROUP BY location
となります。加えて、ある程度条件を絞って各住所での平均を取らないと賃料の高さが純粋な場所由来なのか面積やオートロック有無などの条件の場所での偏りによるものなのか分からなくなると思い、条件をある程度絞りました
- 1K
- オートロック有り
- バストイレ別
- 25 m2 以上30 m2以下
これを踏まえると、クエリ文は、SELECT AVG(rent), location WHERE floor_plan='1K' AND auto_lock=True AND bath_toilet=True AND area > 25 and area < 30 FROM rentproperty GROUP BY location
となります
mapでの平均賃料の可視化
地図表示にはgeopandas
というライブラリを使いました。本当は住所などが記載されているmapに重ね合わせたものを作成したかったのですが方法がわかりませんでした。
こちらみるとやはり港区、千代田区が高く、都心から離れるにつれ安くなっていることがわかります。局所的に平均賃貸価格が高くなっている場所や安くなっている場所があるところが面白いです。N数がただ足らない、新しい家や古い家が偏って立っているということも考えられますが、もしかすると割高な地区、お得な地区というのがあるのかもしれません。