akatak’s blog

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

Flaskで読書記録Webアプリを作る(1)

先日、日経ソフトウェア(2019年3月号)を見ていたら『Flask+SQLite3でWeb-DBアプリ開発!蔵書管理データベースで基本をマスターしよう』という記事が掲載されていました。それはそれで作ってみたのですが、sqlite3に依存するコードとなっていたので、将来的にHeroku等のクラウドサービスへデプロイし、PostgresSQLの利用を考える場合には、コードを書き換えなくてはいけなくなります。

そこで、上記の記事を参考にしつつ、データーベースの種類が変わってもコードの変更が最小限になるように、SQLAlchemy等を利用した簡単な読書記録Webアプリを新たに作りましたので、次回以降ご紹介したいと思います。今回は、そのWebアプリで利用したパッケージを先にご紹介します。

Flaskとは

Flaskは、PythonによるWebアプリケーション開発用のフレームワークです。開発者がマイクロフレームワークと呼んでいるように軽量で最小限の機能しかない一方で、柔軟性が高いという特徴を持っています。Pythonにおける有名なフレームワークであるDjangoはWebアプリケーションの作成までの決まりごとが多く、ハードルが高いため、取り敢えずでいいからWebアプリケーションを簡単に作ってみたいという方には Flaskがオススメです。

Flask公式ホームページ

SQLAlchemyとは

SQLAlchemyはPythonのORM(OBject Relational Mapper)です。Python用ORMの中では人気があり、広く利用されているようです。

ORM(Object-relational mapping、オブジェクト関係マッピング)とは、データベースとオブジェクト指向プログラミングの間の非互換なデータを変換するプログラミング技法のことをいいます(出典:Wikipedia)。Object-relational Mapperは、そうしたプログラム技法を利用したパッケージのことを指しています。

これにより、SQLiteMySQLPostgreSQLといったデータベースの各々異なるSQLを直接記述することなく、Pythonオブジェクトを通じて、データベースを扱えることができます。加えて、データベースの種類に関係なく、共通のコードを利用してデータベースを取り扱うことができるので、コードを変更せずに、自分のパソコンではSQLiteを利用して、Herokuにデプロイした後はPostgrteSQLを利用するといったことが可能となります。

SQLAlchemy ORM Tutorial

Flask-SQLAlchemyとは

Flask-SQLAlchemyは、FlaskにおいてSQLAlchemyをより簡単に利用するための拡張パッケージです。これにより、Flaskからより簡単にSQLAlchemyを利用できるようになります。

例えば、Flask-SQLAlchemyでは、dbオブジェクトを通じて、テーブルの作成や検索を実行できるようになります。SQLAlchemyのみの場合とFlask-SQLAlchemyを利用した場合の違いは、例えば以下の通り。

【導入(前置き)部分】
▪️SQLAlchemyのみの場合

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String

engine = create_engine('sqlite:////tmp/test.db')
Base = declarative_base()

class User(Base):
    id = Column(Integer, primary_key=True)
    Username = Column(String)
    email = Column(String)
    def __repr__(self):
        return '<User %r>' % self.username

▪️Flask-SQLAlchemyを利用した場合

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    Username = db.Column(db.String)
    email = db.Column(db.String)
    def __repr__(self):
        return '<User %r>' % self.username

【テーブルの作成】
▪️SQLAlchemyのみの場合

Base.metadata.create_all(engine)

▪️Flask-SQLAlchemyを利用した場合

db.create_all()

【データの追加・コミット(その準備)】
▪️SQLAlchemyのみの場合

from sqlalchemy.orm import sessionmaker
from yourapplication import User

Session = sessionmaker(bind=engine)
session = Session()

user1 = User(username='user1', email='user1@example.com')

session.add(user1)
session.commit()

▪️Flask-SQLAlchemyを利用した場合

from yourapplication import db, User

user1 = User(username='user1', email='user1@example.com')

db.session.add(user1)
db.session.commit()

【テーブルを検索】
▪️SQLAlchemyのみの場合

session.query(User).order_by(User.id)

▪️Flask-SQLAlchemyを利用した場合

db.session.query(User).order_by(User.id)

これ以上は省略しますが、flask-sqlalchemyを利用した方が、dbオブジェクトを通じて、テーブルを作成したり、検索したりできますので、便利かと思います。

flask-sqlalchemy Documentation

Flask-Migrateとは

Flask-Migrateは、Flaskアプリケション用のデータベース移行ツールで、Alembicというパッケージを利用しています。このFlask-Migrateにより、コマンドラインによるデータベース操作が可能となります。

例えば、'flask db init'とコマンドラインに入力すると、models.py等のPythonモジュールに記載された各クラスからデータベースのテーブルを作成してくれますので、Pythonスクリプトを立ち上げて、db.create_all()等をいちいち実行しなくて済みます。また、データベースの構造を変えていく際に、履歴を保存するほか、データの移行を実行したり、戻したりすることが可能となります。

flask-Migrate Documentation

なお、AlembicはSQLAlchemyの作者が作成したデータベース移行ツールで、以下の機能を提供しています(alembic 1.0.10)。
- テーブルその他の構造を変更するためにデータベースに対してALTERステートメントを発行
- 移行のためのスクリプトを記述する仕組みを提供。各スクリプトは、対象となるデータベースを新しいバージョンにアップグレードできる特定の一連の手順と、オプションで、同じ手順を逆の順序でダウングレードできる一連の手順を示す
- スクリプトを順次実行することを許可

flask-WTFとは

flask-WTFはWTFormsというパッケージをFlaskで利用可能とするパッケージです。WTFormsは、flaskその他のWebフレームワークで利用可能なパッケージの一つで、Pythonを利用して各種フォームの作成や入力時のチェックを柔軟に行うことができます。

