日本は世界有数の地震多発国です。気象庁は日々膨大な地震情報を発表していますが、そのデータをプログラムから手軽に扱える形で提供しているサービスは限られています。

本記事では、P2PQuake が公開している JSON API v2 を利用し、直近の日本の地震データを取得・整形・可視化するインタラクティブなダッシュボードを Streamlit で構築した技術的なプロセスを、データエンジニア向けに詳しく解説します。

GitHub リポジトリ:https://github.com/kkoch5t2/streamlit_japan_earthquake_analysis

Streamlit とは何か ― データエンジニアがフロントエンドを持てる時代

従来の課題

データエンジニアやデータサイエンティストが分析結果を社内外に共有しようとすると、多くの場合「可視化の壁」に直面します。Jupyter Notebook を共有しても非エンジニアには使いにくく、BI ツール(Tableau や Looker)は導入コストが高い。Flask や Django で Web アプリを自作すればフロントエンド(HTML/CSS/JavaScript)の知識が必須になります。

Streamlit が解決すること

Streamlit は、Python スクリプトを書くだけでインタラクティブな Web アプリケーションを構築できるオープンソースのフレームワークです。2019 年に登場して以来、データコミュニティで爆発的に普及しました。その理由は明快です。

特徴 詳細
Pure Python HTML/CSS/JS を一行も書かずに UI を構築できる。st.slider() と書くだけでスライダーが出現する。
リアクティブ実行モデル ユーザーが UI を操作するたびにスクリプト全体が上から下へ再実行される。状態管理の複雑さが根本的に排除される。
データライブラリとの統合 Pandas DataFrame を st.dataframe(df) で即座にテーブル表示。Plotly のグラフオブジェクトを st.plotly_chart(fig) で1行レンダリング。
キャッシュ機構 @st.cache_data デコレータで関数の戻り値をメモ化。再実行のたびに API を叩く無駄を排除できる。
ゼロ構成デプロイ streamlit run app.py の一行で開発サーバーが起動。Streamlit Community Cloud を使えば GitHub 連携で即デプロイも可能。

本プロジェクトではこの Streamlit の強みをフルに活用し、わずか 255 行の Python コードで多機能な地震分析ダッシュボードを完成させています。


プロジェクトの全体アーキテクチャ

このダッシュボードは、典型的な ETL(Extract → Transform → Load/Visualize) パイプラインを単一の Python スクリプト内に凝縮した構成です。

DATA PIPELINE FLOW

🌐 P2PQuake API

📥 Requests (Extract)

🔧 Pandas (Transform)

📊 Plotly / PyDeck (Visualize)

🖥️ Streamlit (Serve)

それぞれのレイヤーでどんな技術的判断をしたのか、次章から詳しく掘り下げていきます。


P2PQuake API の仕様とデータ取得戦略

API の概要

P2PQuake JSON API v2 は、気象庁の地震情報をリアルタイムに JSON 形式で配信している公開 API です。認証キーは不要で、誰でも自由にアクセスできます。

本プロジェクトでは、地震情報(code: 551)を取得する以下のエンドポイントを利用しています。

GET https://api.p2pquake.net/v2/history?codes=551&limit=100

API の制約と設計上の対処

この API には 1リクエストあたり最大100件 という取得制限があります。これはアプリケーション設計に直接影響する制約です。

  • 対処①:取得件数の明示 ― アプリの UI 上に「※APIの仕様上、取得・表示できるデータは最新の100件まで」と補足を表示し、ユーザーに期待値を正しく伝えています。
  • 対処②:アプリ側でのフィルタリング ― 100件のデータを一括取得した後、Pandas の DataFrame 上で期間・マグニチュード・震度によるフィルタリングを行います。フィルター操作のたびに API を再呼び出しする必要がないため、レスポンスが高速です。

レスポンスの構造

API から返却される JSON は深くネストされた構造です。1件の地震データは以下のような形をしています。

