なんちゃってエンジニアがAWSでそれっぽいことをしてみた part3


## 今回やりたいこと
　　Webブラウザ上で動くpythonプログラムを作りたい
## 何を作るのか


　　[前回part2はこちら](https://qiita.com/Odin/items/0fa16fdd4f08934f26a6)
　　ようやく自前のWebサーバが出来たところで、Webブラウザ上で動く何かを
　　作りたいな～ということと何か作るなら、興味のあるものを作りたい！
　　と考えた私。

　　と、最近suumoをみていてUIを自分好みにしたいなと思ったことを思い出し
　　suumoのデータを使って坪単価で色を変えて地図上プロット表示することで
　　市況感が可視化されるような独自ページを作ってみることにしました。
　　（個人的趣味全開）


　　そんなわけで今回は以下のようなことをします。
　　（１）suumoからデータを取得する
　　（２）（１）のデータを使って、何らか表示するプログラムを作る
　　（３）htmlを用意して、（２）の実行結果を表示させる
　　
　　もう少し具体的なところとしては
　　（１）はBeautiful Soup を使ったスクレイピングでデータを取得します。
　　　　　先人の皆様が実行されているものをQiitaみながら利用します。
　　（２）はpythonを使って、坪単価で色を変えて
　　　　　地図上プロット表示するプログラムを書きました。
　　（３）物件価格と築年数でフィルタを画面からかけられるようにして
　　　　　自分の興味のある範囲の物件のみ表示させるようにします。

　　そんで
　　いったん、自分のローカル環境で（１）～（３）を試し
　　（４）サーバ側でpythonを稼働させるための環境構築としてflaskをインストール
　　（５）ローカルで作成した資源をサーバ側にあげて、資源を微修正、動作確認をする。
　　で完成です。

　　ではスタート。
## レシピ

###### 　　（１）データ作成

　　　物件データが必要になるため、集めます。
　　　データスクレイピングにはこちらの記事を参考にさせていただきました。

https://www.gis-py.com/entry/scraping-suumo

　　　収集したデータの加工はこちらの記事を参考にさせていただきました。

https://qiita.com/hidemiya666/items/a65d7fed65ff6443947c

　　　この辺りは今回割愛しますが、記事の通りにやるとこんな感じのデータになります。
　　　今回は２３区のデータを集めました。


| 物件名                | 住所                             | 最寄り駅                              | 築年数    | 価格     | 間取り | 面積                 | バルコニーの広さ |
|-----------------------|----------------------------------|---------------------------------------|-----------|----------|--------|----------------------|-----------------|
| シャンボール新高円寺 | 東京都杉並区高円寺南２-1-4        | 東京メトロ丸ノ内線「東高円寺」徒歩5分 | 1971年3月 | 1450万円 | 1DK    | 27.64m2（8.36坪）  | 3.25m2          |

　　　今回はこれを使っていきます。

###### 　　（２）プログラム(.py)作成　
　　　ひとまずローカル環境上で、地図上に物件データを反映させることにtryしてみます。

　　　地図上に物件データを反映させるために緯度経度情報が必要になります。
　　　緯度経度情報を取得する方法として、APIが公開されていますが今回は
　　　Bing MapのAPIを使用しました。

　　　Bing MapのAPI keyはこちらから入手できます。
　　　発行トランザクションに制限はありますが、基本無料です。
　　　https://www.microsoft.com/en-us/maps/bing-maps/create-a-bing-maps-key
　　　

```python
import pandas as pd
import requests

# csvを読み込む
df = pd.read_csv('suumo_bukken_mod_23ku.csv')

# Bing Maps APIキーを設定
BING_MAPS_API_KEY = '*************'  # ここに取得したAPIキーを入力してください

# 住所から緯度経度を取得する
def get_lat_lon_bing(address):
    base_url = "http://dev.virtualearth.net/REST/v1/Locations"
    params = {
        "q": address,
        "key": BING_MAPS_API_KEY
    }
    response = requests.get(base_url, params=params)
    data = response.json()
    
    # data['resourceSets']の長さを確認
    if 'resourceSets' in data and len(data['resourceSets']) > 0 and data['resourceSets'][0]['estimatedTotal'] > 0:
        location = data['resourceSets'][0]['resources'][0]['point']['coordinates']
        return location[0], location[1]
    else:
        return None, None

# 住所から緯度経度情報を取得し、新しいDataFrameに追加
df['latitude'], df['longitude'] = zip(*df['住所'].apply(get_lat_lon_bing))

# csvとして出力
df.to_csv('suumo_bukken_mod2_23ku.csv', index=False)
```
　　　移動経度を追加したデータを使ってplotlyで描画します。　　

```python
import pandas as pd
import plotly.express as px

# suumo_bukken_mod2.csvを読み込む
df = pd.read_csv('suumo_bukken_mod2_23ku.csv')

# 地図上に棒グラフを表示
fig = px.scatter_mapbox(df, 
                        lat='latitude', 
                        lon='longitude', 
                        size='価格(万円)', 
                        color='価格(万円)', 
                        height=600, 
                        title='価格(万円)に基づく地図上の棒グラフ',
                        mapbox_style='carto-positron', 
                        size_max=15, 
                        zoom=10)

fig.show()
```
　　　　できました。plotlyはnotebook上でぐりぐり動かすことも可能です。いい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566513/2f6e5800-c58c-c61f-41c3-ba56a79261fa.png)
　　　これでもだいたいよいのですが、ちょっと視覚的にいまいちです。
　　　どの辺がどの区かちょっとわかりにくいので地図上に山手線を追加することによって
　　　位置がわかりやすいようにします。