flask-WTF Documentation

今回は以上です。次回以降、具体的にコーディング事例をご紹介します。

令和元年を迎えて

令和元年となりました。新たな素晴らしい時代の幕開けとなりますよう祈念します。皆様はいかがお過ごしでしょうか。私自身は、具体的な目標を持って、少しづつでも良いので前に進んでいけたらいいなぁと 思っています。

一方で、最近、ブログの更新頻度がとみに減ってきてしまいました。言い訳としてはアウトプットの前提としてのインプットに注力している、ということになりますでしょうか。インプットとアウトプットのバランスが難しいと感じています。日々更新されている方は本当にすごいと感心します。

具体的に現在インプットに注力している分野は、①Flask ②統計 ③機械学習です。それぞれ簡単に説明しましょう。

Flask

PythonにおけるWebフレームワークの一つで、Djangoと比較すると軽量で機能が限定的である一方で、学習コストが低いという特徴があります。一時期、Djangoを勉強したこともあるのですが、簡単なアプリケーションを作るまでに時間がかかり、半年くらいブランクを空けたら、すっかり忘れてしまいました(笑)。今回、Flaskに宗旨替えし、簡単なWebアプリケーションを作りましたので、別の機会にご紹介します。

主に学習に利用したのが、①Udemy(オンライン学習サイト)、②書籍、③ウェブページです。Flaskだけでなく、Pythonの学習にはこの組合せが多いです。

Udemyのオンライン講座は定価は1〜3万円するのですが、定期的に割引を行う期間があって、その際には、1200〜1800円で購入できるのでお得です。日本語の講座に比べて英語講座は内容が充実しています(20〜30時間の講座でも、場合によっては1200円で買える!)。最近は講座によっては日本語の字幕も付いている場合があるので、ハードルは多少低くなっていると思います。私は、Jose Portilla氏の講座が分かりやすく、エクササイズも適度にあり、気に入っているため、いくつかの講座でお世話になっています。以下のFlaskの講座もそうです(残念ながら日本語の字幕はまだないようです)。 www.udemy.com

書籍は、主に以下の2冊を利用しました。前者は、簡単なBlogアプリを作成しながら、Flaskの全体像を把握するのに分かりやすいです。後者は、英語ですが、Mega-Tutorialと題するだけあって、flask-sqlalchemyやflask-migrateなどの機能も具体的に説明があって良いかと思います。

ホームページは、以下のFlaskチュートリアルなどを利用して学習してきました。

a2c.bitbucket.io

統計

統計を勉強しようと思った理由は主に2つあります。1つは不動産の分析において、重回帰分析を使いましたが、その解釈を含めて、掘り下げる必要があると感じたこと。また、もう1つは、Pythonによる機械学習の勉強をしていくと、どうしても統計を理解していないと単なる作業になってしまい、差別化につながらないと思ったことです。

そこで思い立って、統計検定2級を受験することにしました!

www.toukei-kentei.jp

学習に利用しているのは、主に書籍です。以下の3冊にて学習を進めています。

入門の名に相応しく、網羅的に概念的な理解をするのに適した書籍です。数式は最小限に抑えられており、数式を使って理解したい方には向きません。統計の初学者にとっては、記述統計(平均・分散・標準偏差)はもちろん、確率分布(正規分布、標準正規分布、t分布、χ2分布、F分布)から検定・多変量解析・実験計画法まで、更にはノンパラメトリック手法・因子分析・主成分分析も一応網羅しており、一通りの概念を理解するのは良い本かと思います。

基本統計学 第4版

基本統計学 第4版

上記と同じような範囲を、数理的な側面からしっかり説明しているほか、具体例も豊富。また、標本が少ない場合の検定等でなぜ自由度n-1を使うのかを厳密に説明しています。上記入門とセットでみると完璧かなと思います。これで受からなかったら...自分の理解力の問題。

日本統計学会公式認定 統計検定 2級 公式問題集[2016〜2018年]

日本統計学会公式認定 統計検定 2級 公式問題集[2016〜2018年]

これらについても、記事にまとめていけたらと思っています。

機会学習

これについても ①Udemyと②書籍を中心に学習しています。

www.udemy.com

機械学習の一部ですが、ビジネスの実例を分析していきますので、導入としては良いかと思います。

www.udemy.com Jose Portilla氏の講座です。機械学習を網羅的に学習するのには良いと思います。今、学習中です。日本語字幕も使えます。

その他、Udemyでは、日本語で機械学習の講座も増えてきていますので、それらを利用するのも手かと思います。もちろん割引が適用されるタイミングで、出来れば1200円の時を狙いたいものです。

機械学習における日本語の書籍も最近いろいろと出ていますね。理論面からの勉強には、以下が無料で良いとJose Portilla氏の講座で勧められ、ちょっとずつ読み進めていますが、何せ英語。時間がかかっています。日本語訳もあるようですが、高すぎ。地道にやっていこう。まずは上記の統計の勉強を優先。その後、読み進めていきます。これも纏め記事が書けたら良いなとは思っています。

www-bcf.usc.edu

以下が上記の日本語版。最近出たようですが、値段が高め。

Rによる 統計的学習入門

Rによる 統計的学習入門

  • 作者: Gareth James,Daniela Witten,Trevor Hastie,Robert Tibshirani,落海浩,首藤信通
  • 出版社/メーカー: 朝倉書店
  • 発売日: 2018/08/03
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (1件) を見る

最後に

以上の学習が一段落したら、不動産価格分析の精緻化や資産運用への応用などまだまだやりたいことがあります。新たな時代になったことですし、また、新たな気持ちで前向きに取り組んでいければと思います。

Pythonによる不動産情報のデータ取得&分析(6)【売却物件/分析編】

前回の記事から随分時間がたってしまいました。