{
  "code": 551,
  "earthquake": {
    "time": "2026/08/02 01:19:00",
    "maxScale": 10,
    "hypocenter": {
      "name": "熊本県天草・芦北地方",
      "latitude": 32.5,
      "longitude": 130.5,
      "depth": 0,
      "magnitude": 2.8
    }
  },
  "points": [
    { "pref": "熊本県", "addr": "上天草市松島町", "scale": 10 }
  ]
}

この earthquake.hypocenter の中に地震の本質的なデータ(震央名・緯度経度・深さ・マグニチュード)が、earthquake.maxScale に最大震度が格納されています。次章でこのネスト構造をどうフラット化し、分析可能な DataFrame にするかを解説します。


データの前処理 ― 泥臭いクレンジングと変換の実際

API から取得した生データをそのまま可視化に回すことはできません。データエンジニアリングの根幹である「前処理」をしっかり行っています。

JSON のフラット化(非正規化)

ネストされた JSON をループで走査し、必要なフィールドを1階層のレコード(辞書)として抽出しています。item["earthquake"]["hypocenter"]["latitude"] のような深い階層へのアクセスは、キーが存在しない場合に KeyError を発生させるリスクがあるため、.get() メソッドでデフォルト値を設定しつつ安全にアクセスしています。

震度スケールの変換(エンリッチメント)

P2PQuake API は最大震度を独自の数値コードで返却します。これは API 利用者にとって最も注意が必要なポイントです。

API の値 (maxScale) 10 20 30 40 45 50 55 60 70
日本の震度階級 震度1 震度2 震度3 震度4 震度5弱 震度5強 震度6弱 震度6強 震度7

この対応を Python の辞書(scale_map)として定義し、.get(max_scale_val, "不明") でマッピングしています。マッピングに該当しない未知の値が来ても「不明」として安全にフォールバックする設計です。

異常値・無効データのハンドリング

実運用で最も重要なのが、データの信頼性を確保するクレンジング処理です。

  • 震源不明データの除外: 震央が特定できなかった地震は、API が緯度・経度に -200 を返します。この値をそのまま地図にプロットすると、アフリカ大陸の遥か南に点が打たれてしまいます。lat == -200 or lon == -200 の条件でフィルタリングしています。
  • 座標範囲の論理チェック: 緯度は -90〜90、経度は -180〜180 の範囲外の値を持つレコードも除外しています。これにより、API 側の障害や将来的な仕様変更で異常値が混入した場合でも、マップの描画が破綻しません。
  • 時刻データの型変換: API は時刻を "2026/08/02 01:19:00" のような文字列で返します。これを pd.to_datetime() で Pandas の Datetime 型に変換することで、後続の時系列集計・日付フィルタリング・ソートが可能になります。
  • 例外の握りつぶし: 各レコードの処理を try-except で囲み、1件でも不正なデータがあった場合にアプリ全体がクラッシュしないようにしています。データが不完全でも「表示できるものだけ表示する」という方針です。

フィルタリング設計 ― ユーザーに「分析の自由度」を渡す

Streamlit のサイドバーに3種類のフィルターを配置し、ユーザーが自由にデータをスライスできるようにしています。

