こんにちは、Yuinaです🍋
今日はとても過ごしやすいお天気だったので、
すみれ系の味噌ラーメンを食べに行ってきました。
生姜がしっかり効いたスープで、とても美味しかったです!
さて、これまでの作業では、
データベースの正規化や、郵便番号から都道府県名を表示・逆引きする処理を進めてきました。
今回は、それらに加えて「データベースと地図を連携させたWebアプリ」を作っていこうと思います。
開発環境は以下の通りです:
- Python 3.12(macOS)
- MySQL(Ubuntu上)
- OS:Mac(M3チップ)
前提条件として:
pandas
とStreamlit
パッケージがインストール済み- Ubuntu環境とのSSH連携が完了している
それでは、よろしくお願いします!
ファイルをダウンロードしよう
今回は、こちらのオープンデータを使わせていただきました。
ターミナルにて、ダウンロードしていきます。
ダウンロードコマンド:
wget https://geolonia.github.io/japanese-addresses/latest.csv -O /
/(yourPath)/latest.csv
テーブルを作って、インポートしよう
先ほどダウンロードしたデータを入れるテーブルを作ります。
CREATE TABLE latest_data (
pref_code VARCHAR(5), -- 都道府県コード
pref_name VARCHAR(50), -- 都道府県名
pref_kana VARCHAR(50), -- 都道府県名カナ
pref_romaji VARCHAR(50), -- 都道府県名ローマ字
city_code VARCHAR(10), -- 市区町村コード
city_name VARCHAR(100), -- 市区町村名
city_kana VARCHAR(100), -- 市区町村名カナ
city_romaji VARCHAR(100), -- 市区町村名ローマ字
town_name VARCHAR(100), -- 大字町丁目名
town_kana VARCHAR(100), -- 大字町丁目名カナ
town_romaji VARCHAR(100), -- 大字町丁目名ローマ字
street_alias VARCHAR(100), -- 小字・通称名
lat DECIMAL(10,6), -- 緯度
lng DECIMAL(10,6) -- 経度
);
データをインポートしていきます。
LOAD DATA INFILE '/var/lib/mysql-files/latest.csv'
INTO TABLE latest_data
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS
(@pref_code, @pref_name, @pref_kana, @pref_romaji, @city_code, @city_name, @city_kana, @city_romaji, @town_name, @town_kana, @town_romaji, @street_alias, @lat, @lng)
SET
pref_code = NULLIF(@pref_code, ''),
pref_name = NULLIF(@pref_name, ''),
pref_kana = NULLIF(@pref_kana, ''),
pref_romaji = NULLIF(@pref_romaji, ''),
city_code = NULLIF(@city_code, ''),
city_name = NULLIF(@city_name, ''),
city_kana = NULLIF(@city_kana, ''),
city_romaji = NULLIF(@city_romaji, ''),
town_name = NULLIF(@town_name, ''),
town_kana = NULLIF(@town_kana, ''),
town_romaji = NULLIF(@town_romaji, ''),
street_alias = NULLIF(@street_alias, ''),
lat = NULLIF(@lat, ''),
lng = NULLIF(@lng, '');
最初、lat(緯度)・lon(経度)に空文字(“)が含まれていたので、MySQLのdecimal(10,6)
型は空文字をそのまま入れられないでした。
NULLIF(@lat, ”) により空文字が NULL に変換され、decimal型のデータも挿入できるようにしました。
こちらでインポートしたデータの件数を確認できます。
-- 欠損している緯度・経度の件数
SELECT COUNT(*) FROM latest_data WHERE lat IS NULL OR lng IS NULL;
-- 都道府県ごとの件数を確認
SELECT pref_name, COUNT(*) FROM latest_data GROUP BY pref_name ORDER BY COUNT(*) DESC;
正規化しよう
csvファイルのデータがインポートできたので、次はテーブルを都道府県、市区、町村、緯度経度別に分割して正規化していきます。
まずは、都道府県テーブルを作ります。
(なぜ、テーブル名をprefectures2にするかというと、
すでにprefecturesという名前のテーブルを作成しているためです。)
都道府県テーブル:
mysql> CREATE TABLE prefectures2(
-> pref_code VARCHAR(5) PRIMARY KEY,
-> pref_name VARCHAR(50),
-> pref_kana VARCHAR(50),
-> pref_romaji VARCHAR(50)
-> );
データをインポートします。
ysql>
mysql> INSERT INTO prefectures2
-> SELECT DISTINCT pref_code, pref_name, pref_kana, pref_romaji
-> FROM latest_data
-> WHERE pref_code IS NOT NULL;
市区テーブル:
mysql> CREATE TABLE cities2 (
-> city_code VARCHAR(10) PRIMARY KEY,
-> city_name VARCHAR(100),
-> city_kana VARCHAR(100),
-> city_romaji VARCHAR(100),
-> pref_code VARCHAR(5),
-> FOREIGN KEY (pref_code) REFERENCES prefectures2(pref_code)
-> );
データをインポートします。
mysql> INSERT INTO cities2
-> SELECT DISTINCT city_code, city_name, city_kana, city_romaji, pref_code
-> FROM latest_data
-> WHERE city_code IS NOT NULL;
町村テーブル:
CREATE TABLE areas2(
-> id INT AUTO_INCREMENT PRIMARY KEY,
-> town_name VARCHAR(100),
-> town_kana VARCHAR(100),
-> town_romaji VARCHAR(100),
-> street_alias VARCHAR(100),
-> city_code VARCHAR(10),
-> FOREIGN KEY (city_code) REFERENCES cities2(city_code)
-> );
データをインポートします。
INSERT INTO towns2(town_name, town_kana, town_romaji, street_alias, city_code)
-> SELECT DISTINCT town_name, town_kana, town_romaji, street_alias, city_code
-> FROM latest_data
-> WHERE town_name IS NOT NULL;
緯度・経度テーブル:
CREATE TABLE geo_locations (
-> id INT AUTO_INCREMENT PRIMARY KEY,
-> town_id INT,
-> lat DECIMAL(10,6),
-> lng DECIMAL(10,6),
-> FOREIGN KEY (town_id) REFERENCES towns2(id)
-> );
データをインポートします。
NSERT INTO geo_locations (town_id, lat, lng)
-> SELECT t.id, d.lat, d.lng
-> FROM latest_data d
-> JOIN towns2 t
-> ON d.town_name = t.town_name AND d.city_code = t.city_code
-> WHERE d.lat IS NOT NULL AND d.lng IS NOT NULL;
テーブルを結合しよう
テーブルを扱いやすい形に結合します。
SELECT
pref.pref_name,
city.city_name,
a.town_name,
geo.lat,
geo.lng
FROM areas2 a
JOIN geo_locations geo ON a.id = geo.town_id
JOIN cities2 city ON a.city_code = city.city_code
JOIN prefectures2 pref ON city.pref_code = pref.pref_code
WHERE geo.lat IS NOT NULL AND geo.lng IS NOT NULL
LIMIT 100;