昨年10月以降、仕事の関係で海外出張に暫く行っていたほか、現在行っている不動産分析結果を参考にもしながら年末には遂に引越しすることになりまして、片付けから新居の準備と非常に忙しく、そうこうしているうちに年明け。すっかりPythonから離れておりました。

前回記事では中途半端でありましたので、不動産売却物件編(中古マンション)について、今回で一旦完結したいと思っております。

データの基礎分析

前回の記事で前処理として加工したデータを取り込んで、基礎的な分析をしておきましょう。データは前回のものを利用しているので、半年ほど古いですが、ご容赦ください。

まずは、おまじないのごとく、いつものように必要なライブラリを取り込んでおきます。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

次に、前回加工したデータをpandas のDataFrameとして取り込みます。

df = pd.read_csv('data/suumo_used_mansion_mod_owner.csv', index_col=0)

データの概要を見ておきましょう。 f:id:akatak:20190302223111j:plain

14000件ほどのデータがあることが分かります。

前回、加工仕切れていませんでした。不動産価格や平米当りの単価が円単位だと、数値が大きくなってしまうので、万円単位に変更し、新たな列として保存しておきます。

df['価格_万円'] = df['価格']/10000
df['単価_万円'] = df['単価']/10000

さて、賃貸物件の場合と同様に、売却物件数を市区毎に比較してみましょう。 groupbyにて、市区別にデータをカウントし、件数の多い順にソートして表示してみます。

f:id:akatak:20190302225006j:plain

売却物件数と人口を散布図にプロットしたら、以下の通り。

f:id:akatak:20190302225314j:plain

賃貸物件と同様に、相関が見られますね。 次に、seabornをimportして、市区別に単価を比較してみましょう。

f:id:akatak:20190302225634j:plain

更に、築年数と平米単価をグラフ化してみます。

f:id:akatak:20190302230106j:plain

築年数毎に平均平米単価を算出してプロットしてみます。

f:id:akatak:20190302230424j:plain

築50年を超えるとデータ数の減少に伴い歪になりますが、それまでは傾向をよく表している感じですね。

次に、駅からの距離(徒歩何分か)と平均平米単価をプロットしてみましょう。 f:id:akatak:20190303075240p:plain

40分を超えた所に異常値がありそうです。 f:id:akatak:20190303085246p:plain

徒歩91分は流石に異常値なので、除外します。 f:id:akatak:20190303085636p:plain

重回帰分析

さて、ここからは重回帰分析を行っていきましょう。 まず、重回帰分析の説明変数に使うデータの種類を特定します。不動産価格に影響を与えると思われるものを取り敢えず、df_for_analysis_baseとして設定することにします。また、欠損値のあるデータを除外しておきます。

f:id:akatak:20190303091041p:plain

重回帰分析を行うためにscikit-learnライブラリをimportします。

import sklearn
from sklearn.linear_model import LinearRegression as LR

次に、予測モデル作成にあたり、

  1. LinearRegressionクラスを読み込みます。
  2. 説明変数Xに、df_for_analysis_baseから「単価万円」「部屋数」「総戸数」「路線」を除いたものを取り敢えず設定します。「単価万円」は被説明変数なので除外。「部屋数」「総戸数」は単価に殆ど影響がないため除外。「路線」を入れると「駅」と相関が高いため、多重共線性の影響により、回帰係数が異常値となっていますので、除外しています。
  3. 被説明変数yに「単価_万円」を設定します。
  4. 「市区」「駅」「手段」など、質的データをダミー化します。

f:id:akatak:20190303115115p:plain

そして、予測モデルを作成し、結果である回帰係数と切片を呼び出します。

f:id:akatak:20190303105140p:plain

決定係数は76%とまずまずのようです。この結果を見てみると、概略以下のことが言えるかと思います。

  • 築年数が1年経過すると、平米単価で約1.1万円安くなる
  • 駅からの距離が1分遠くなると、平米単価で約1.4万円安くなる
  • 物件階が1階高くなると、平米単価が約1万円高くなる
  • 市区だけでなく、最寄駅の銘柄によって結構、単価が変わる

ただし、方角については、必ずしも南向きが高くなっていないので、実感と異なる状況です。現在、統計については勉強している最中ですので、将来的にこのモデルの評価についてもう少し掘り下げて行いたい、必要があれば修正したいとは思っています。まだ、先になりそうですが。

さて、この結果をダウンロードして、Excel上で簡単に不動産価格を試算できるスプレッドシートを作成しましたので、ご参考までにアップします。ただし、モデルの検証もきちんと行っていないので、あくまで参考の域をでないこと、また、分析に利用したデータは半年以上前のものですので、ご了承ください。

drive.google.com

本日は以上です。

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

akatak.hatenadiary.jp

さて、前回記載したスクリプトでSUUMOさんのサイトからスクレイピングしましたデータを、分析しやすいようにきれいにしていきましょう。このデータの前処理は、データの状況にもよりますので、なかなか完全な自動化は難しいですよね。自動化できる良い方法があれば、どなたか是非教えて下さい!

さて、例によって、Jupyter Notebookにてデータ前処理を行っていきましょう。データの状況を都度確認しながら、データ前処理を実行していくには、Jupyter Notebookは非常に有効だと思います。まだ、お使いでない方は是非試していただきたいと思います。

まず、データを取り込みましょう。pandas numpy reをインポートします。re正規表現を利用して文字列を操作するために必要なモジュールです。

前回保存しておいたcsvファイルをpandasのdataframeとしてdfに読み込みます。index_col=0として、ファイルの0列目(pythonでは0から始まる)をindex列として指定します。引数でこれを指定しないと、pandasが自動的に0から数値をindexとして割り付けてしまいます。

import pandas as pd
import numpy as np
import re