　　　また、後ほどhtmlからフィルタ条件を入力できるようにしたいと思いますが
　　　ひとまず、コードべた書きで条件による絞り込みを行うようにします。

```python
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

# csvを読み込む
df = pd.read_csv('suumo_bukken_mod2_23ku.csv')

# 条件による絞り込み
df = df[(df['不動産単価（万／坪）'] >= 300) & (df['不動産単価（万／坪）'] <= 400) & (df['築年月数'] <= 240)]

# 地図上に棒グラフを表示
fig = px.scatter_mapbox(df, 
                        lat='latitude', 
                        lon='longitude', 
                        size='不動産単価（万／坪）', 
                        color='不動産単価（万／坪）', 
                        height=1000,
                        width=1000, 
                        title='不動産単価（万／坪）に基づくヒートマップ',
                        mapbox_style='carto-positron', 
                        size_max=15, 
                        zoom=12,)

# 山手線の各駅の緯度経度情報
yamanote_stations = {
    "東京": [35.681382, 139.76608399999998],
    "有楽町": [35.675069, 139.763328],
    "新橋": [35.665498, 139.75964],
    "浜松町": [35.655646, 139.756749],
    "田町": [35.645736, 139.74757499999998],
    "品川": [35.630152, 139.74044000000004],
    "大崎": [35.6197, 139.72855300000003],
    "五反田": [35.626446, 139.72344399999997],
    "目黒": [35.633998, 139.715828],
    "恵比寿": [35.64669, 139.710106],
    "渋谷": [35.658517, 139.70133399999997],
    "原宿": [35.670168, 139.70268699999997],
    "代々木": [35.683061, 139.702042],
    "新宿": [35.690921, 139.70025799999996],
    "新大久保": [35.701306, 139.70004399999993],
    "高田馬場": [35.712285, 139.70378200000005],
    "目白": [35.721204, 139.706587],
    "池袋": [35.728926, 139.71038],
    "大塚": [35.731401, 139.72866199999999],
    "巣鴨": [35.733492, 139.73934499999996],
    "駒込": [35.736489, 139.74687500000005],
    "田端": [35.738062, 139.76085999999998],
    "西日暮里": [35.732135, 139.76678700000002],
    "日暮里": [35.727772, 139.770987],
    "鶯谷": [35.720495, 139.77883700000007],
    "上野": [35.713768, 139.77725399999997],
    "御徒町": [35.707438, 139.774632],
    "秋葉原": [35.698683, 139.77421900000002],
    "神田": [35.69169, 139.77088300000003]
}

# 緯度と経度のリストを作成
lats = [lat for lat, lon in yamanote_stations.values()]
lons = [lon for lat, lon in yamanote_stations.values()]

# 駅の位置をプロット
fig.add_trace(go.Scattermapbox(lon=lons, lat=lats, mode='markers', marker=dict(size=10), text=list(yamanote_stations.keys())))

# 駅を結ぶ線を描画
fig.add_trace(go.Scattermapbox(lon=lons + [lons[0]], lat=lats + [lats[0]], mode='lines'))

fig.show()
```
　　これで実行プログラムが出来ました。

###### 　　（３）html作成　
　　都合上、後述します。

　<b>これでできた！！！！</b>
　とはなっていないですね。まだサーバ側に資源がないので、今作ったものを
　サーバ側にあげつつ、微修正をしていく必要があります。

###### 　　（４）環境構築
　　今度はサーバ側で動かす準備をします。
　　サーバ側でpython実行させるためにflaskをインストールします。
　　pythonを入れていなければ、このタイミングでやっておきます。

```python　
　pip install Flask
```

　　以降、どうしたらよいのか分からない・・・
　　ChatGPTにきいてみます。

　　サンプルコードを提示してくれたので、まずはサンプルコードで
　　webブラウザからpython実行結果を表示できるかやってみました。