プログラムを書いてみよう
今回使用したプログラムはこちらになります。
streamlitというパッケージを使ってブラウザ上で地図を表示させます。
import streamlit as st
import pandas as pd
import mysql.connector
# MySQLデータベースに接続
conn = mysql.connector.connect(
host='***', # IPアドレス
user='***', # ユーザー名
password='***', # パスワード
database='***', # データベース名
connection_timeout=5,
charset='utf8mb4'
)
# 市区町村名の検索を受け付ける
search_term = st.text_input("市区町村名を入力してください:")
# データベースから市区町村を検索
if search_term:
query = f"""
SELECT
pref.pref_name,
city.city_name,
a.town_name,
geo.lat,
geo.lng
FROM areas2 a
JOIN geo_locations geo ON a.id = geo.town_id
JOIN cities2 city ON a.city_code = city.city_code
JOIN prefectures2 pref ON city.pref_code = pref.pref_code
WHERE geo.lat IS NOT NULL AND geo.lng IS NOT NULL
AND (city.city_name LIKE '%{search_term}%' OR a.town_name LIKE '%{search_term}%')
LIMIT 100;
"""
df = pd.read_sql(query, conn)
# 地図に検索した結果を表示
if not df.empty:
st.map(df[['lat', 'lng']])
else:
st.write("該当する市区町村が見つかりませんでした。")
# データベース接続を閉じる
conn.close()
実行したところ、[streamlit.errors.StreamlitAPIException: Map data must contain a longitude column named: ‘LON’, ‘LONGITUDE’, ‘lon’, ‘longitude’. Existing columns: ‘lat’, ‘lng’]のようなエラーが出てきました。
Streamlitの地図表示には、longitude
というカラム名が必要ですが、df
のデータフレームにはそのカラムがlng
という名前で存在していることが原因みたいです。
df
内のlng
カラムの名前をlongitude
に変更したものがこちらです↓
import streamlit as st
import pandas as pd
import mysql.connector
# MySQLデータベースに接続
conn = mysql.connector.connect(
host='***', # IPアドレス
user='***', # ユーザー名
password='***', # パスワード
database='***', # データベース名
connection_timeout=5,
charset='utf8mb4'
)
# 市区町村名の検索を受け付ける
search_term = st.text_input("市区町村名を入力してください:")
# データベースから市区町村を検索
if search_term:
query = f"""
SELECT
pref.pref_name,
city.city_name,
a.town_name,
geo.lat,
geo.lng
FROM areas2 a
JOIN geo_locations geo ON a.id = geo.town_id
JOIN cities2 city ON a.city_code = city.city_code
JOIN prefectures2 pref ON city.pref_code = pref.pref_code
WHERE geo.lat IS NOT NULL AND geo.lng IS NOT NULL
AND (city.city_name LIKE '%{search_term}%' OR a.town_name LIKE '%{search_term}%')
LIMIT 100;
"""
df = pd.read_sql(query, conn)
# データフレームのカラム名を変更
if not df.empty:
df = df.rename(columns={'lat': 'latitude', 'lng': 'longitude'})
# 地図に検索した結果を表示
st.map(df[['latitude', 'longitude']])
else:
st.write("該当する市区町村が見つかりませんでした。")
# データベース接続を閉じる
conn.close()
修正が必要なエラーを吐かなかったので、大丈夫だと思います。
ターミナルでrunコマンドをStreamlitを実行してみます。
streamlit run city_search_app.py

完成しました✨
まとめ
今回は、市区町村名を入力すると、該当する市区が一覧で表示されて、地図上にも位置が表示されるWebアプリをStreamlitで作りました。
MySQLを使って市区町村のデータを扱うことで、ちょっとしたデータベースの扱いにも慣れることができました。
今後はデータベースとpandasと組み合わせたデータ解析とか面白そうなので、遊んでみようと思います。
読んでくださってありがとうございました。
コメント