df = pd.read_csv('data/suumo_used_mansion.csv', index_col = 0)

info()にてdataframeの概要を見てみます。また、head()にて最初の5行もどんな感じか見ておきましょう。

f:id:akatak:20180915061142p:plain f:id:akatak:20180915061350p:plain

約23000行のデータ系列ですね。ところが、データ系列をExcel等に取り込んで眺めてみると分かるのですが、同じ物件のデータが場合によっては3個も4個もあるいはもっとあります。異なる業者が同じ物件をSUUMOさんのホームページに登録しているようです。

そこで重複行を削除してしまいましょう。pandasには「重複行を抽出」したり、「重複行を削除」するメソッドが準備されています。前者がduplicated()、後者がdrop_duplicates()です。

まず、duplicated()を利用して、どの位重複があるのかを確認しましょう。引数subsetで重複を確認する列を指定できます。また、keep=Falseと指定すると、重複行全てTrueを返します(keep='first'の指定で、重複行のうち最初に出てくる行がFalseとなり、それ以外がTrue。keep='last'とすると最後の行がFalseとなり、それ以外の行がTrueとなります)。sum()で合計すると重複行をカウントできます。

f:id:akatak:20180915075911p:plain

9216も重複していますね。 物件名が「オリエンタル大森」のもののみ抽出して確認すると、以下の通り。

f:id:akatak:20180915080122p:plain

重複行の最初の行のみ残して、後は削除してしまいましょう。

df.drop_duplicates(subset=['物件名', '価格', '専有面積', '間取り'], keep='first', inplace=True)

ただし、Excel等に取り込んだデータをよく見てみると物件名が「ザ・パークハウス小石川春日」と「◆ザ・パークハウス小石川春日◆2駅4路線利用可!」と異なっていますが、その他の列を見ると専有面積もバルコニー面積も価格も住所も一緒のものがあります。

完全ではないにしろ、ある程度きれいにしておきたいところですので、これらも除外しておきましょう。

f:id:akatak:20180915081308p:plain

さて、相当数を削除した結果、indexが歯抜け状態になっていますので、indexを付け直しましょう。

df.reset_index(drop=True, inplace=True)

ここで、データ系列の概要を見てみると、以下の通り、データ数は14631個になりました。

f:id:akatak:20180915081840p:plain

さて、ここから、賃貸物件と同様に、データを加工していきます。

# 文字列を複数文字で split。住所から「都県」列・「市区」列を新たに作成
df['都県'] = df['住所'].apply(lambda x: re.split('[都県]', x)[0]) 
df['市区'] = df['住所'].apply(lambda x: re.split('[都県市区]', x)[1]) 

次に、築年数を計算するために以下の加工・計算を行います。

from datetime import datetime

df['築年'] = df['築年月'].apply(lambda x: x.split('年')[0])
df['築月'] = df['築年月'].apply(lambda x: x.split('年')[1].strip('月'))
df['築月'] = df['築月'].apply(lambda x: x if x != '' else '6')

df['築年月'] = df['築年'] +'/'+ df['築月'] +'/15'  # 一律に日付を15日に設定
df['築年月'] = pd.to_datetime(df['築年月'])  #  stringからdatetimeオブジェクトに変換
df['築日数'] = df['築年月'].apply(lambda x:(datetime.today() - x).days) #今日までの日数計算
df['築年数'] = df['築日数']/365.25 # 年数(概算)を計算(うるう年も考慮)

計算の過程で作った「築年」「築月」「実日数」の列は分析でも不要なので、削除してしまいます。

df.drop(['築年','築月','築日数'], axis=1, inplace=True)

続きまして、「1億円」「1080万円」等のStringとなっているものを、数値に変換していきます。まずは、「4998万円※権利金含む」などの記載があるものの処理から。

df['価格'] = df['価格'].apply(lambda x: x.split('※')[0] if '※権利金' in x else x)

次に「億円」表示のものを「万円」に変換。また、「1億800万円」のような表示を「万円」に変換します。

df['価格'] = df['価格'].apply(lambda x: x.strip('億円') + '0000万円' if '億円' in x else x)
df['価格'] = df['価格'].apply(lambda x: str(int(x.split('億')[0]) *10000 + int(x.split('億')[1].split('万円')[0])) + '万円' if '億' in x else x)

念のため、下2文字が「万円」でないものがあるか確認すると、 f:id:akatak:20180915083628p:plain

一つだけだけありました。仕方が無いので、個別で調整します。

df.iloc[2761,2] = '2335.6万円'

後は、範囲で指定しているものもありますが、これも個別で上下の平均値でも入れておきましょう。 f:id:akatak:20180915083937p:plain f:id:akatak:20180915083947p:plain

価格の仕上げとして、数値化しておきます。

df['価格'] = df['価格'].apply(lambda x: int(float(x.strip('万円')) * 10000))

間取り、部屋数、路線、駅、手段、分は賃貸物件の場合と同様に処理します。

df['間取り'] = df['間取り'].str.replace('ワンルーム', '1RM')
df['部屋数'] = df['間取り'].apply(lambda x: int(re.search('[0-9]+', x).group(0)))
df['路線'] = df['最寄駅'].apply(lambda x: x.split('「')[0])
df['駅'] = df['最寄駅'].apply(lambda x: x.split('「')[1].split('」')[0])
df['手段'] = df['最寄駅'].apply(lambda x: x.split('「')[1].split('」')[1][0])
df['手段'] = df['手段'].apply(lambda x: x.replace('徒', '歩')) # 手段の「徒」を「歩」に変えます。
df['分'] = df['最寄駅'].apply(lambda x: re.search('[0-9]+',x.split('「')[1].split('」')[1]).group(0))

あと、今回は物件階の影響も調べたいので、加工していきましょう。 データを一覧すると f:id:akatak:20180915084551p:plain