期間フィルター(st.sidebar.date_input

取得済みデータの最古・最新の日時を自動検出し、それを min_value / max_value に設定しています。ユーザーは取得済みデータの範囲内でのみ日付を選択できるため、「データがない期間を選んで何も表示されない」というUXの問題を未然に防いでいます。

さらに、date_input はタプルで2つの日付を返す場合(範囲指定)と1つの場合(単日指定)があるため、len(selected_dates) で分岐する実装を入れています。

マグニチュードフィルター(st.sidebar.slider

0.0〜9.0 の範囲を 0.1 刻みで指定可能なスライダーです。初期値は 1.0 に設定し、極端に小さな地震を除外しつつも広い範囲が見えるバランスを取っています。

震度フィルター(st.sidebar.selectbox

「すべて」「1」「2」…「7」の選択肢を用意。ユーザーが選択した震度に対応する数値コードへ逆変換し、DataFrame のフィルタリングに使用しています。

これら3つのフィルターは すべて AND 条件で連動 しています。フィルターを変更するたびに Streamlit が自動で再実行し、KPI サマリー・マップ・グラフ・テーブルのすべてが即座に更新されます。


可視化① 地理空間マッピング(PyDeck)

なぜ PyDeck を選んだのか

Streamlit には st.map() というビルトインのマップ機能がありますが、表現力に限界があります。PyDeck(deck.gl の Python バインディング)を採用した理由は以下の通りです。

  • マグニチュードに応じた円の半径のカスタマイズ
  • 色・透明度の細かい制御
  • マウスホバー時のツールチップ表示

マグニチュードの視覚的スケーリング

本プロジェクトで最も工夫したポイントの一つが、バブル半径の計算式です。

filtered_df["radius"] = (10 ** (filtered_df["マグニチュード"] / 2.0)) * 200

地震のエネルギーはマグニチュードが 1 上がると約 32 倍になります(対数スケール)。単純に「マグニチュード × 定数」で半径を計算すると M2 と M5 の視覚的差異が伝わりません。指数関数 10^(M/2) を使うことで、大きな地震ほど劇的に円が大きくなり、地図を一目見ただけで規模の違いを直感的に把握できます。

マグニチュード 計算結果 (radius) 倍率(M1 基準)
M 1.0 632 ×1.0
M 2.0 2,000 ×3.2
M 3.0 6,325 ×10
M 4.0 20,000 ×32
M 5.0 63,246 ×100
M 6.0 200,000 ×316

ツールチップの設計

マウスホバーで「震央地名」「マグニチュード」「最大震度」「発生日時」「深さ」の5つの情報を表示するよう設定しています。特に「深さ」を追加したことで、プレート境界型の深発地震なのか、内陸の浅い直下型地震なのかを、マップ上だけで判別できるようにしています。


可視化② 統計グラフ群(Plotly Express)

「📈 分析グラフ」タブでは、2カラムレイアウトに4つのグラフを配置し、多角的な分析を可能にしています。

マグニチュード分布(ヒストグラム)

地震の規模がどの範囲に集中しているかを把握するためのヒストグラムです。nbins=10 を指定し、M1〜M7 程度の範囲をバランスよくビニングしています。

最大震度分布(棒グラフ)

ここで重要な技術的工夫が、カテゴリカルデータの完全性担保です。

scale_order = ["1", "2", "3", "4", "5弱", "5強", "6弱", "6強", "7"]
scale_counts = filtered_df["最大震度"].value_counts().reindex(scale_order).fillna(0)

Pandas の value_counts() は、データに存在する値しかカウントしません。つまり、直近100件に震度7の地震がなければ、X軸から「7」が消えてしまいます。reindex(scale_order).fillna(0) を使うことで、発生が0回の震度階級もX軸に表示し、常に一貫したグラフ構造を保っています。これは BI ツールでは自動化されがちですが、コードベースではエンジニアが意識的に行う必要がある処理です。

地域別発生回数ランキング(横棒グラフ)

value_counts().head(10) で上位10地域を抽出し、横棒グラフで表示しています。yaxis={'categoryorder':'total ascending'} を設定することで、回数が多い地域が上に来る直感的なランキング表示を実現しています。

震源の深さ vs マグニチュード(散布図)

多変量解析の入口となる散布図です。X軸に深さ、Y軸にマグニチュードを取り、さらにカラーマップ(YlOrRd:黄→橙→赤のグラデーション)で視覚的な第3軸を追加しています。これにより、以下のような仮説の検証が可能です。

  • 「浅い場所で発生する地震は規模が小さい傾向にあるか?」
  • 「深さ 300km 以上の深発地震はどの程度のマグニチュードか?」

可視化③ 時系列分析

「⏱️ 時系列分析」タブでは、時間軸での地震活動のトレンドを2つのグラフで捉えます。

地震発生回数の時間分布(ヒストグラム)

Plotly の px.histogram に Datetime 型のカラムを渡すと、自動的に時間のビニングを行い、「どの時間帯に地震が集中していたか」を把握できます。nbins=20 で適度な粒度を確保しつつ、bargap=0.1 でバー間に隙間を入れて視認性を高めています。

マグニチュードの時系列推移(折れ線グラフ)

個々の地震を時間順に結んだ折れ線グラフです。線の色(青:#4B8BBE)とマーカーの色(赤:#FF4B4B)を分けることで、全体のトレンドライン(線)と個々のデータポイント(点)の両方を一目で認識できるデュアルビジュアル設計にしています。

hover_data に「震央地名」「最大震度」を追加しており、グラフ上の任意のポイントにカーソルを合わせるだけで、どこでどの規模の地震が起きたかをドリルダウンして確認できます。


パフォーマンス最適化 ― キャッシュ戦略と TTL 設計

Streamlit のリアクティブ実行モデル(UIが変わるたびにスクリプト全体を再実行)は、開発の簡便さと引き換えにパフォーマンスの懸念を生みます。本プロジェクトでは以下の戦略でこれを解消しています。

@st.cache_data(ttl=300) によるメモ化

@st.cache_data(ttl=300)
def fetch_earthquake_data():
    url = "https://api.p2pquake.net/v2/history?codes=551&limit=100"
    response = requests.get(url, timeout=10)
    return response.json()

このデコレータは、関数の引数と戻り値をシリアライズしてキャッシュします。ttl=300(5分間)を設定することで、以下の動作を実現しています。

  • 初回アクセス時: API にリクエストを送信し、レスポンスをキャッシュに保存。
  • 5分以内の再アクセス時: API を呼ばず、キャッシュからデータを返却。ユーザーがフィルターを何回操作しても API コールは発生しない。
  • 5分経過後: キャッシュが失効し、次のアクセス時に最新データを取得。

この TTL の「5分」という値は、「地震データのリアルタイム性」と「API サーバーへの負荷軽減」のバランスを考慮して設定しています。


技術スタック一覧とまとめ

役割 技術 本プロジェクトでの利用箇所
データ取得 Requests P2PQuake API への GET リクエスト、タイムアウト制御
データ整形 Pandas JSON → DataFrame 変換、フィルタリング、集計、型変換
地図描画 PyDeck (deck.gl) ScatterplotLayer による震央プロット、ツールチップ
グラフ描画 Plotly Express ヒストグラム、棒グラフ、散布図、折れ線グラフ
フレームワーク Streamlit UI全体(サイドバー、タブ、メトリクス、レイアウト)、キャッシュ制御
データソース P2PQuake JSON API v2 気象庁の地震情報をリアルタイム配信する公開API

まとめ

このプロジェクトは、「公開APIからリアルタイムデータを取得し、前処理を行い、多角的に可視化する」というデータエンジニアリングの基本的なワークフローを、Streamlit を活用して極めてコンパクトに実現した実践例です。

特に注目すべきは以下の3点です。

  1. 200行台のコードで完結 ― Streamlit の抽象化により、フロントエンド開発の工数がゼロ。
  2. 泥臭い前処理の重要性 ― 震度スケールの変換、無効座標の除外、日時の型変換。「きれいなデータ」は自動的には降ってこない。
  3. 可視化の使い分け ― 地理空間データには PyDeck、統計分析には Plotly Express と、データの性質に応じた最適なライブラリを選定。

データエンジニアが「データの価値をすばやくビジネスに届ける」時代において、Streamlit は最強の武器の一つです。ぜひ皆さんのプロジェクトでも活用してみてください。