地理分布可視化を支える技術  —  Looker Studioによる身近な情報発信

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/531235eb-f8de-402f-a4a9-48fc19f2bcc6.png)

[チームみらい](https://team-mir.ai/)ではサポーターが日本全国にどのように分布しているかを[Looker Studioを用いたダッシュボード](https://lookerstudio.google.com/u/0/reporting/e4efc74f-051c-4815-87f1-e4b5e93a3a8c/page/p_lm7tz3mktd)で公開しています。これがどのような仕組みで動いているのかを解説します。この記事はチームみらいの技術的な取り組みを共有するものであり、投票を呼びかけるものではありません。

## 全国の分布を見たかった

このシステムはもともと神奈川県選挙区総支部長の河合みちおさんと、[チームみらいアクションボード](https://action.team-mir.ai/)をバリバリ開発しているKenta Muraiさんとが作っていたものらしいです。僕はLooker Studioという言葉を聞いたこともなかったのですが、ものができてから眺めていじって「これは面白いな！」となりました。

その中で一つ不満点があったのはこれ:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/0f422bef-7b77-429f-abac-2a4dc3455d69.png)

面白い可視化なのだけど妻が北海道出身の僕としては北海道なども含めて全体を見たいなと思いました。じゃあズームアウトすればいいじゃないかと思いましたか？やってみましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/b24a50bf-d23a-449a-83fa-ab05136a3da4.png)

枠線が太すぎる！！！

これを何とかしたい気持ちがLooker Studioを色々試してみる原動力になりました。

## 当初の仕組み

Muraiさんから色々話を聞いていて興味深かったのはデータの集め方と持ち方でした。詳細に住所の情報をとると個人情報としての扱いが必要になって運営の負担が増えるし、そもそも何かを郵送するわけではないので詳細な住所が必要ではない。しかし東京なのか福岡なのかなどの「大体どこにいるのか」の情報は欲しい。このニーズを満たす方法として「郵便番号だけとる」という解決が行われていました。

初期の構成は下記のようになっていました
- Google Formでボランティア登録
- レスポンスデータがGoogle Spreadsheetに入る
- Google App Scriptでzipcloudの[郵便番号検索API](https://zipcloud.ibsnet.co.jp/doc/api)を叩いて郵便番号を粗い住所に変える
- Looker StudioがそのSpreadsheetを読んで可視化する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/cf60f0e3-0f57-4b1c-bcfa-4b7462169bdb.png)

## 悪戦苦闘

SpreadsheetをLooker Studioで読んだ時に、デフォルトでは住所は文字列型のままなので、それを地域の型に変換してやらないとマップ集計ができないことにまずつまずきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/d5a976ca-b404-40a2-94e3-b2ae21d6fd43.png)

また、領域塗り分けの線は上記の図ですでに最小の1pxであり、これ以上細くはできないこともわかりました。枠線を消すことはできます。でもそれだと色が薄い地域は見づらくなってしまいます。

色々試行錯誤して、最終的に「領域の境界や人数による色の濃淡変化は今回やりたい可視化には必要ではない」と判断し、冒頭のバブルチャートを採用しました。

再掲:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/531235eb-f8de-402f-a4a9-48fc19f2bcc6.png)

余談ですが、最初はチームみらいの色のミントグリーンにしていて、色覚特性によっては見づらいという報告があり、デフォルトの色に戻しました。色は素人考えでカスタマイズすべきではないですね。

## 仕様の変更！！