うわー、仕方ないですね。一つ一つ変換していきましょう。

df['物件階'] = df['物件階'].str.replace('B2階','-2階')
df['物件階'] = df['物件階'].str.replace('B1階','-1階')
df['物件階'] = df['物件階'].str.replace('2-3階','2.5階')
df['物件階'] = df['物件階'].str.replace('5-6階','5.5階')
df['物件階'] = df['物件階'].str.replace('3-4階','3.5階')
df['物件階'] = df['物件階'].str.replace('B1-1階','-0.5階')
df['物件階'] = df['物件階'].str.replace('1-2階','1.5階')
df['物件階'] = df['物件階'].str.replace('4-5階','4.5階')
df['物件階'] = df['物件階'].str.replace('6-7階','6.5階')
df['物件階'] = df['物件階'].str.replace('1-3階','2階')
df['物件階'] = df['物件階'].str.replace('B1-2階','1階')
df['物件階'] = df['物件階'].str.replace('8-9階','8.5階')
df['物件階'] = df['物件階'].str.replace('1--1階','0.5階')
df['物件階'] = df['物件階'].str.replace('B1-3階','1階')
df['物件階'] = df['物件階'].str.replace('1-1階','1階')
df['物件階'] = df['物件階'].str.replace('46-47階','46.5階')
df['物件階'] = df['物件階'].str.replace('37-38階','37.5階')
df['物件階'] = df['物件階'].str.replace('B1.5階','-1.5階')
df['物件階'] = df['物件階'].str.replace('B2階','-2階')
df['物件階'] = df['物件階'].apply(lambda x: float(x.strip('階')) if x != '-' else x.replace('-', ''))

総戸数も数値に変換します。

df['総戸数'] = df['総戸数'].apply(lambda x: int(x.strip('戸')) if x !='-' else x.replace('-', ''))

リフォームは有無のみ「リフォームflag」に入れます。有り=True・無し=Falseです。

df['リフォームflag'] = df['リフォーム'] != "['-\\r\\n\\t\\t\\t']"

今回は、単価を分析の対象としたいので、追加します。

df['単価'] = df['価格'] / df['専有面積'] #単価(単位):円/㎡

権利形態としてとして各種「借地権」の物件もありますが、分析の対象は「所有権」のものに絞りたいと思います。最後に加工後のデータを保存します。

df_owner = df[df['権利形態'] == '所有権']
df_owner.to_csv('data/suumo_used_mansion_mod_owner.csv')

以上、駆け足でしたがいかがでしたか。結構骨が折れる作業かと思います。慣れてくると、あーでもないこーでもないと考えることを楽しみながら、加工できますので、作業自体はそんなに苦痛ではないですよ。

分析は次回にしましょう。それでは。

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

9月になりましたが、まだまだ、暑い日が続きますね。

Pythonによる不動産情報でデータ取得&分析については、賃貸物件編が一応終わりましたので、今回から売却物件編に進みたいと思います。

前回までの記事は、参考までにこちらにリンクを張っておきます。

akatak.hatenadiary.jp

akatak.hatenadiary.jp

akatak.hatenadiary.jp

売却物件編についても、Suumoさんからデータ取得を行いたいと思います。マンションの中古物件に特定して分析を行っていきますので、今回は目黒区の中古物件データを取得するために、以下のサイトの情報を取得しましょう。

# SUUMO 中古マンション 目黒区
url = 'https://suumo.jp/jj/bukken/ichiran/JJ010FJ001/?ar=030&bs=011&ta=13&jspIdFlg=patternShikugun&sc=13110&kb=1&kt=9999999&mb=0&mt=9999999&ekTjCd=&ekTjNm=&tj=0&cnb=0&cn=9999999&srch_navi=1'

まずは、トップページの情報を取得します。

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

続いて、ページ数を取得しましょう。ページ数に基づき、全てのページのURLのリストを作成します。

# ページ数を取得
s = soup.find("div", {"class": "pagination pagination_set-nav"})
num_pages = int(s.find_all("a")[-2].string)

# 全てのページのURLを作成
urls = []
urls.append(url)
for i in range(num_pages-1):
    pg = str(i+2)
    url_page = url + '&pn=' + pg
    urls.append(url_page)

さて、賃貸物件と同様にデータを取り込むリスト(data= [ ] )とエラーを取り込むリスト(errors = [ ])を作っておきましょう。

data = []
errors = []

ここで、各ページ毎にURLを取得し、必要なデータをスクレイピングしていきます。 エラーが起きた時のデータの位置を把握するために、kという箱を用意し、ナンバリングしていきます。また、エラーが発生した際にスクリプトを実行し続けるとともに、どこでどんなエラーが発生したのか把握するため、try〜exceptを使います。

for url in urls:
    k = 0
    try:

    # ここにエンジン部分を記載します。
        
    except Exception as e:
        errors.append([e, url, k, subtitle])
        pass

さて、ここからが本番です。エンジン部分を作っていきます。 各ページ毎にデータを取得して、物件毎のデータをunitsに保管します。

result = requests.get(url)
c = result.content
soup = BeautifulSoup(c, "html.parser")
summary = soup.find("div",{'id':'js-bukkenList'})
units = summary.find_all("div",{'class':'property_unit'})

次に、unitsに保管された物件データを一つ一つ取り出して、必要なデータを取得していきます。 賃貸物件の場合と異なり、今回は各物件のページだけですと、データが限られていますので、そのページから更に詳細なデータが記載されているページに移動してデータを取得します。ですので、以下の通り、詳細ページへのリンク、データ取得を行っておきます。

# 物件詳細情報へのリンク
h2 = unit.find('h2', {'class':'property_unit-title'})
a = h2.find('a')
href = a.get('href')
link = 'https://suumo.jp' + href + 'bukkengaiyo/'

