3年前から参加しているオープンデータ・ハッカソン
都知事杯オープンデータ・ハッカソンに出るのは、これで3年目です。
会社の同僚に「出ませんか」と声をかけて、有志で集まった人でチームを作っています。
昔からハッカソンが好きです。限られた期間。即興のチーム。その中で何かを作り上げる。あの慌ただしさが楽しくて、続けています。
直近だと、別のハッカソン(AI HACK 2026)にも出ました。エラーを投げると答えではなく問いが返ってくるAIメンター、SocraMetry というサービスのプロトタイプを作っています。そのときの話はこちらの記事に書きました。この記事の後半に出てくる「AIを役割で分ける」設計は、ここから持ち込んでいます。
ハッカソンで提案したプロダクトは、その後、メンバーが会社を作るきっかけになっていたり、サービス化して事業になっていたりと、終わった後の展開もいろいろあります。
(何も展開が起こらないことも多いです)
過去2年に作ったものは、自社サービスの機能や新しい提案に繋がりました。それも私がハッカソンに参加するモチベーションの一つです。
そして今年から、ひとつ変わったことがあります。ハッカソンのテーマが、都民の声から作られるようになりました。
今年度はハッカソンのテーマを都民の皆様から募集いたします。都民の皆様の意見を起点に課題に取組むことができます。
— 都知事杯オープンデータ・ハッカソン
集まったテーマは12の分野に分かれていて、その最初が「防災」です。そのなかに 「地域の災害リスクを題材にした防災学習」 がありました。
私たちが作ったものは、このテーマに沿っています。自分たちで思いついた課題ではなく、どこかの都民が「これをなんとかしてほしい」と書いたものでした。
そして今年は、作っている最中に、思わぬものが鳴りました。。
提出の15時間前に、緊急地震速報が鳴った
2026年8月23日、午前2時。
ハッカソンの1日目が終わり、夜通しで最後の作り込みをしていました。提出の締切はその日の17時。あと15時間。
作りかけの避難所マップをデバッグしていた、その時でした。
あの音が鳴りました。まず、音に驚きました。
少し間があって、揺れ始めます。思ったより大きい。
茨城県南部を震源とするM5.9、最大震度5弱。茨城・埼玉・千葉、そして、作り込むために泊まっていた東京でも5弱を観測とのことです。
避難するほどの被害はありませんでした。それでも、余震をしばらく警戒していました。
7月の熊本の地震が、記憶に新しかったからです。
テーマに沿って要件を詰めていく中で踏まえたのは、熊本地震や能登半島地震の検証で繰り返し指摘されてきた避難所の課題です。災害関連死、スフィア基準(避難所の国際基準)との開き、TKB(トイレ・キッチン・ベッド)。7月28日の熊本の地震でも、同じ問題が起きていました。
ここから本題 「最寄りの避難所がなんとなくわかるぐらい」
最寄りの避難所の名前を言えますか。
たぶん言えると思う。程度です。
なんとなく近所の小学校かなと想像つくぐらい。
では、その避難所は水害のときに使えますか。トイレに段差はありますか。オストメイト対応の設備はありますか。夜、照明は点きますか。
聞かれても、答えられません。
実際、災害によって利用できない避難所やペットが連れていけるかなど、避難所によって対応が異なります。
これは、調べればある程度はわかります。
ですが、どんな設備があるかなど、細かいところはわからない点もありました。
これは、一般的な課題なんじゃないかなと思い、周りに聞いてみました。
37人に聞いたら、思い込みではなかった
社員や友人、知人、計37人にアンケートを取りました。
| 聞いたこと | 結果 |
|---|---|
| 最寄りの避難所の場所が浮かぶ | 25/37(67.6%) |
| 場所以外の情報(水害対応・トイレ設備など)を知っている | 6/37(16.2%) |
| 場所が浮かぶ25人のうち、場所以外を知らない | 19/25(76.0%) |
7割が場所を知っている。中身を知っているのは6人に1人以下。
場所を知っている人に限っても、その76%が「名前しか知らない」。 私と同じような感じでした。
オープンデータの調査
ある程度一般的な課題だとわかったところで、オープンデータをわかりやすく提示するサービスを開発すれば、解決できるのではと思い、オープンデータから避難所の属性情報や関連する情報を調べました。
すべてのオープンデータを調べるのは量が多かったので、東京都内にある勤務先近辺やよく行く場所である、千代田区と港区のものを調査しました。
避難所、AED(港区)、公衆便所(千代田区)、給水スポット(東京都水道局)などを取り込んで、属性の欄を見ると370件のうち232件が、空欄でした。
段差があるかどうか。オストメイト対応かどうか。夜間に使えるかどうか。必要としていた情報がありませんでした。
もうひとつ気づいたことがあります。千代田区にAEDのデータが無く、港区に公衆トイレのデータが無い。 という点。区ごとに、公開されているものが違うこともわかりました。
※網羅的に調査したわけではないので、調査したサイト以外に情報がある可能性はあります。
なぜ情報ないのか
アンケート取った中で、もう一つ、気になった数字があります。
ベビーカーや車椅子で「この先を通れるか分からない」と遠回りした経験を聞きました。
小さい子どもや要配慮者がいるのは、37人中10人。その10人のうち6人が「ある」と答え、残りの27人はゼロでした。
4人に3人は、この困りごとに出会わないまま暮らしています。
段差の有無やトイレの仕様は、要る人には死活問題で、要らない人には存在しない情報です。無いことに気づく人が少なければ、「足りない」という声も上がりません。 声が上がらなければ、調査の項目にもならない。台帳の欄も空いたままになります。
オープンデータを用意している行政職員のリソースも限られています。優先度の結果でもあると思います。
「情報が足りない」の正体は、これだと思っています。
有事に開いても、間に合わない
ここまでは「データが足りない」話でしたが、もうひとつ、別の壁があります。
防災アプリは、普段使われません。
自治体のホームページを直近3ヶ月に見た人は37人中12人。そのうち防災目的で見た人は2人でした。全体の5.4%。20人に1人です。残りの閲覧理由は「ごみの捨て方」「行政の手続き」。
情報を公開しても、防災のために自分から見に行く人は、ほとんどいない。
では、有事に開けばいいのか。開こうとした人たちの言葉が、これです。
「そもそも繋がらない」
「主に『X』で情報収集するが、最近のタイムラインの仕様上、最新の情報が集めにくい/テレビやラジオが家にないため、インターネットの回線が弱いと情報を収集できない」
「スマホのバッテリー問題」
繋がらないときに開くものは、開けません。 通信障害を想定したとき「避難所やルートが頭に浮かばない」と答えた人は40.5%。4割です。
そして、これは想定の話ではなく、7月の熊本の地震では、実際に一部エリアで通信障害が起きています。
仮に繋がったとしても、次の壁があります。
「携帯にアラートが来たが、そもそも避難すべきか、避難するとどのようなメリットがあるかわからなかった為結局自宅(賃貸3階)に留まった。」
「どの交通手段が生きているか、しんでいるか、遅れはどれくらいか、原因は?とわからないことだらけで、困りました」
何を見ればいいのか分からない。 アプリは複数ある。どれが自分のいまの状況に答えてくれるのか、その場で判断できない。判断できないまま、留まる。
そしてこの1文が、いちばん効きました。
「大雨洪水警報が出た際、避難所を検索して、行ってみたら開いていなかった。」
検索はできた。たどり着けた。開いていなかった。
3つ並べると、順番が見えます。
- 普段は開かない
- 有事に開こうとすると、繋がらない
- 繋がっても、どれを見ればいいか分からない
有事に情報を取りに行くという設計そのものが、間に合っていない。
だから順番を逆にしました。有事に調べるのではなく、平時に覚えてしまう。
とはいえ、覚えるために座学をさせるのは無理です。防災目的で見に来る人が20人に1人なのだから、「防災を学びましょう」と正面から言っても開かれません。
楽しいから開く、という形にするしかない。
そのため、ゲームにしました。
作ったもの:平時に遊んで、有事に開く
『イマノウチ・ヨリミチ』というLINEミニアプリです。
平時は位置情報ゲームです。歩いた場所から順に、地図が見えていく。 同じ町をひととおり歩けば、その一帯がまるごと開く。避難所やAEDに着いたら3択のアンケートに答える。防災クイズが出る。カードが増える。
歩いていて浸水想定区域に入ると、その場で知らせます。「浸水想定区域に入りました」と出て、洪水か高潮か、想定の深さはどのくらいか(0.5〜3m未満、といった区分)も一緒に出ます。国土交通省のハザードマップポータルのデータです。
同時に、自分のキャラクターの足元が濡れます。
こうすることで、洪水になりやすい場所や土砂崩れの恐れがある場所などを自然と体感できると思いました。
ただし点数は動きません。危ない場所を、報酬にはしない。 知らせるだけです。あわせて「想定です。いま浸水していることを示すものではありません」と必ず添えます。
有事はゲームを捨てます。配色が変わり、まだ歩いていない場所も出て、必要な情報だけが残る。
大事なのは、この2つを別のアプリにしないことでした。有事モードは平時と同じ画面配置のまま、配色だけを変えます。見ていた場所と縮尺も残る。平時に何百回も触った指の記憶で、有事の画面を操作できるようにするためです。
覚えてもらうのは、避難所の名前ではありません。そこまでの道と、着いた先の中身です。歩いて、立ち止まって、段差を見て、3択に答える。その一連が「脳内の避難訓練」になっていればいい。
集まったデータは、そこで止めません。別々に歩いた数人の答えが一致した項目だけを「検証済み」にして、自治体標準オープンデータセット準拠のCSVで行政へ返します。 空欄を埋めて返すところまでが、このサービスの一周です。
そして入口をLINEに置きました。インストールは要りません。災害のときに「まずインストールしてください」と言わずに済む。 この一点だけで、この選択には価値がありました。
ここから技術の話
ポエムはここまでです。ここからは、制約と、その制約をどう飲み込んだかの話をします。
地理検索のないデータストアで、近くのスポットを出す
まずはプロトタイプです。動く部品を増やさないことを優先しました。
バックエンドは enebular のクラウド実行環境とデータストアで完結させています。データベースも別のホスティングも立てていません。フロントの配信もクラウド実行環境からやっていて、CORS も Cookie の SameSite も同じドメインから行うことでシンプルになっています。デプロイも1か所で済みます。
enebular は、私が所属する会社のサービスです。選んだ理由は単純で、使い慣れているからです。
この記事は会社としての発信ではなく、個人がハッカソンでやったことの記録です。宣伝のつもりはないので、うまくいったところも、詰まったところも、そのまま書いています。
その代わり、データストアはシンプルなKVSのためJOIN が無い。二次索引が無い。地理検索が無い。集計関数が無い。
「現在地から近い順に20件」が、素直には書けません。
やったことは3つです。
1. スポットはエリア単位でパーティションを切り、まとめて一度だけ受け取る。 距離計算は端末の中でやります。移動中に再取得しない。歩いている間はほとんど通信しません。バッテリーにも効きます。
2. 数えるのは、表示するときではなく、書き込むとき。 スポットの画面に「ここで何回チェックインがあったか」を出しています。ふつうなら表示するたびに数えますが、その数える機能がありません。なので記録が入った瞬間にカウンタを1つ増やしておき、画面ではその値を読むだけにしました。
3. 時系列のサブキーは必ず数値型で作る。 ここは実際に痛い目を見る前に気づけた点です。文字列にすると範囲クエリが辞書順になり、桁が上がった瞬間に「新しい順」が壊れます。しかもテーブルを作り直すしか直せません。
2つ目には、当然の疑問があります。同時に2人がチェックインしたら、どうなるのか。
取りこぼします。加算(atomic increment)が無いので、読んだ値に1足して書き戻す形だからです。タイミングが重なれば、片方が消えます。
それを許しているのは、この数字を表示にしか使っていないからです。ポイントの計算にも、再チェックインの制限判定にも使っていません。1つずれても、誰の記録も壊れません。
ただ、同じ作りでアンケートの集計も数えています。こちらは「何人が同じ答えを出したか」が検証済みの判定そのものなので、ずれると意味が変わります。
そこで確かめたのは、取りこぼしがどちらへ倒れるかでした。数え漏らせば、閾値に届くのが遅れる。逆は起きません。届いていない項目を、検証済みとして行政へ返すことはない。 安全な側へ倒れることを確認したうえで、この形にしています。
辞書順にやられた話
その辞書順で、実際に一度やられました。
スポットの取得上限を MAX_SPOTS_PER_REQUEST=200 にしていたのですが、spotId は <出典>-<ハッシュ> という形です。辞書順で先頭に来るのは aed。AEDは224件あります。
| カテゴリ | 全件 | 上限200のとき |
|---|---|---|
| aed | 224 | 200 |
| shelter | 72 | 0 |
| toilet | 36 | 0 |
| water | 38 | 0 |
避難所が1件も出ない。 防災アプリなのに。。
やったことは2つ
単純にしれっと上限を上げました。 対象エリアは370件なので、400にすれば全部入ります。最初の読み込みはちょっと時間かかりますが、ストレスに感じることは無かったです。本番仕様は、別途考えます!
打ち切られたら、黙らせないようにしました。 画面に警告を出し、ログにも [spots] truncated at 200 を残します。撮影のために上限を下げることもあるので、切れたこと自体は必ず残す。黙って切れるのが一番怖いので。
前段キャッシュに勝てないので、URLを変える
「デプロイしたのに画面が変わらない」。誰もが一度は通る道です。
実行環境の前段が、拡張子を見て Cache-Control を上書きします。CSS と JS には no-cache が効かず max-age=14400 になる。ヘッダを通せません。
打てる手は一つでした。URLを変える。
<link rel="stylesheet" href="app.css?v=__ASSET_VERSION__" />
<script src="app.js?v=__ASSET_VERSION__"></script>
__ASSET_VERSION__ は配信時にコミットハッシュへ置換します。index.html は素通しで届くので、そこに版を差し込めばよい。
トリガーのパスにも一つ罠がありました。enebular の HTTP トリガーはトリガーのパスを含めたパスでハンドラを呼びます。/imanouchi/v1/health が来る。/v1/health ではない。
環境変数でトリガーのパスを持つと、実設定とずれた瞬間に全リクエストが404になり、「関数は動いているのに何も応答しない」という一番切り分けにくい壊れ方をします。なので3通りマウントして吸収しました。
app.route("/", createRoutes()); // /v1/health ローカル・テスト
app.route("/:base", createRoutes()); // /imanouchi/v1/health トリガー経由
app.route("/:base/", createRoutes()); // /imanouchi/ トリガーのルートURL
美しくはないですが、一旦これで。
歩きスマホを助長しないという、外せない制約
位置情報ゲームです。人を街に歩かせます。
歩きスマホを増やしたら、このサービスは存在してはいけない。
これは機能ではなく前提だと考えて、実装に落としました。歩き出すと地図の操作を止めて画面を伏せる。進捗は音で知らせる。立ち止まると自動で戻る。
問題は、LINEのWebViewでそれが本当に動くのかが仕様を読んでも分からないことでした。なので確認ページを作って、実機で測りました。
| 機能 | iOS | Android |
|---|---|---|
| 通知音 | ○ 鳴る | ○ 鳴る |
| 振動(Vibration API) | ✕ | ○ |
| 画面の常時点灯(Wake Lock) | ○ | ○ |
振動はiOSで使えない。 だから「振動で知らせる」設計にはできませんでした。音を主役にして、振動は補助に回しました。
生成AIを、役割で二段に分ける
一番考えたところです。
素朴に作ると、クイズを出すたびに高性能モデルを叩きます。するとコストが利用者数に比例します。
なので二段に分けました。
この設計は、前のハッカソンで作った SocraMetry からの持ち込みです。 あちらも役割ごとにモデルを出し分けていて、原価を45%落としました(実測)。今回は、その分け方を「取り込み」と「利用」に当てはめています。
| いつ | モデル | 何をするか |
|---|---|---|
| 取り込み時(1回) | Claude Opus 5 | オープンデータの表記ゆれ・欠損・自由記述を、出典と取得日を保った構造化ナレッジへ整える |
| 利用時(都度) | Gemini の軽量モデル | 整ったナレッジだけを材料に、その場の防災クイズと解説を作る |
これで効くことが3つあります。
コスト構造が変わる。 高性能モデルの呼び出しはデータ件数に比例します(370件、しかも増分のみ)。利用者数には比例しません。「利用者が増えるほど高性能モデルを叩く」形を避けられます。
品質が落ちない。 軽量モデルが弱いのは「散らばった情報から意味を組み立てる」作業で、それは取り込み時に済んでいます。整った入力から文を作るだけなら軽量で足ります。
安全を、注意書きではなく構造で担保できる。 防災の文言の誤りは命に関わります。都度生成に自由な知識を持たせず、人が検収した一度ぶんのナレッジの範囲でしか書かせない。「生成AIを防災に使って大丈夫か」への答えを、運用ルールではなく構造で示せます。
どちらのモデルで出したかを記録する欄も、最初から持たせました。
QuizPrompt.generatedBy: 'fixture' | 'llm'
生成が落ちて固定データに落ちたことを、隠さないためです。
なぜ3択なのか
集めたいのは「段差があるか」のような事実です。素朴にやれば「はい/いいえ」の2択です。
2択にしませんでした。
export const SURVEY_VALUES = ["yes", "no", "unknown"] as const;
2値は「無い」と「見ていない」を潰します。 見ていない人に二択を出せば、どちらかを押します。そして公開データの精度が、そのぶん落ちます。
ポイントの設計も、ここに合わせました。加点は答えの中身では変わりません。 「はい/いいえ」に加点して「わからない」に加点しない形にすると、分からないのに断定する動機を作ってしまう。倍率はスポット側の欠損数だけで決めています。
そして1人の回答では確定させません。別々に歩いた数人の答えが揃って初めて「検証済み」にして、そのぶんだけを行政へ返します。何人で確定とするかは設定で変えられるようにしてあり、実証実験で詰めるつもりです。
報酬つきのアンケートは、必ず「適当に答えて報酬」を生む。 そこを塞がないと、集めたデータに意味がなくなります。
この先、どこを置き換えるか
ここまでの工夫は、プロトタイプの制約から出てきたものです。実証実験へ進むなら、いくつかは素直な作りに戻します。
| いまの作り | そうした理由 | この先 |
|---|---|---|
| エリア単位でまとめて配り、距離は端末で計算 | 地理検索が無い | 地理検索のあるDBに移せば、サーバー側で「近い順」を返せる |
| 書き込み時にカウンタを1つ増やす | 集計も加算も無い | 加算がそのまま効くDBなら、取りこぼしの心配が消える |
| 音を主役にして、振動は補助 | iOSのWebViewで振動が使えない | ネイティブアプリなら、振動も含めて選べる |
インフラも、データ量と同時アクセスが増えれば AWS 側へ寄せることになります。いまの形は、370件・数人で動かすプロトタイプに対して過不足がないというだけです。
ただし、置き換えたくないものもあります。
LINEミニアプリでインストールが要らないことは、災害時の入口として譲れません。ネイティブアプリにすると、そこを失います。振動のために入口を狭めるのでは本末転倒なので、やるなら片方を捨てずに両方を持つ形になります。
おわりに
作りながら、ずっと引っかかっていたことがあります。
「寄り道」を名前に冠したのに、直近1週間で寄り道した人は**24.3%**しかいなかった。4人に1人です。都合の悪い数字でした。
でも自由記述を読んだら、解釈が変わりました。
「なんとなく別の道を通ってみた」
「気分転換にルートを変えた」
「たまには違うルートで散歩をしてみよう!となりました」
寄り道した9人の動機は、どれも目的がなく、軽い。
つまり寄り道は「強い動機のある人だけの行動」ではなく、きっかけがあれば起きる行動でした。
24.3%という数字は「寄り道する人が少ない」ではなく、**「寄り道のきっかけが、街に存在していない」**と読むべきだった。
きっかけを置くこと。それがこのサービスのやることなんだと、そこで腹が決まりました。
避難所の名前しか知らない19人のうちの1人が、散歩の帰りに少し遠回りをして、トイレの段差をひとつ記録する。
それが積もったとき、誰かの避難経路が変わる。
そういう遠回りを、作りたかった。
チーム SMOOTH WORLD(6人)で作りました。まだ実証実験の前です。
まずは、消防団や防災士の方などの防災意識の層に実証実験の協力をしてもらい、データを組み立てていこうと考えています。
その後、ゲーム性をブラッシュアップした上で、展開していこうと思っています。