　　・Flaskアプリの作成
　　　こんなコードをテキストに書いてapp.pyという名前にします。
　　　これをapacheの公開ディレクトリ上に適当な名前のディレクトリを作成して格納します。
　　　今回はvar/www/flask-appとしました。

　　　app.pyは後で（２）で作成したプログラムを追加していきます。

```python　
　from flask import Flask, render_template

　app = Flask(__name__)

　@app.route('/')
　def index():
    result = "これはPythonからのメッセージです！"
    return render_template('index.html', result=result)

　if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)
```

　　・htmlの作成
　　　appと同じディレクトリに今度はtemplateというフォルダを作成して
　　　htmlを置きます。htmlのサンプルは以下のようなものです。
```html
　<!DOCTYPE html>
<html>
<head>
    <title>Python結果の表示</title>
</head>
<body>
    <h1>Pythonの結果：</h1>
    <p>{{ result }}</p>
</body>
</html>
```
　・プログラムの起動
　　ここまで準備ができたら、次のコマンドでプログラムを起動します。
```python　
　python app.py
```
　　次のような表示がされればOK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566513/88da0d74-aa2d-de82-ddaf-c1e41ddffeb9.png)
　・動作確認
　　プログラムを起動した状態でブラウザ上でhttp://<WebサーバIP>:5000/を開くと
　　「これはPythonからのメッセージです！」と表示されます。
 　 　（キャプチャ紛失）

###### 　　（５）微修正～完成
　　これでベースはできたので、app.pyを（２）で作成したものと合わせていきます。
　　htmlはここで一気に作ります。

　　・app.py
　　　最後にapp.pyが描画したものをplot_htmlに出力するようにしています。
　　　これは後述するhtmlで表示させるようにしていきます。

```python
from flask import Flask, render_template, request
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
    error = None
    plot_html = ""

    # csvを読み込む
    df = pd.read_csv('suumo_bukken_mod2_23ku.csv')

    # デフォルトの条件
    min_price_million = 0
    max_price_million = float('inf')
    max_age_years = 20
    min_area = 0
    max_area = float('inf')

    if request.method == 'POST':
        min_price_million = float(request.form.get('min_price_million', min_price_million))
        max_price_million = float(request.form.get('max_price_million', max_price_million))
        max_age_years = float(request.form.get('max_age_years', max_age_years))
        min_area = float(request.form.get('min_area', min_area))
        max_area = float(request.form.get('max_area', max_area))

        if not min_price_million or not max_price_million or not max_age_years or not min_area or not max_area:
            error = "すべての項目を入力してください。"
            return render_template('realw.html', plot_html="", error=error)

    # 条件に基づいてデータをフィルタリング
    df = df[(df['価格(万円)'] >= min_price_million) & 
            (df['価格(万円)'] <= max_price_million) & 
            (df['築年月数'] <= max_age_years * 12) &  # 年数を月数に変換
            (df['面積'] >= min_area) & 
            (df['面積'] <= max_area)]

    # 地図上にマッピング
    fig = px.scatter_mapbox(df, 
                            lat='latitude', 
                            lon='longitude', 
                            size='不動産単価（万／坪）', 
                            color='不動産単価（万／坪）', 
                            height=1000,
                            width=1000, 
                            title='',
                            mapbox_style='carto-positron', 
                            size_max=15, 
                            zoom=12,
                            hover_name='物件名',  
                            hover_data=['価格', '築年数'])  
    # 山手線の各駅の緯度経度情報
    yamanote_stations = {
        "東京": [35.681382, 139.76608399999998],
        "有楽町": [35.675069, 139.763328],
        "新橋": [35.665498, 139.75964],
        "浜松町": [35.655646, 139.756749],
        "田町": [35.645736, 139.74757499999998],
        "品川": [35.630152, 139.74044000000004],
        "大崎": [35.6197, 139.72855300000003],
        "五反田": [35.626446, 139.72344399999997],
        "目黒": [35.633998, 139.715828],
        "恵比寿": [35.64669, 139.710106],
        "渋谷": [35.658517, 139.70133399999997],
        "原宿": [35.670168, 139.70268699999997],
        "代々木": [35.683061, 139.702042],
        "新宿": [35.690921, 139.70025799999996],
        "新大久保": [35.701306, 139.70004399999993],
        "高田馬場": [35.712285, 139.70378200000005],
        "目白": [35.721204, 139.706587],
        "池袋": [35.728926, 139.71038],
        "大塚": [35.731401, 139.72866199999999],
        "巣鴨": [35.733492, 139.73934499999996],
        "駒込": [35.736489, 139.74687500000005],
        "田端": [35.738062, 139.76085999999998],
        "西日暮里": [35.732135, 139.76678700000002],
        "日暮里": [35.727772, 139.770987],
        "鶯谷": [35.720495, 139.77883700000007],
        "上野": [35.713768, 139.77725399999997],
        "御徒町": [35.707438, 139.774632],
        "秋葉原": [35.698683, 139.77421900000002],
        "神田": [35.69169, 139.77088300000003]
    }

    # 緯度と経度のリストを作成
    lats = [lat for lat, lon in yamanote_stations.values()]
    lons = [lon for lat, lon in yamanote_stations.values()]

    # 駅の位置をプロット
    fig.add_trace(go.Scattermapbox(lon=lons, lat=lats, mode='markers', marker=dict(size=10), text=list(yamanote_stations.keys())))

    # 駅を結ぶ線を描画
    fig.add_trace(go.Scattermapbox(lon=lons + [lons[0]], lat=lats + [lats[0]], mode='lines'))

    plot_html = fig.to_html(full_html=False)

    return render_template('realw.html', plot_html=plot_html, error=error)

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)
```