[チームみらいアクションボード](https://action.team-mir.ai/)の話は実は伏線でした！！

チームみらいアクションボードは、サポーターの人たちがいろいろな活動をRPGのレベル上げみたいゲーム感覚で楽しめるシステムです。現時点で1200+人が登録しています。私は「日本初の規模で政治をゲーミフィケーションする社会実験が行われている状態」と思っています。この最初の社会実験に参加できる機会はあと1ヶ月くらいです: [登録はこちら](https://action.team-mir.ai/sign-up?ref=JypcVNEk)。

![IMG_5786.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/b1496193-a4b0-4218-8e7a-088a2346c38c.jpeg)

こうなると次に何が起こるでしょうか？勘付いたエンジニアも多いことでしょう。「Google Formで登録した人とチームみらいアクションボードで登録した人のどちらをサポーターとして集計すべきなのでしょうか？」という問いが発生します。

議論の結果、「どちらの入口を好むかは人によって異なるので両方維持したい」ということになり、集計値としては「重複を排除して合算したい」ということになりました。つまりこういうこと:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/f57dab88-14f8-4a42-a4ee-b827ebe5466e.png)

うん？これ、結構めんどくさい要求仕様ですよね？(苦笑)

Supabaseに入ってるデータを取り出して重複除去するところまで全部Google App Scriptで扱うのは少し荷が重そう。そう考えてデータ分析パイプラインをまるっとGitHub Actionsに移行しました。

新しい構成:
- (A): Supabaseのauth.usersと郵便番号を含むテーブルをJOINして最小限の出力を返す[Postgres関数](https://supabase.com/docs/reference/python/rpc)を作り、Remote Procedure Callsで呼ぶ
- (B): [Google Cloudのサービスアカウント](https://cloud.google.com/iam/docs/service-account-overview?hl=ja)で[Google Spreadsheet API](https://developers.google.com/workspace/sheets/api/reference/rest?hl=ja)を呼ぶ(対象Spreadsheetが公開のものではないため、自動化用のアカウントを作り、閲覧権限者に追加する必要がある)
- (C): マージ処理「共通のメールアドレスのものが両方にある場合、created_atが新しいものだけを残す」をする
- (D): マージ結果を新しいSpreadsheetに書く

図解するとこうです:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/dea27c31-d857-41b3-870d-92b74521a8ef.png)

やった！合算したグラフが出せました！めでたしめでたし！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/8ad22eee-0287-4a93-8fb9-665594716371.png)

## できたと思ってからが長い

めでたしめでたし！で終わったら世の中幸せなんですけど、そうはいかないのが世の真実。切り替えの数日前には作業は完了したが、いざ本番切り替えしてアナウンスすると途端に問題に気づくもんです……。

## 大阪市に穴が空いている問題

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/45a32076-247d-4837-b853-c1d643f3e76b.png)

この可視化の何がおかしいかわかりますか？中央のデータがない穴の領域には大阪市があります。大阪市のサポーター数が0人とは考えにくい。分析パイプラインをGoogle App ScriptsからGitHub Actions上のPythonへと切り替えたタイミングで何か挙動が変わってしまっているようです。

本当は移行前に「プロセスの変更によって従来のデータが変化しないか」を、データを通し直して検証してみるべきでしたね。迂闊でした。まあAIエージェントのDevinに「大阪市周りで何が変わったのか調べて」と言ったらあっさり特定してくれたので結果オーライとしましょう。

Devinの報告
>大阪市関連「地方行政区画（第2レベル）」データ比較結果
両スプレッドシートのデータを比較した結果、明確な相違が確認されました。
>主要な相違点:
>- 現在のスプレッドシート: 大阪市・堺市の区レベル詳細データ（例: 大阪府大阪市中央区、大阪府堺市北区）
>- 従来のスプレッドシート: 市レベルのデータ（例: 大阪府大阪市、大阪府堺市）
>
>これは、現在のコードが郵便番号からより詳細な行政区画情報を取得しているためと考えられます。現在のデータはより精密で正確です。

現在のデータはより精密で正確です……。この報告をChatGPT o3に渡すと正解に辿り着きます。

ChatGPTの回答
>Looker Studio には「地方行政区画（第 2 レベル）」＝市区町村レベルのジオタイプが用意されていますが、政令指定都市の“区”まではポリゴンが用意されていません（例外は東京 23 区のみ）。

そう、Looker Studioの市区町村可視化は「東京都23区では区まで含める必要があるが、他の市では区を含めてはいけない」という仕様なんですね。ここを修正して無事大阪市も表示されるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/a61123a4-e289-4940-9638-0045ba370d37.png)
やっぱり大阪市が一番色濃いですね。

## 郵便番号APIが止まる問題

アクションボード由来で数百人分の新しいデータが増えました。これを処理するとエラーがたくさん... たぶん[郵便番号検索API](https://zipcloud.ibsnet.co.jp/doc/api)が一定時間ウィンドウ内で100件までに制限されてそうだなと思いました。数百件を一度に処理すると、高頻度アクセスすぎて止められてしまうのでしょう。

この件は、そもそもGitHub ActionsでやるならAPIに頼らなくてもいいなと考えなおしました。郵政公社から元データCSVをダウンロードして自前で郵便番号を住所に変換する実装にしました。

キャッシュヒット率を事前に計算したところ97.9%でした。約2%の失敗例を観察すると0000000（全てゼロ）のような明らかに郵便番号ではないものもありましたが、一見郵便番号に見えて実は該当レコードがないものもありました。誤入力かもしれないですね。

API呼び出しをなくすことで、処理時間は 210秒 → 17.7秒 と 92%削減になりました。


## 高頻度実行したらデータが壊れている報告が……

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/bbde9e5a-b0fd-4f22-bdc9-03423394dddd.png)

データが壊れていると言う報告があり、確認すると壊れていない、と言う問題。でもスクリーンショットという動かぬ証拠があるので、実際に壊れているタイミングがあったことがわかります。

