はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

「街区レベル位置参照情報」を使って市区町村を囲むような長方形の4角の緯度経度を算出する体でのPandasとLeafletのサンプルプログラム

はじめに

行政が公開している「街区レベル位置参照情報」というデータを使って、↓こんな感じで、ある市区町村を囲むような4角形の矩形をあぶりだせないか試してみました。

... という体裁をとった、PandasとLeafletのサンプルプログラムを動かしてみた・やってみた記事です。

f:id:azotar:20190326211747p:plain

nlftp.mlit.go.jp

入力データのダウンロード

上記のページから、位置参照情報ダウンロードサービスのリンクを辿って次の ページに行ってください。

位置参照情報ダウンロード

ここから、何画面か、データをダウンロードしたい地域と欲しいデータが「街区レベル」か「大字・町丁目レベル」かを選択するかをいく通りかの方法で選べる画面遷移です。

画面遷移がちょっと説明が難しいのと最後に同意を求められるので、直リンクはしません。

ただ、どのルートで選んでも最終的には、ある地域一帯のデータがダウンロードできる画面に行くようなので、そこで対象としている地域の「街区レベル」のデータをダウンロードしてください。

※ファイルはSJISです。変換コマンドなどでutf8に変換してください。ここでは、utf8に変換したものとして説明を続けます。

ダウンロードしたデータを集計

「街区レベル位置参照情報」は、その地域の全ての街区レベルの住所の代表地点の緯度経度を保持しています(いるようです)。詳しくは提供元の仕様書やダウンロード時に同梱のhtmlファイルに記述がありますのでそちらを参照ください。

ここで、このデータから、今回あぶり出したいデータ、どのようにあぶり出しするかのアイディアは次の図のようなイメージです。

f:id:azotar:20190326212713p:plain

そんなに難しい話ではないですね。

もし本気でこのデータを使うとすれば、元のデータが都合のよいものになっているかどうかが重要ですが、ここではPandasで遊んでみて、Leafletで遊んでみる目的なので、いざPandasでこのようなデータを編集してみます。

データ編集のプログラム例

import pandas as pd
import numpy as np
import sys
import csv
import copy


# 13_2017.utf8.csv はインプットデータ。ここでは東京のデータ。30MBほどなのでここでは一括で処理しましたが、少しずつためしたい場合は、read_csvのskiprowsオプションなどを利用するのが良い。
#  http: // nlftp.mlit.go.jp/isj/index.html
df = pd.read_csv('13_2017.utf8.csv',
                 low_memory=False) 


# 2つの座標の中心の座標を求める。ここではDEG形式なので多分この計算式でも良い。
def center(series):
    ss = list(map(float, list(series.sort_values())))
    return ss[0] + (ss[-1] - ss[0])/2


# 注:この指定方法はPandasの新しいバージョンではdeprecatedだが、ここでは実施内容がわかりやすいためこの指定方法
AGG_COND = {
    '緯度': {'lat_n': 'max', 'lat_c': center, 'lat_s': 'min'},
    '経度': {'lon_w': 'min',  'lon_c': center, 'lon_e': 'max'}
}

ADDRESS = ['都道府県名', '市区町村名']  # , '大字・丁目名']
GEO_LOC = ['緯度', '経度']

A_AND_G = copy.deepcopy(ADDRESS)
A_AND_G.extend(GEO_LOC)

# 今回は、市区町村レベルの囲みの長方形をあぶり出すため、ある市区町村の配下のデータでgroupbyする。
# 緯度経度の値はDEG形式だが、求めるのは最大・最小のため、ここでは数値とみなして、集計して良い。
locations = df[A_AND_G].groupby(ADDRESS).agg(AGG_COND)


def gen_latlngs(series):
    """
    DataFrameの緯度経度の集計値を「JavaScriptの配列定義の文字列」に当てはめる。
    ※この時点では意図がわかりにくいが、後のLeafletのサンプル例をアドホックひとまず動かすための仕掛け
    """
    lat_n = str(series[('緯度', 'lat_n')])
    lon_w = str(series[('経度', 'lon_w')])
    lon_e = str(series[('経度', 'lon_e')])
    lat_s = str(series[('緯度', 'lat_s')])
    lat_c = str(series[('緯度', 'lat_c')])
    lon_c = str(series[('経度', 'lon_c')])
    area = series['市区町村名'][0]
    ls = f"latlngs.push([[{lat_n},{lon_w}],[{lat_n},{lon_e}],[{lat_s},{lon_e}],[{lat_s},{lon_w}],[{lat_n},{lon_w}]]); "
    cn = f"centers.push([{lat_c},{lon_c}]);"
    ar = f"popups.push('{area}');"
    return ls + cn + ar


# 編集結果を出力
locations.reset_index().apply(gen_latlngs, axis=1).to_frame().to_csv(
    sys.stdout, index=False, quoting=csv.QUOTE_NONE, escapechar=' ')