　　・表示用html
　　　一番最後の {{ plot_html|safe }}　でapp.pyの実行結果を表示させるようにしています。
　　　それ以外は普通のhtml表記に従って書かれています。
　　　（ほとんどchatGPTに書いてもらった）

```html
<!DOCTYPE html>
<html>

<head>
    <title>マンションサーチくん</title>

<style>
    body {
        font-family: Arial, sans-serif;
        margin: 40px;
        background-color: #0052A5; /* ブルー */
        color: white; /* 文字色を白に */
    }

    h1 {
        background-color: #FF7F00; /* オレンジ */
        text-align: center;
        padding: 10px;
        border-radius: 5px;
    }

    p.description {
        text-align: center;
        font-size: 1.2em;
        margin-bottom: 20px;
    }

    form {
        background-color: white; /* フォームの背景色を白に */
        padding: 20px;
        border-radius: 5px;
        margin-bottom: 20px;
        max-width: 500px;
        margin: 20px auto;
        color: black; /* 文字色を黒に */
    }

    input[type="submit"] {
        background-color: #FF7F00; /* オレンジ */
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 5px;
        cursor: pointer;
    }

    input[type="submit"]:hover {
        background-color: #E56D00; /* オレンジの濃い色 */
    }

    .error {
        color: #FF7F00; /* オレンジ */
        font-weight: bold;
        text-align: center;
    }

    p {
        text-align: center;
    }
</style>


</head>

<body>
    <h1>マンションサーチくん</h1>
    <p class="description">こちらはあなたのお好みの条件にあったマンション群を販売価格のヒートマップで表示するサービスです。</p>

    <!-- 条件入力フォームの追加 -->
    <form action="/" method="post">
        価格(万円)（下限）: <input type="number" name="min_price_million" step="10" required><br>
        価格(万円)（上限）: <input type="number" name="max_price_million" step="10" required><br>
        築年年数（以下）    : <input type="number" name="max_age_years" required><br>
        面積（以上）: <input type="number" name="min_area" step="0.1" required><br>
        面積（以下）: <input type="number" name="max_area" step="0.1" required><br>
        <div style="text-align: center;">
            <input type="submit" value="結果を表示する">
        </div>
    </form>

    <!-- エラーメッセージの表示 -->
    {% if error %}
    <div style="display: flex; justify-content: center;">
        <p class="error">{{ error }}</p>
    </div>
    {% endif %}

    <!-- 結果の表示 -->
    <div style="display: flex; justify-content: center;">
        {{ plot_html|safe }}
    </div>

</body>

</html>
```
　　　・動作確認
　　　　再びhttp://<WebサーバIP>:5000/へ訪れるとこんな感じ
　　　　マップ部分がpython実行結果です。
　　　　テキストボックス部分の条件を入れて「結果を表示する」を押すと
　　　　フィルタがかかって、ヒートマップの結果が変わるようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566513/c34ebf6d-d047-d0b1-3bc8-9a31a4097f4f.png)

　　　　Apacheのプロキシ設定で80アクセス出来るように出来ますが、いったん割愛

## まとめ

　　これでひとまず「Webブラウザ上で動くpythonプログラムを作りたい」を達成できました。

　　ただ、ドメインすら取っていませんし、外部に公開するにはあまりに適当な作りなので
　　まだまだインフラ面でも改善が必要です。

　　APとしても検索条件は色々見直ししたいですし、ヒートマップ的なアウトプット以外にも
　　色々用途に応じた機能を考えていきたいです。

　　技術的なところでいくとAmazon Personalizeでのレコメンドなども面白そうですし
　　Amazon Bedrockも今月利用可能となったので、やってみたいところです。

　　まだまだスキルと思いが追い付いていない部分が多いですが、今後とも
　　暖かく見守っていただけると幸いです(｀・ω・´)