result_child = requests.get(link, timeout=10)
c_child = result_child.content
soup_child = BeautifulSoup(c_child, "html.parser")
summary_child = soup_child.find_all('tbody', {'class':'vat tal'})

その上で、各変数にデータを取り込んでいきます。実は、ここが個別ではうまく取り込めなかったりして、テクニカルには大変なのですが、最終形だけ掲載いたします。リフォームの箇所のみうまくテキストが抽出できず、リスト形式にしていますが、ご了承ください。将来的には何とかしたいと思いますが、今はリストの中身よりも、リフォームの有無のみ取り上げたいので、中途半端ですが許容いただければと思います。

# 新着、価格更新等の情報
try:
    info = unit.find('span',{'class':'ui-label ui-label--cta1 ui-label--cta4'}).string
except:
    pass

# 物件名
subtitle = unit.find('dd',{'class':'dottable-vm'}).string

# 価格
price = unit.find('span',{'class':'dottable-value'}).string

# 住所
location = unit.find_all('dd')[2].string

# 最寄駅
station = unit.find_all('dd')[3].string

# 専有面積
area = unit.find_all('dd')[4].contents[0].split('m')[0]

# 間取り
floor_plan = unit.find_all('dd')[5].string

# バルコニー
balcony = unit.find_all('dd')[6].contents[0].split('m')[0]

# 築年月
yrs = unit.find_all('dd')[7].string

# link
link = unit.find('h2',{'class':'property_unit-title'}).a.get('href')

# 管理費
kanrihi = summary_child[0].find_all('td')[5].string.strip('\r\n\t')

# 修繕積立費
shuzenhi = summary_child[0].find_all('td')[6].string.strip('\r\n\t')

# 物件階
stair = summary_child[0].find_all('td')[14].string.strip('\r\n\t')

# 方角
direction = summary_child[0].find_all('td')[15].string.strip('\r\n\t')

# リフォーム
reform = summary_child[0].find_all('td')[16].contents

# 総戸数
total_units = summary_child[1].find_all('td')[2].string.strip('\r\n\t')

# 構造・階建て
structure = summary_child[1].find_all('td')[3].string.strip('\r\n\t')

# 権利形態
right_form = summary_child[1].find_all('td')[5].string.strip('\r\n\t')

# 用途地域
usage_district = summary_child[1].find_all('td')[6].string.strip('\r\n\t')

# 駐車場
parking = summary_child[1].find_all('td')[7].string.strip('\r\n\t')

1件ずつデータを取得して、都度リストに加えていきます。

data.append([info, subtitle, price, location, station, area, floor_plan, balcony, yrs, link, kanrihi, shuzenhi, stair, direction, reform, total_units, structure, right_form, usage_district, parking])

最終的に、dataリストをデータフレームに変換して、タイトルを付けて、csvファイルとして保存します。エラーリストも同じく保存しておきましょう。

# data listを DataFrameに変換
df = pd.DataFrame(data, columns=['情報','物件名','価格','住所','最寄駅','専有面積','間取り','バルコニー','築年月','リンク','管理費', '修繕積立費','物件階', '方角','リフォーム','総戸数', '構造・階建て','権利形態','用途地域','駐車場'])

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

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

どうでしょうか。無事データが取り込めましたでしょうか。 取り込んだデータを次回以降、加工・分析していきたいと思います。

本日はこれにて。

【珈琲たいむ】Pythonでコロプレス図を描く

不動産関連の分析の最中ですが、【珈琲たいむ】にしましょう。今回は「コロプレス図」なるものを描いてみます。 英語では"choropleth map"ですが、日本語では「階級区分図」というようです。余り聴いたことがありませんが、Wikipediaでは以下のように記載があります。

階級区分図 - Wikipedia

簡単に言うと、「統計地図の1つで,区域ごとに統計量を色分けした地図」のことをいうようです。米国大統領選挙の際によく利用されるような地図です。

f:id:akatak:20180825212958j:plain

この図表は、日経平均株価AI予想さんのページから引用させていただいております。
トランプ大統領誕生のリスク-株価暴落リスク要因 | 日経平均株価 AI予想

さて、これをPythonで描いてみましょう。海外ではplotlyというモジュールが良く使われているようです。ただ、米国や世界のコロプレス図には対応していますが、残念ながら日本を対象としたコロプレス図は準備されていません。そこで、ネットで検索して調べていると、Python環境で、しかもJupyter Notebook上に日本のコロプレス図を作成するには、どうやらfoliumというモジュールを使うのがよさそうです。

ohke.hateblo.jp

www.monotalk.xyz

等のブログを参考にさせて頂きました。

事前準備を行う

さて、コロプレス図を作成していきましょう。

まず、foliumほか、pandasをインポートしておきます。

import folium
import pandas as pd

そして、コロプレス図に表示する情報(例えば、賃貸物件数や平均家賃)の以下のようなデータを予め作っておき、tokyo23.csvとして保存しておきます。

f:id:akatak:20180825223439p:plain

pandasのread_csvメソッドにて情報データを取り込みます。そして、行政区分コード(例えば、千代田区の場合は「13101」)をstr型に変換しておきます。

tokyo_data = pd.read_csv('tokyo23.csv', index_col=0)
tokyo_data['行政区分コード'] = tokyo_data['行政区分コード'].astype('str')

また、先達が作成された以下のサイトから東京23区境界のGeoJSONデータtokyo23.jsonをダウンロードして保存しておきます。

github.com

GeoJSONデータを保存している場所(相対パス)を指定します。また、地図の中心点としたい場所の位置の緯度・経度データを指定します。今回は港区の場所を指定しています。