"""
標準出力に下記のイメージのテキストがつらつら出力
 latlngs.push([[35.745558, 139.677755] , [35.745558 , 139.752208] , [35.712728000000006 , 139.752208] , [35.712728000000006 , 139.677755] , [35.745558 , 139.677755]])
 centers.push([35.72914300000001, 139.7149815])
 popups.push('○○区')
"""

Leafletで地図表示の例

前述のプログラムの出力結果をよしなに取り込んだ次のHTMLファイルを保存して、Chromeなどのモダンブラウザ(?)で開いて見てください。 (※ 実際はちょっとだけ書き換えが必要になるので、下記、HTMLファイル中の解説は確認してくださいね。)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Leaflet/OpenStreetMap他を使った市区町村枠表示<</title>
    <!--
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css">
    <script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"></script>
    -->
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v1.4.0/leaflet.css" />
    <script src="http://cdn.leafletjs.com/leaflet/v1.4.0/leaflet.js"></script>
</head>
<body>
    <div id="mapid" style="width: 100%; height: 1200px;"></div>
</body>
<script>
    let mymap = null

    let latlngs = []
    let centers = []
    let popups = []
</script>

<!-- script src="./center_and_area_data.js"></script -->

<script>
// 前のPandasのプログラムの標準出力のテキストが、JavaScriptのスニペットなので、それをファイルにして↑のようにscript srcで読み込むか、下記↓のように貼り付け。
// ここではモダンブラウザであればHTTPサーバに配置しなくてもひとまず動かしてみる目的のものであり、もちろん、もっとちゃんとしたやり方があることには注意のこと。

 latlngs.push([[35.745558, 139.677755] , [35.745558 , 139.752208] , [35.712728000000006 , 139.752208] , [35.712728000000006 , 139.677755] , [35.745558 , 139.677755]])
 centers.push([35.72914300000001, 139.7149815])
 popups.push('○○区')

</script>

<script>
    mapInitCenter = centers[0]

    window.onload = function () {
        mymap = L.map('mapid', {
            center: centers[0],
            zoom: 13
        })
        let osm = L.tileLayer(
            'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
            {
                attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
            }
        )

        let blank2 = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png',
            { id: 'blankmap', attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>国土地理院</a>" })

        let seamlessphoto11 = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
            { id: 'blankmap', attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>国土地理院</a>" })

        let relief12 = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/relief/{z}/{x}/{y}.png',
            { id: 'blankmap', attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>国土地理院</a>" })

        //osm.addTo(mymap)
        blank2.addTo(mymap)
        //seamlessphoto11.addTo(mymap)
        //relief12.addTo(mymap)

        let popupObj = []
        for (let i = 0; i < centers.length; i++) {
            // 直線の色をランダムに生成する(ことにした)
            let lineStyle = {
                "color": `rgb(${~~(256 * Math.random())}, ${~~(256 * Math.random())}, ${~~(256 * Math.random())})`,
                "weight": 5
            }
            // 直線の地図のインスタンスに描く。ここでは、四角形の4点の座標について、左上、右上、右下、左下の配列を引き渡すので、この座標を結ぶような直線が引かれることになり、長方形になる。
            L.polyline(latlngs[i], lineStyle).addTo(mymap)

            // ポップアップを表示。今回やりたいことからすると必須ではないが、Leafletの練習としてやってみたの例。
            popupObj.push(new L.Popup({ 'autoClose': true }))
            popupObj[i].setLatLng(centers[i]).setContent(popups[i])
            L.marker(centers[i]).addTo(mymap).bindPopup(popupObj[i]).openPopup()
        }

    }

</script>
</html>


参考

この記事の参考書籍↓です。 また、この号の「便利帳」という章には地理関係のオープンデータのリンク集とライセンス条件等をまとめた表が載っているので、ここで紹介されているリンクを探って行くと、この記事で扱ったような編集もそもそも不要で、加えてより精細なShapeデータなども得られるかもしれません。

Interface(インターフェース) 2019年 04 月号

免責事項

一応他所様のデータを使った内容なので免責事項的なところをひとこと。

この記事は、実質pandasとLeafletの勉強メモです。 だれかの参考になればということで、○○風のデータがあれば、それをpandasでこんな加工をして、こんな感じで取り込めば、こういう活用ができるよねという技術メモをフリーハンドで記載したものです。 当たり前の話ですが念のため記載しておくと、本当にどこかで使う場合は、データ提供元が定める利用規約を確認してください。

また、各種コード例ですが、ひとまず私自身も含めて初学者の方がきっかけをつかむための、見てのとおりの完成度です。 言うまでもなく、そのままプロダクトに取り込めるレベルではないですし、プロダクトと言わずもなんらかコピペ実行するなどの際は、ご注意ください。