すでにあるデータを削除してからSpreadsheetに書き込むところに10秒程度かかっていて、運悪くそのタイミングでLooker Studioが読みにくるとデータがないので壊れた表示になる、という現象でした。Looker Studioはデフォルトで15分間隔でデータを読みます。当初は元データの側を1日1回更新にしていましたが、せっかく盛り上がってきたからとGitHub Actionsの側も15分間隔にしたので、運悪く壊れる確率が96倍に？

Spreadsheetに書かれているものを全部消してから、マージした新しいデータを全部書く仕組みだったのをやめました。かわりに、Spreadsheetに現在書かれているデータを読んで、手元のデータとの差分だけを追記するようにしました。これで更新にかかる時間を大幅削減！データが消えている時間もないので安心！

## 翌日「データが増えている」という報告が……

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/c6cfbf35-6010-4c90-90da-38e12b58e20b.png)

ぎゃー、エンジニアからは「処理がバグっているのでは」という指摘が飛んでくる反面、コミュニティマネージャーは数値が正しいと思い込んで大喜び！待って！SNSアナウンスしないで！状況確認します！

Devinの報告
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/7645a617-8f99-4a58-8f25-41751e9074bf.png)

なるほど、マイクロ秒の桁数かー。o3による解説:

>PostgreSQL では内部的に「秒＋ 0–6 桁の小数部」で時刻を保持しますが、文字列にする段階で末尾の 0 を自動的に削るため、たとえば
2025-06-21 15:56:20.123450+09 → 2025-06-21 15:56:20.12345+09
のように「5 桁マイクロ秒」が現れます。

つまり10%の確率で末尾が0になり、Supabaseが内部で使っているPostgreSQLはその時に0を削って5桁で返してくる。Python側がそれをケアしないでいるとパースエラーになり、Devinくんが親切に「エラーが起きても無視して続行」のコードにしていたのでそのまま実行完了して、1割重複したデータが書かれてしまう、という現象でした。

この件、増分更新みたいな複雑なことをしないでも「時系列にソートして、シートクリアなしで常に上書き」でよいなと一晩寝て考え直しました。その後色々な属性を付与する際にも全更新の方が都合が良かったですね。もっとデータが多くなるとまた考えないといけないですが、当面は仕様変化に対する適応速度を重視した設計がよさそうです。

## うれしい声とさらに開発

このようにすったもんだ開発していると、うれしい声が聞こえてきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/455c4a32-3a84-4a6e-b62c-46496e282ff5.png)

自分がボスティングをやっていた地域でサポーター登録が2倍になったという喜びの声です。Looker Studioのリアルタイム性の高いフィードバックで自分の行動が世界に影響を与えていることを観測できたわけですね。やっててよかったダッシュボード。

というわけで自分のポスティングの成果をもっと確認しやすくするために都道府県より細かい単位で人数グラフを見られるようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/c8f9d50f-0951-4ed3-8dd6-7559171a9d61.png)

地理情報の可視化は、自分の住んでいる地域や活動した地域に絞り込んだビューを提供することで人々に「身近な情報」を提供できるのだなという気づきがありました。Looker Studioを使うと簡単に絞り込みビューをつくれていいですね。(元データがちゃんと整っていれば)

## まとめ

出来事がたくさんありすぎて後半はかなり駆け足になりましたが、[チームみらい](https://team-mir.ai/)による[Looker Studioを用いたダッシュボード](https://lookerstudio.google.com/u/0/reporting/e4efc74f-051c-4815-87f1-e4b5e93a3a8c/page/p_lm7tz3mktd)での地理情報可視化の裏側の仕組みを公開しました。

これらのコードは[都知事選の時と同じように落ち着いてからOSS公開](https://note.com/takahiroanno/n/nf0bc57572dd7)しようかなと思っています。実はパイプライン切り替えから実は1週間しか経っておらずコードが高速に変化していっているのが理由の一つ。扱うデータの性質的に今の段階では複数人でオープンソース開発する感じではないよなと言うのがもう一つの理由です。

一方でアクションボードの方はオープンソース開発が日々ガンガン行われています。[登録はこちら](https://action.team-mir.ai/sign-up?ref=JypcVNEk)、[リポジトリはこちら](https://github.com/team-mirai-volunteer/action-board)。日々平均20~30件のコミットがあるhotなプロジェクトです。興味があれば眺めてみてください。このグラフも[チームみらいのLooker Studioダッシュボード](https://lookerstudio.google.com/u/0/reporting/e4efc74f-051c-4815-87f1-e4b5e93a3a8c/page/p_x61wkrrltd/edit)で見ることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38209/ff213b7c-ec09-47f2-8c07-ae2990ba91b5.png)