geojson = 'tokyo23.json'
tokyo23_location = [35.658593, 139.745441]   # 地図の中心点を指定。

ベースマップを作成する

コロプレス図を表示するためのベースマップは、以下の1行で作成されます。

m = folium.Map(location=tokyo23_location,tiles='cartodbpositron',zoom_start=10) 

locationで地図の中心を指定し、zoom_startで縮尺のレベルを指定します。デフォルト値は10です。デフォルトなので、記載しなくても良いですが、後から変更できるように記載しています。

tilesでベースマップの種類を指定します。コロプレス図が見やすいのはcartodbpositronだと思いますので、ここで指定します。

尚、foliumにビルトインされているベースマップのうち、API_keyがなくても利用できるのは以下の8つです(本ページの最後に表示)。

  • openstreetmap
  • mapboxbright
  • cartodbdark_matter
  • cartodbpositron
  • mapboxcontrolroom
  • stamenterrain
  • stamentoner
  • stamenwatercolor

後はWeb上で公開されているLeaflet用の URL をtilesで指定して利用可能なようです。その場合の書式は http://{s}.yourtiles.com/{z}/{x}/{y}.png

folium — Folium 0.9.1 documentation

ベースマップを表示させてみると、こんな感じ。

f:id:akatak:20180826090008p:plain

コロプレス図をベースマップに表示する

まず、表示させたいデータを確認しておきましょう。

f:id:akatak:20180826090207p:plain

先ほどのベースマップ上にコロプレス図を作成するのは簡単です。先ほど作成したベースマップmに対して、choroplethメソッドを適用するだけです。

m.choropleth(
    geo_data=geojson,       # GeoJSONデータ
    name='choropleth',
    data=tokyo_data,    # DataFrameまたはSeriesを指定
    columns=['行政区分コード', '物件数'],  # 行政区分コードと表示データ
    key_on='feature.id',    # GeoJSONのキー(行政区分コード)
    fill_color='YlGn',      # 色パレットを指定(※)
    threshold_scale=[0,4000, 8000, 12000, 16000, 20000], # 境界値を指定
    fill_opacity=0.7,       # 透明度(色塗り)
    line_opacity=0.2,     # 透明度(境界) 
    legend_name='物件数'    # 凡例表示名
)

(※)色パレットは、‘BuGn’, ‘BuPu’, ‘GnBu’, ‘OrRd’, ‘PuBu’, ‘PuBuGn’, ‘PuRd’, ‘RdPu’, ‘YlGn’, ‘YlGnBu’, ‘YlOrBr’, and ‘YlOrRd’から選びます。

さて、表示させると以下のような感じになります。 f:id:akatak:20180826092239p:plain

図をhtmlファイルに保存することも可能です。

m.save('choropleth_bukkensuu.html')

ついでに、平均家賃をコロプレス図にしてみました。

f:id:akatak:20180826092901p:plain

ベースマップの種類

f:id:akatak:20180826093031p:plain

f:id:akatak:20180826093107p:plain

f:id:akatak:20180826093139p:plain

f:id:akatak:20180826093209p:plain

f:id:akatak:20180826093253p:plain

f:id:akatak:20180826093345p:plain

f:id:akatak:20180826093418p:plain

f:id:akatak:20180826093453p:plain

f:id:akatak:20180826093530p:plain

foliumでは、他にもマーカー等を付けたりできるようです。 またの機会にしましょう。今回はこれにて失礼します。

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

さて、いよいよ分析にチャレンジします。

目的は、家賃の適正水準を探ることです。今後、マンション売却物件の分析も行いたいと思っていますので、賃貸価格と売買価格の比較を行い、割安割高の検証を行うことも念頭に置いています(出来るかどうかはやってみないと分かりませんが)。

前回、賃貸物件データの前処理を行いましたので、そのデータを用いて、pyhonにて重回帰分析を行いたいと思います。が、統計分析についても、全くのシロウトですので、書籍やブログ等を参考にしながら行います。

まずは、前回保存したデータをDataframeとして取り込みます。また、逐次グラフ化して確認しながら作業を行っていきますので、Jupyter Notebookを利用します。

f:id:akatak:20180825092959p:plain

なお、70㎡の面積に引き直した家賃を「標準家賃」として、家賃単価(円/㎡)×70で計算した結果を列として追加しておきます。

f:id:akatak:20180825093602p:plain

賃貸データの概要の把握

データ系列の情報を見ておきましょう。

f:id:akatak:20180825093745p:plain

15万件の賃貸データがありますね。まず、家賃の分布と家賃単価の分布を見てみましょう。それぞれの要約統計量describeヒストグラムhist()を表示します。

f:id:akatak:20180825100652p:plain

f:id:akatak:20180825102024p:plain

家賃の平均は10万円ほど、家賃単価の平均は3350円ですね。家賃は物件の広さによって異なりますので、今後の分析は、不動産業者が物件比較の際に良く利用する家賃単価を中心に行っていきたいと思います。

市区別の物件数

f:id:akatak:20180825101854p:plain

世田谷区の物件数が突出して多いですね。各区の人口と比較してみます。

各市区町村の平成30年3月現在の人口が以下に掲載されていますので、Excelにコピペして、csvファイル化します(市町村のデータも取り込まれます)。  

都内区市町村マップ|東京都

そのcsvファイルをdf_popとして取り込んだ上で、groupedに統合concat()します。その際に、統合方法としてjoin='inner'を指定します。その後、必要のない面積の列等を除外して、グラフ化します。

f:id:akatak:20180825103737p:plain

人口が多いと物件も多いという傾向が出てますね。大田区だけ、人口の割には物件数が少ないですが。スクレイピングの際にデータ取得が完全でなかったのが影響しているのかもしれません。

もともと目的が、家賃の適正水準を探ることなので、データ数が十分にあれば良いとします。

市区別の家賃・家賃単価

市区別の家賃・家賃単価の平均を見ると、当たり前かと思いますが、明らかに傾向がありますよね。

f:id:akatak:20180825105628p:plain

f:id:akatak:20180825105638p:plain

ついでに家賃単価の箱ひげ図を描いてみましょう。グラフを平均によって並べ替えするのには、seabornライブラリが便利です。傾向がよく分かって面白いですね。世田谷区が案外低いですね。

f:id:akatak:20180825121833p:plain

築年数別の家賃単価

築年数別に家賃単価を散布図にしてみます。

f:id:akatak:20180825105932p:plain

上下に異常値がありそうですので、中身を見てみましょう。

f:id:akatak:20180825134725p:plain f:id:akatak:20180825110406p:plain

面積が2㎡のものがあったり、逆に660㎡のものがあったり、どうも入力ミスと思われるデータが散見されます。そうでないものもありますが、単価が14000円以上、500円以下のものはこの際、除外して分析していくことにします。

f:id:akatak:20180825110822p:plain

そのうえで、築年数毎の平均値をグラフ化すると、以下のようになります。

f:id:akatak:20180825112355p:plain

60年以降の平均値が大きくバラついていますので、築年数毎の物件数を見てみましょう。2000件以下だけ表示させます。

f:id:akatak:20180825112419p:plain

データ数の少ない築年数56年以降を分析から除外してしまいましょう。 さらに、市区別・築年数別の家賃単価(平均)をデータフレーム化したものを作成し、グラフ化します。 f:id:akatak:20180825113439p:plain f:id:akatak:20180825113612p:plain

区別に家賃単価の水準感が異なり、更に築年数が増加すれば、単価が下がっていく傾向がよく分かりますね。

最寄駅からの時間による家賃単価

最寄駅からの時間によって単価がどう変化するのかを確認しておきます。 最寄駅から物件までの移動手段として「徒歩」の場合にてグラフ化します。

f:id:akatak:20180825114742p:plain

これも異常値がありそうです。データ数を見てみましょう。

f:id:akatak:20180825114908p:plain

明らかに、750分は間違いですね。75分もどうでしょうか。切りの良いところで40分超のデータは除外しましょう。さらに、これも市区別にグラフ化しておきます。

f:id:akatak:20180825120804p:plain

これも異常値ですので、確認して必要があれば除外しておきましょう。

重回帰分析を行ってみる

さて、いよいよ重回帰分析です。有名なライブラリscikit-learnをimportして、分析を行います。異常値を除外したdf_modから、重回帰分析の説明変数、目的変数とするもののみ取り出して、データフレーム化しておきます。面積と部屋数、物件階と建物の高さは、各々やや相関が高いので、ここでは面積、物件階を残すことにします。この当たりは専門家でないので、適当です。すみません。

f:id:akatak:20180825125144p:plain

# scikit-learnライブラリをimportします
import sklearn
from sklearn.linear_model import LinearRegression as LR

# 線形回帰モデルのインスタンス化
model = LR()

# 説明変数Xに家賃単価(目的変数)と除外するとした「部屋数」「階数」以外を挿入
X = df_for_analysis_base.drop(['家賃単価', '部屋数','階数'], axis=1)

# 数値以外の説明変数(「市区」「手段」)をダミー化
X = pd.get_dummies(X)

# 目的変数に家賃単価を代入する
y = df_for_analysis_base['家賃単価']

説明変数Xはこんな感じ。 f:id:akatak:20180825130844p:plain

予測モデルの作成は、以下の1行を実行するだけです。

# 予測モデルの作成
model.fit(X, y)

結果として、回帰係数model.coef_、切片model.intercept_、決定係数score()をそれぞれ表示させます。

f:id:akatak:20180825131710p:plain

決定係数は61%となりました。まぁこんなもんなんでしょうか。 説明変数と係数の関係をもう少し分かりやすく表示させてみましょう。

coeff_df = pd.DataFrame(X.columns)
coeff_df.columns = ['説明変数']
coeff_df['係数推定'] = model.coef_

coeff_dfを表示させると以下の通りとなりました。

f:id:akatak:20180825133227p:plain

従って、(場所)世田谷区(築年数)20年(駅からの距離)徒歩10分(面積)70㎡ (物件階)2階の賃貸物件の家賃単価は、4164 .43 -3.84 +276.05 - 15.0470 - 26.3320 - 22.2110 + 50.852 = 2736円/㎡となります。

賃貸物件の面積が70㎡ですので、約191,500 円が家賃推定額となります。

(場所)江東区(築年数)15年(駅からの距離)徒歩5分(面積)80㎡ (物件階)10階の賃貸物件ですと、4164.43 -161.86 + 276.05 -15.0480 - 26.3315 - 22.41 * 5 + 50.85 * 10 = 3076円/㎡が家賃単価となります。

賃貸物件の面積が80㎡ですので、約246,000円が家賃推定額となります。

いかがでしょうか。なんとなくイメージあってますか。

検証してみる

データを学習用データ(Train)と検証用データ(Test)に分けて、残差プロットなるものを作成し、検証してみることも行うようです。

f:id:akatak:20180825133712p:plain

y=0の回りに、残差がランダムにばらけているように見えれば、モデルは良かったと言えるようですが、上のグラフの場合はどうなんでしょう。ランダムにはばらけていないように見えるので、モデルを修正していく必要があるかもしれません。

今回は、方角のデータ等、家賃単価に影響がありそうなデータを取り込んでいません。また、統計や機械学習は、まだ、勉強を始めたばかりですので、今後、機会があれば、モデルの修正にチャレンジしていきたいと思います。

長くなりましたので、本日はこれにて失礼します。次回以降は、売却物件について分析していきたいと思います。