この用語と一緒に検索・参照されやすいタグ。 関連ページに飛ぶときの手がかりにも使えます。
このページで繰り返し出てくる用語。 窓で集計する道具(上の 3 つ)、 速さの制約(次の 3 つ)、 画面を誤読させない工夫(その次の 4 つ)、 実装に使うライブラリ(最後の 3 つ)の順に並べた。
🍰 まずはやさしく
今のデータをすぐにグラフにする方法です。
最新の状況をすぐに知るために使います。
スマホで株価や電車の位置を見るのが例です。
この章では基本のやり方と注意点を読みます。
リアルタイム可視化は、 データが届くと同時にグラフ・地図・指標を更新し続ける可視化手法。
🍰 まずはやさしく
今の状況を判断するための道具です。
正しい答えを出すために使います。
部活の記録をすぐに画面に出すような場面です。
どんな問いに答える道具なのかを読みます。
SSDSE のような年次データはリアルタイム要素は薄いですが、 「分析結果を BI に乗せる」段階で関連します。 業務監視・意思決定支援システムの中核。
🍰 まずはやさしく
今の瞬間を映し出す鏡のようなものです。
直感的に状況を掴むために使います。
スポーツの試合中の計測データが例です。
まずはイメージで全体の仕組みを読みます。
「リアルタイム可視化」を最初に学ぶときは、 厳密な定義よりイメージを優先しましょう。 以下は具体例・比喩を用いた直感的理解の入口です。
リアルタイム可視化は「到着し続けるデータを 1〜10 秒以内に画面に反映する」可視化の枠組み。 IoT・株価・SNS・スポーツ計測などで使われる。 SSDSE-B-2026 のような年次集計データは「リアルタイム」ではないが、 「毎年の更新で図が自動でリフレッシュされる」ダッシュボードは同じ設計思想。
リアルタイム可視化 (Realtime Visualization) とは、 ストリーミング的に到来するデータを 1 秒以下のレイテンシで描画し続ける手法。 IoT センサ、 株価、 SNS トレンド、 公共交通モニタリング、 災害情報共有などで必須。
「単に matplotlib をループで叩く」のとは違う。 真の難所は次の 5 点。
🍰 まずはやさしく
ルールを数式(計算式)で表したものです。
正確に意味を伝えるために使います。
直近の平均点を計算して出すような仕組みです。
記号が何を表しているかを詳しく読みます。
やさしい説明で掴んだ感覚を、ここで ストリーム集計(スライディングウィンドウ) の定義式に対応づけます。下の式は左辺 $\bar{x}_t$ が何で決まるかを右辺で書き下したもので、x̄(平均)、Σ(合計)、分数(割り算) が現れます。それぞれの記号が何の量を指すのかは、次の「🔬 数式を言葉で読み解く」で 1 つずつ確かめてください。
$$ \bar x_t^{(W)} = \frac{1}{W} \sum_{i=t-W+1}^{t} x_i $$
$$ \mathrm{EMA}_t = \alpha \cdot x_t + (1 - \alpha) \cdot \mathrm{EMA}_{t-1}, \quad \alpha = \frac{2}{W + 1} $$
$$ P(\text{要素 } x_t \text{ がサンプルに残る}) = \frac{k}{t} $$
$t$ 件目を見た時点で、 サイズ $k$ のリザーバの中に保持される確率は一様 $k/t$。 全データを保存せずとも、 一様ランダムサンプルが維持できる。
📐 章の 2 つの式を、 東京都の総人口(A1101)を「1 年度 = 1 件」として到着順に流す擬似ストリームで読む。 最新の到着を 2023 年度、 窓幅を W = 3 とする。
| 記号 | 言葉にすると | 東京都の擬似ストリームでの値 |
|---|---|---|
| $t$ | いちばん新しく届いた時刻(画面を描き直す瞬間) | 2023 年度 |
| $W$ | 画面に効かせる過去の件数(窓幅)。 大きいほど線は滑らかだが、 変化への反応が遅い | 3 |
| $i = t-W+1, \dots, t$ | 窓に入っている時刻の範囲。 これより古い値は平均から外れる | 2021・2022・2023 年度 |
| $x_i$ | 各時刻に届いた生の観測値 | 14,010,000 / 14,038,000 / 14,086,000 人 |
| $\frac{1}{W}\sum x_i$ | 窓の中だけの平均。 ダッシュボードの「直近 W 件の平均」 | 42,134,000 ÷ 3 = 14,044,666.67 人 |
| $T_{\text{ingest}}$ | データが発生してから集計側に届くまでの時間 | 年次統計では「年度末から公表まで」の数か月にあたる |
| $T_{\text{process}} + T_{\text{render}}$ | 窓の更新と描画にかかる時間。 窓平均は 1 件ごとに足して引くだけなので件数に依存しない | 1 件あたり加算 1 回・減算 1 回・除算 1 回 |
和の範囲 $i=t-W+1$ から $t$ が 1 つずれるたびに、 入る値は最新の 1 件、 抜ける値は最古の 1 件だけである。 そのため窓平均は全件を足し直さなくても $\bar{x}_{t} = \bar{x}_{t-1} + (x_t - x_{t-W})/W$ で更新できる。 2022 年度の窓平均 14,031,864.67 人から、 2023 年度に入る 14,086,000 人と抜ける 2020 年度の 14,047,594 人の差 38,406 人を 3 で割った 12,802 人を足すと 14,044,666.67 人になり、 表の値と一致する。 レイテンシの式の側から見ると、 この「足して引くだけ」の更新が $T_{\text{process}}$ を件数によらず一定に保つ仕組みである。
ダッシュボード設計の典型レイヤ:
| レイヤ | 更新頻度 | 用途 |
|---|---|---|
| 速報(生) | 秒 | 異常検知 |
| 近未来(5分集計) | 分 | 運用判断 |
| 傾向(時間集計) | 時 | シフト判断 |
| 戦略(日次) | 日 | KPI 確認 |
SSDSE-B-2026 (年度×都道府県 のパネル) を擬似ストリームとして再生し、 リアルタイム可視化の挙動を学ぶ。 1 年分 = 1 イベント (47 県分) と見立てて 0.5 秒間隔で配信。
📥 入力データ:
🎯 このコードでやること: SSDSE データを年度順に「1 年あたり 0.5 秒」で送り出し、 47 都道府県の人口推移をリアルタイムに折れ線描画する最小例。
📥 入力: SSDSE-B-2026 全行 (564 行)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import pandas as pd import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.dropna(subset=['A1101']).sort_values(['Prefecture','SSDSE-B-2026']) prefs = df['Prefecture'].unique()[:5] # 5 県だけ可視化 (見やすさ) years = sorted(df['SSDSE-B-2026'].unique()) fig, ax = plt.subplots(figsize=(10, 5)) lines = {p: ax.plot([], [], label=p)[0] for p in prefs} ax.set_xlim(years[0], years[-1]) ax.set_ylim(0, df[df['Prefecture'].isin(prefs)]['A1101'].max() * 1.1) ax.legend() ax.set_xlabel('SSDSE-B-2026'); ax.set_ylabel('人口 (人)') def update(frame_year): for p in prefs: sub = df[(df['Prefecture']==p) & (df['SSDSE-B-2026']<=frame_year)] lines[p].set_data(sub['SSDSE-B-2026'], sub['A1101']) ax.set_title(f'年度: {frame_year}') return list(lines.values()) ani = FuncAnimation(fig, update, frames=years, interval=500, blit=False, repeat=False) ani.save('rtv_demo.gif', writer='pillow', fps=2) print('saved rtv_demo.gif') |
📤 実行例:
💬 prefs は unique()[:5] で取っているので、人口の大きい県ではなく並べ替えで先に来る 5 県(三重・京都・佐賀・兵庫・北海道)が描かれ、どれも右下がりの線になる。東京都などを見たいときは prefs を名前で指定する。FuncAnimation は最も学習コストが低い手法。 ただしブラウザ表示には GIF/MP4 化が必要。 本番ストリーミングには bokeh/Dash 系を選ぶ。 SSDSE のように既知データを「擬似ストリーム再生」する練習に最適。
合成データでリアルタイム可視化のメモリ使用量を計算する。
| 項目 | 値 |
|---|---|
| 更新頻度 | 10 Hz |
| 1 ポイント | 20 byte |
| 保持時間 | 1 時間 |
1 2 3 4 5 6 7 | hz = 10 point_b = 20 hours = 1 points = hz * 3600 * hours mem_kb = points * point_b / 1024 print(f"ポイント数: {points:,}") print(f"メモリ: {mem_kb:.1f} KB") |
💬 手計算 (Step 2) 703 KB と Python 出力が完全一致。
上のメモリ計算に続けて、 📐 章のスライディングウィンドウ平均と EMA を実データで手計算し、 同じ値を Python で再現する。 データは SSDSE-B-2026 の東京都 A1101(総人口)を年度の昇順に並べたもの。
| 年度 | 2019 | 2020 | 2021 | 2022 | 2023 |
|---|---|---|---|---|---|
| A1101(人) | 14,007,000 | 14,047,594 | 14,010,000 | 14,038,000 | 14,086,000 |
Step 1(2021 年度の窓平均、 W = 3):(14,007,000 + 14,047,594 + 14,010,000) ÷ 3 = 42,064,594 ÷ 3 = 14,021,531.33 人。
Step 2(2022 年度へ足し引き更新):入る 14,038,000、 抜ける 14,007,000。 14,021,531.33 + (14,038,000 − 14,007,000) ÷ 3 = 14,021,531.33 + 10,333.33 = 14,031,864.67 人。
Step 3(2023 年度へ足し引き更新):14,031,864.67 + (14,086,000 − 14,047,594) ÷ 3 = 14,031,864.67 + 12,802 = 14,044,666.67 人。
Step 4(EMA、 α = 2/(W+1) = 0.5):2012 年度の値を初期値にして 2022 年度まで回した EMA は 14,013,472.77 人。 2023 年度は 0.5 × 14,086,000 + 0.5 × 14,013,472.77 = 7,043,000 + 7,006,736.39 = 14,049,736.38 人。 EMA は最新値に重み 0.5 を置くので、 窓平均(14,044,666.67 人)より最新の 14,086,000 人に近い。
🎯 このコードでやること:東京都の総人口を 1 年度ずつ流し、 窓の合計だけを持ち歩く足し引き更新と EMA を自前で計算して、 pandas の rolling・ewm と一致するかを確かめる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from collections import deque import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) tokyo = (df[df['Prefecture'] == '東京都'] .sort_values('SSDSE-B-2026') # CSV は新しい年度が先なので昇順に並べ直す .set_index('SSDSE-B-2026')['A1101']) # 総人口(人) W = 3 win, s = deque(), 0.0 # 窓の中身と、その合計だけを持ち歩く ema, alpha = None, 2 / (W + 1) # alpha = 0.5 for year, x in tokyo.items(): win.append(x); s += x if len(win) > W: s -= win.popleft() # 最古の 1 件を引く → O(1) 更新 ema = x if ema is None else alpha * x + (1 - alpha) * ema if year >= 2021: print(f'{year}: x={x:,} 窓平均={s / len(win):,.2f} EMA={ema:,.2f}') # pandas の rolling / ewm と一致するか print('rolling(3) 2023 =', f"{tokyo.rolling(W).mean()[2023]:,.2f}") print('ewm(0.5) 2023 =', f"{tokyo.ewm(alpha=alpha, adjust=False).mean()[2023]:,.2f}") |
💬 Step 1〜3 の窓平均 14,021,531.33・14,031,864.67・14,044,666.67 人と、 Step 4 の EMA 14,049,736.38 人が手計算と一致し、 pandas の rolling(3)・ewm(alpha=0.5, adjust=False) とも同じ値になった。 自前の更新は窓の中身 3 件と合計 1 つしか持たないが、 全件を足し直す rolling と同じ結果を返す。
st.empty() プレースホルダを 1 秒ごとに書き換え、 SSDSE-B-2026 から無作為に選んだ 5 行の表を 10 回差し替える(リアルタイム表示の最小形)。data/raw/SSDSE-B-2026.csv(47 都道府県 × 12 年度 = 564 行 × 112 列)。 毎回 df.sample(5) で 5 行を取り出す。1 2 3 4 5 6 7 8 9 | # Streamlit でリアルタイム風の表示(streamlit run app.py で起動する) import streamlit as st, pandas as pd, time ph = st.empty() df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) # 2 行目の日本語ラベルを飛ばす for i in range(10): snapshot = df.sample(5) # 1 秒ごとに 5 行を無作為に選び直す(seed なしなので毎回変わる) ph.dataframe(snapshot) time.sleep(1) |
streamlit run app.py で起動したブラウザ画面に、 5 行 × 112 列の表が 1 秒ごとに 10 回入れ替わって表示される(標準出力は無い)。 seed を指定していないので、 選ばれる行は実行ごとに変わる。ph を上書きしているので画面上の表は常に 1 つだけで、 過去の 5 行は残らない。 これは「最新の状態だけ見せる」表示で、 推移を見せたいなら折れ線に追記していく(下の FuncAnimation・Bokeh の例)か、 直近 N 件を保持して描き直す必要がある。 python app.py で直接実行すると画面は出ず、 警告だけが出て 10 秒で終わる。▶ 実行 を押せばこのページの中でそのまま動きます(ライブラリもデータも同梱済みで、 準備は要りません)。 手元の Python に移して動かすときは pip install matplotlib numpy pandas が必要です。 読んでいるデータは data/raw/SSDSE-B-2026.csv。 日本語を含むので encoding='cp932' の指定を落とさないでください。
公的統計(SSDSE-B-2026)を題材に、 最小限の Python コードで リアルタイム可視化 を動作させます。 まずはこのまま実行してみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # リアルタイム可視化 を SSDSE-B-2026 で実行する最小コード import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 2023 年のみ抽出 print(df.shape) # (47, 112) print(df[['Prefecture','A1101','A1303','L3221']].head()) # Plotly Dash 風の最小コード(教育用) import pandas as pd import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation fig, ax = plt.subplots(figsize=(10,5)) def update(frame): ax.clear() subset = df.head(frame+1) ax.bar(subset['Prefecture'], subset['A1101']) ax.set_xticklabels(subset['Prefecture'], rotation=90) # anim = FuncAnimation(fig, update, frames=len(df), interval=200) print('FuncAnimation で 200ms ごとに 47 県を順次描画する設計') |
💬 前半では 2023 年の 47 行 × 112 列を読み込めていることを、北海道から秋田県までの 5 行で確かめている。FuncAnimation の行はコメントアウトされているので、実際に出力されたのは最後の説明文だけで、アニメーションは描かれていない。47 フレームを 200 ms 間隔で回すと 1 周 9.4 秒になり、動かすにはこの行を有効にして Jupyter なら HTML(anim.to_jshtml()) で表示する。
上のコードで動かない場合は、 ①必要なパッケージがインストール済みか(pip install pandas scikit-learn scipy statsmodels matplotlib)、 ②データファイルが data/raw/SSDSE-B-2026.csv に存在するか、 ③encoding='cp932' になっているかを確認してください。
🎯 このコードでやること: 北海道の消費支出(L3221、 二人以上の世帯・月平均)を 2012〜2023 年度の時系列として描き、 生データ・3 年移動平均 (SMA)・EMA (α=0.4) を重ねて比較する(完全失業率は SSDSE-B-2026 に収録されていないため消費支出で代える)。
📥 入力: SSDSE-B-2026 の L3221 列(消費支出、 円)の北海道 12 年度分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import pandas as pd, numpy as np import matplotlib.pyplot as plt # 英字の項目コードを使うので skiprows=[1] で読む。 # なお E0301(完全失業率)は SSDSE-B-2026 には収録されていないため、 # 実在する L3221(消費支出)を平滑の題材にする。 df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.dropna(subset=['L3221']).sort_values(['Prefecture','SSDSE-B-2026']) pref = '北海道' sub = df[df['Prefecture']==pref].copy() # 単純移動平均 (window=3) sub['SMA3'] = sub['L3221'].rolling(3, min_periods=1).mean() # EMA (alpha=0.4) alpha = 0.4 ema = [] prev = sub['L3221'].iloc[0] for v in sub['L3221']: prev = alpha*v + (1-alpha)*prev ema.append(prev) sub['EMA'] = ema plt.figure(figsize=(10,4)) plt.plot(sub['SSDSE-B-2026'], sub['L3221'], 'o-', label='raw', alpha=0.5) plt.plot(sub['SSDSE-B-2026'], sub['SMA3'], 's-', label='SMA(W=3)') plt.plot(sub['SSDSE-B-2026'], sub['EMA'], '^-', label='EMA(α=0.4)') plt.legend(); plt.title(f'{pref}: 消費支出の平滑') plt.xlabel('SSDSE-B-2026'); plt.ylabel('消費支出 (円)') plt.tight_layout(); plt.savefig('rtv_ema.png') print(sub[['SSDSE-B-2026','L3221','SMA3','EMA']].to_string(index=False)) |
📤 実行例:
💬 2021 年度に生データが 301,683 円から 268,396 円へ 3.3 万円落ち込むと、EMA は 293,176 → 283,264 円と約 1 万円だけ下がり、SMA3 は 288,254 円にとどまる。2023 年度の反発(296,888 円)でも EMA は 287,387 円まで戻すのに、SMA3 は 2021 年度の落ち込みを窓に含むため 281,007 円と低いまま。EMA は新値への追随が早く (α=0.4 で半減期≈1.4 年)、 SMA は窓の長さだけ遅れる。 リアルタイム可視化では「最新値を即座に反映したい × でも変動を抑えたい」のトレードオフで α を選ぶ。
🎯 このコードでやること: Dash の dcc.Interval で 1 秒ごとに再描画する Web ダッシュボードの最小例。 SSDSE データを擬似ストリームとして年度ごとに描画。
📥 入力: SSDSE-B-2026 (前処理済 df)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import pandas as pd from dash import Dash, dcc, html, Output, Input import plotly.graph_objects as go df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.dropna(subset=['A1101']) years = sorted(df['SSDSE-B-2026'].unique()) app = Dash(__name__) app.layout = html.Div([ html.H2('SSDSE 人口 リアルタイム可視化 (擬似)'), dcc.Graph(id='line'), dcc.Interval(id='tick', interval=1000, n_intervals=0), ]) @app.callback(Output('line','figure'), Input('tick','n_intervals')) def update(n): idx = n % len(years) cur = years[idx] sub = df[df['SSDSE-B-2026']<=cur] fig = go.Figure() for p in df['Prefecture'].unique()[:5]: s = sub[sub['Prefecture']==p] fig.add_trace(go.Scatter(x=s['SSDSE-B-2026'], y=s['A1101'], name=p, mode='lines+markers')) fig.update_layout(title=f'年度 {cur}', xaxis_title='SSDSE-B-2026', yaxis_title='人口') return fig if __name__ == '__main__': app.run(debug=True, port=8050) |
📤 実行例(イメージ・未実測。dash 未導入のため、ブラウザで http://localhost:8050 を開いたときの想定):
💬 dcc.Interval で 1 秒ごとに callback が走り、 グラフが再描画される。 本物のストリームに繋ぐ場合は n_intervals を起点に Kafka/Redis から最新値を取りに行く。 SSDSE のような static データの「擬似ストリーム」として動作確認できる。
🎯 このコードでやること: 全 SSDSE データを「無限ストリーム」と見立て、 reservoir sampling でサイズ 100 のサンプルだけを保持しながら統計を計算。
📥 入力: SSDSE-B-2026 を 1 行ずつストリームで読む想定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import pandas as pd import random df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.dropna(subset=['A1101']).reset_index(drop=True) K = 100 # reservoir size random.seed(0) reservoir = [] for t, row in df.iterrows(): if t < K: reservoir.append(row['A1101']) else: j = random.randint(0, t) if j < K: reservoir[j] = row['A1101'] import numpy as np print(f'全体 N = {len(df)}') print(f'reservoir size = {len(reservoir)}') print(f'全体 平均 = {df["A1101"].mean():.2f}') print(f'reservoir 平均 = {np.mean(reservoir):.2f}') print(f'全体 中央値 = {df["A1101"].median():.2f}') print(f'reservoir 中央値 = {np.median(reservoir):.2f}') |
📤 実行例:
💬 保持しているのは 100 値だけだが、 中央値は 1,619,000 人と全体の 1,620,000 人にほぼ一致する。 一方で平均は 2,499,171 人と全体の 2,690,688 人より約 7% 低く、 東京都(約 1,400 万人)のような少数の大きな値が 100 件に何回入ったかで平均は大きく動く。 seed=0 で固定しているので値は毎回同じだが、 seed を変えると平均の方が揺れやすい。 ストリームが何百万・何億行になっても、 reservoir は K 件しか持たない → スケーラブル。 ヒストグラム描画用のサンプル維持に有効。
🎯 このコードでやること: Streamlit の st.empty() プレースホルダで毎秒書き換える方式。 Dash より少ないコード行数で「動くチャート」を実現。
📥 入力: SSDSE-B-2026 (前処理済 df)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import pandas as pd import streamlit as st import time import matplotlib.pyplot as plt df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.dropna(subset=['A1101']).sort_values(['Prefecture','SSDSE-B-2026']) years = sorted(df['SSDSE-B-2026'].unique()) prefs = df['Prefecture'].unique()[:5] st.title('SSDSE リアルタイム人口推移') chart_slot = st.empty() table_slot = st.empty() for cur in years: sub = df[df['SSDSE-B-2026']<=cur] fig, ax = plt.subplots(figsize=(10, 4)) for p in prefs: s = sub[sub['Prefecture']==p] ax.plot(s['SSDSE-B-2026'], s['A1101'], label=p) ax.legend(); ax.set_title(f'年度: {cur}') chart_slot.pyplot(fig) table_slot.dataframe(sub[sub['SSDSE-B-2026']==cur][['Prefecture','A1101']].head(5)) time.sleep(0.5) plt.close(fig) st.success('再生完了') |
📤 実行例 (ターミナルで streamlit run app.py):
💬 Streamlit は「Python スクリプトを上から下に流す」モデル。 リアクティブ性は弱いが、 教育用途・社内ツールでは圧倒的に作りやすい。 Dash はコンポーネント駆動でスケールしやすく、 Streamlit はプロトタイプに最適 — 使い分け。
🎯 このコードでやること: Bokeh server の ColumnDataSource.stream で差分追加のみ送信、 全データ再送せずに滑らかなストリーミング描画を実現。
📥 入力: SSDSE-B-2026 (前処理済 df, 北海道だけ抽出)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import pandas as pd from bokeh.io import curdoc from bokeh.models import ColumnDataSource from bokeh.plotting import figure df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) sub = df[df['Prefecture']=='北海道'].dropna(subset=['A1101']).sort_values('SSDSE-B-2026').reset_index(drop=True) source = ColumnDataSource(dict(x=[], y=[])) p = figure(title='北海道 人口推移 (streaming)', x_axis_label='SSDSE-B-2026', y_axis_label='人口') p.line('x', 'y', source=source, line_width=3) p.circle('x', 'y', source=source, size=8, color='red') idx = [0] def update(): if idx[0] >= len(sub): return row = sub.iloc[idx[0]] source.stream(dict(x=[row['SSDSE-B-2026']], y=[row['A1101']]), rollover=20) idx[0] += 1 curdoc().add_root(p) curdoc().add_periodic_callback(update, 500) # 500ms ごと |
📤 実行例 (bokeh serve --show app.py):
💬 Bokeh の stream は「差分のみ追加」方式で、 全データ再送がない → ネットワークと CPU が軽い。 rollover で sliding window もコード 1 つで実現できる。 本番ストリーミング可視化の事実上の標準。
$$ \text{転送量}_{\text{stream}} = O(\text{差分点数}) \quad \text{vs} \quad \text{転送量}_{\text{full redraw}} = O(\text{全点数}) $$
このコードでやること:SSDSE-B-2026 の都道府県データを使い、リアルタイム可視化の「1 フレーム」に相当する散布図・ヒストグラム・箱ひげ図を、実データから生成する手順を確認する。ストリーミング処理の前段として、各フレームがどう作られるかを再現する。
📥 入力データ(SSDSE-B-2026 抜粋・最新年スナップショット):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import pandas as pd import matplotlib.pyplot as plt # SSDSE-B-2026 を読み込み(最新年度 2023 のスナップショット) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) latest = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()] print(f'スナップショット: {latest["SSDSE-B-2026"].iloc[0]} 年度, {len(latest)} 都道府県') # フレーム 1: 散布図(総人口 × 65歳以上人口) fig, ax = plt.subplots(figsize=(6,4)) ax.scatter(latest['A1101'], latest['A1303']) ax.set_xlabel('総人口(人)'); ax.set_ylabel('65歳以上人口(人)') ax.set_title('リアルタイムフレーム例: 散布図') plt.savefig('rtv_frame_scatter.png', dpi=110, bbox_inches='tight') print('rtv_frame_scatter.png 生成完了(散布図フレーム)') # フレーム 2: ヒストグラム(人口分布) fig, ax = plt.subplots(figsize=(6,4)) ax.hist(latest['A1101'], bins=15) ax.set_xlabel('総人口(人)'); ax.set_ylabel('都道府県数') ax.set_title('リアルタイムフレーム例: ヒストグラム') plt.savefig('rtv_frame_hist.png', dpi=110, bbox_inches='tight') print('rtv_frame_hist.png 生成完了(ヒストグラムフレーム)') # フレーム 3: 箱ひげ図(地方ブロック別) groups = {'北海道':['北海道'], '東北':['青森県','岩手県','宮城県','秋田県','山形県','福島県'], '関東':['茨城県','栃木県','群馬県','埼玉県','千葉県','東京都','神奈川県']} data = [latest[latest['Prefecture'].isin(v)]['A1101'].values for v in groups.values()] fig, ax = plt.subplots(figsize=(6,4)) ax.boxplot(data, tick_labels=list(groups.keys())) ax.set_ylabel('総人口(人)') ax.set_title('リアルタイムフレーム例: 箱ひげ図') plt.savefig('rtv_frame_box.png', dpi=110, bbox_inches='tight') print('rtv_frame_box.png 生成完了(箱ひげ図フレーム)') for k, v in zip(groups, data): print(f' {k}: {len(v)} 都道府県, 中央値 {pd.Series(v).median():,.0f} 人') |
📤 実行すると次の出力が得られる:
💬 最新年度 2023 の 47 都道府県だけを 1 フレームとして切り出し、 散布図・ヒストグラム・箱ひげ図の 3 枚を作業フォルダに書き出した(上方の「視覚補助 1〜3」と同じ種類の図)。 箱ひげ図の北海道ブロックは 1 道だけなので箱にならず、 東北 6 県の中央値 117 万人に対して関東 7 都県は 626 万人と 5 倍以上の開きがある。本番ダッシュボードでは このコードを 30 秒間隔で実行し、画像を差し替えることでリアルタイム可視化が成立する。Plotly/Bokeh/Streamlit などの WebSocket 対応ライブラリを使えば、画像差し替えではなくブラウザ側で部分更新できるため、よりスムーズな更新体験が得られる。
静的な図と違い、 リアルタイム可視化は「データが流れ込み、 やがて画面から流れ去る」ことが本質です。 下のミニダッシュボードは、 架空のセンサー値ストリーム(乱数によるシミュレーションで、 実在のデータではありません)を生成して流し続けます。 「▶ 開始」を押して、 更新間隔・ウィンドウ幅・平滑化・スパイク注入を切り替えながら、 リアルタイム監視の基本要素を一通り体感してください。 グラフ上をクリック(タッチ)またはドラッグするとアラート閾値(赤い破線)を上下に動かせます。
※ 表示されるセンサー値は平均回帰つきランダムウォーク(乱数)で生成した架空のシミュレーションです。 内部バッファは最大 600 点で頭打ちにしており、 これ自体が「全データを保持しない」ストリーム設計の縮図です。 タブを非表示にすると生成を自動停止し、 戻ると再開します。
バッチ(静的)可視化では、 手元に全データが揃ってから最適な軸・ビン・要約を選べる。 リアルタイム可視化では、 データは 1 点ずつ到着し、 ウィンドウの左端から流れ去って二度と画面に戻らない。 つまり「あとで見返す」ことを前提にできず、 (1) 描画時点で意味が完結する図、 (2) 保持しない過去を要約統計(移動平均・EMA・カウンタ)に畳み込む仕組み、 の 2 つが必須になる。 上のウィジェットで「閾値超過回数」カウンタだけが流れ去った過去を記憶している点に注目してほしい。 これが本文で述べた「O(1) メモリの逐次集計」の最小例である。 折れ線という図法そのものの読み方は 折れ線グラフ を、 時間方向の構造(トレンド・季節性)の扱いは 時系列データ を参照。
このウィジェットは「乱数生成 → 配列 push → canvas 再描画」をブラウザ内で完結させた玩具だが、 本番構成でも各部品は 1 対 1 に対応する。 乱数生成器は センサデータ や 行動ログ などのデータソースに、 配列バッファは Kafka などのメッセージ基盤と時系列 DB に、 setInterval は WebSocket/SSE のプッシュ配信に置き換わる(ストリーム処理基盤の個別ページは未整備のため、 本ページ「🗺 アーキテクチャ全体図」の表を参照)。 ダッシュボード全体の画面設計・KPI 階層化は BI ツール、 閾値の統計的な決め方(3σ・分位点・曜日別ベースライン)とアラート運用は モデルモニタリング が隣接領域。 操作 UI(ブラシ・ズーム・ツールチップ)で過去を掘る方向の拡張は インタラクティブ可視化、 「動いていても誤解させない」誠実な見せ方の原則は データ可視化 で扱っている。
この用語を使うときに初学者が踏みやすい失敗パターン。 1 度経験してしまえば次から避けられますが、 先に知っておくに越したことはありません。
リアルタイム可視化 を使うときに初学者が踏みやすい失敗パターン。 1 度経験してしまえば次から避けられますが、 先に知っておくに越したことはありません。
リアルタイム可視化は「動くから良い」のではなく、「正しく動くから良い」。次の 12 個の落とし穴は実務で頻発する代表例である。
🎯 このコードでやること:47 都道府県の総人口を年度の昇順に並べ、 生値(W = 1)と W 年移動平均の前年差が正になる県の数を 2020・2021 年度で数える。 窓幅によって「増加」と表示される県がどれだけ変わるかを見る。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) pop = (df.pivot(index='SSDSE-B-2026', columns='Prefecture', values='A1101') .sort_index()) # 行 = 年度(昇順), 列 = 47 都道府県 raw = pop.diff() # 生値の前年差 for W in [1, 2, 3, 5]: sm = pop.rolling(W).mean().diff() # W 年移動平均の前年差 n_up_2020 = int((sm.loc[2020] > 0).sum()) n_up_2021 = int((sm.loc[2021] > 0).sum()) print(f'W={W}: 前年より「増加」と表示される県数 2020 年度 {n_up_2020:2d} 2021 年度 {n_up_2021:2d}') print('東京都 生値の前年差 2021 =', f"{raw.loc[2021, '東京都']:+,.0f}") for W in [2, 3, 5]: print(f'東京都 W={W} 平均の前年差 2021 =', f"{pop.rolling(W).mean().diff().loc[2021, '東京都']:+,.0f}") print('2021 年度に生値で増えた県:', list(raw.columns[raw.loc[2021] > 0])) print('W=5 で 2021 年度も「増加」:', list(raw.columns[pop.rolling(5).mean().diff().loc[2021] > 0])) |
💬 生値で 2021 年度に前年より増えたのは沖縄県だけ(1 県)なのに、 W = 3 の窓平均では 5 県、 W = 5 では東京都・神奈川県・愛知県など 7 県が「増加」と表示される。 東京都は生値で −37,594 人でも、 W = 3 の窓平均の前年差は +41,000 人、 W = 5 では +72,800 人になる。 窓平均の画面だけを見ていると、 人口が減り始めた年を「増加」と読み違える。 平滑値と並べて生値の差分も表示するのが安全である。
リアルタイム可視化は「動く魔法の図」ではない。実体は 静止画フレームの連続である。だから、まず静止画 1 枚を完璧に作れることが前提となり、その上に「同期」「履歴」「アラート」「アクセス管理」などの運用要件が積み重なる。本ページでは散布図・ヒストグラム・箱ひげ図という 3 枚の代表静止画を題材に、それぞれが何を映し、リアルタイム文脈でなぜ重要かを掘り下げてきた。
この 3 枚構成を SSDSE-B-2026 のような実データで一度通しで作っておけば、後で本番ストリーミング環境(Kafka・Kinesis・WebSocket 等)に移植する際の設計品質が大きく向上する。リアルタイム可視化を学ぶ最短ルートは、まず静止画を上達させること、これが本ページの中核メッセージである。
本番システムでは、 ライブラリ単体ではなくスタック全体を意識した設計が必要。
| 層 | 役割 | 代表ツール | SSDSE 演習版 |
|---|---|---|---|
| Source | データ発生 | IoT, API, log | SSDSE CSV reader |
| Transport | 配送 | Kafka, MQTT | queue.Queue |
| Process | 集約・平滑 | Flink, Spark Streaming | pandas rolling |
| Store | サンプル保持 | InfluxDB, Redis | reservoir list |
| Visualize | 描画 | Dash, Streamlit, Bokeh | matplotlib anim |
| UI/UX | ダッシュボード | Grafana, Tableau | HTML テンプレ |
$$ L_{\text{total}} = L_{\text{source}} + L_{\text{transport}} + L_{\text{process}} + L_{\text{render}} $$
「リアルタイム可視化」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。
SSDSE 系の静的な公的統計だけでなく、 流れてくる時系列を Plotly Dash や Streamlit で即時描画 → 閾値判定 → アラートまで一気通貫させると価値が出る。
「リアルタイム可視化」を実際の課題に当てはめるとき、 状況別に何を選ぶかを 3 段階で判定する。
SSDSE-B-2026 は年次データなのでリアルタイムは不要。 リアルタイム化が意味を持つのは PoS 売上・気象センサ・SNS 投稿数など、 秒~分単位で意思決定したいデータ。
本ページはここまで、 レイテンシ・ウィンドウ集計・ライブラリ・運用設計を扱ってきた。 この追補では視点を変え、 「画面を監視し続ける」という行為そのものが持つ統計的性質を掘り下げる。 平滑化が真の変化を隠す現象と、 逐次監視が偽アラートを必ず生む現象は、 どちらも実装ではなく確率の問題であり、 どんなに高速な描画基盤を使っても消えない。
ページ上部の 🎮 ウィジェットで「移動平均 (W=5)」を ON にすると線が滑らかになるが、 スパイク注入への反応も鈍る。 これはデモ上の演出ではなく、 実データでも同じことが起きる。 SSDSE-B-2026 の東京都 A1101(総人口)2012–2023 年を「1 年 = 1 イベント」の擬似ストリームとして流し、 生値・W=3 スライディング平均・EMA(α=0.5、 W=3 相当)を並べてみる。
| 年 | 生値(人) | 前年差 | W=3 平均 | EMA (α=0.5) |
|---|---|---|---|---|
| 2019 | 14,007,000 | +120,000 | 13,887,333 | 13,888,188 |
| 2020 | 14,047,594 | +40,594 | 13,980,531 | 13,967,891 |
| 2021 | 14,010,000 | −37,594 | 14,021,531 | 13,988,946 |
| 2022 | 14,038,000 | +28,000 | 14,031,865 | 14,013,473 |
| 2023 | 14,086,000 | +48,000 | 14,044,667 | 14,049,736 |
生値の前年差は 2021 年に観測期間 12 年で初めて負(−37,594 人)となり、 COVID 期の東京都の人口減少をその年のうちに示す。 ところが W=3 平均は 13,980,531 → 14,021,531 → 14,031,865 と、 EMA も 13,967,891 → 13,988,946 → 14,013,473 と一貫して増加し続け、 この転換を一度も「減少」として表示しない。 平滑化はノイズ由来の誤報を減らす保険だが、 保険料として「真の変化の検知遅れ(ときには完全な隠蔽)」を支払っている。 リアルタイムダッシュボードでは「生値の差分」と「平滑値」を両方表示するのが正解で、 どちらか一方に絞るのは情報の破棄である。
閾値超過アラートは「1 回の判定」としては妥当でも、 リアルタイム可視化では同じ判定を何百回も繰り返す。 これは検定の多重性の問題と同型で、 1 回あたりの誤報率を $\alpha$、 判定回数を $N$ とすると、 少なくとも 1 回誤報する確率は次式で膨張する。
つまり「データが完全に正常でも、 1 時間監視すれば 3σ アラートは高確率で 1 回は誤発火する」。 これが本文で触れたアラート疲労の数理的な正体である。 対策は閾値を下げることではなく判定の構造を変えること:(1)連続 k 回超過で初めて発報する持続性ルール(単発ノイズの誤報率は $\alpha^k$ に急減)、(2)超過量を累積する CUSUM や平滑値に管理限界を引く EWMA 管理図の発想を借りる、(3)「1 画面 = 1 時間に何回誤報してよいか」という時間あたり誤報予算から逆算して $\alpha$ を決める。 🎮 ウィジェットで閾値線をドラッグし、 正常運転のまま放置したときの「閾値超過回数」の増え方を見ると、 この式を体感できる。
本文の EMA は「忘却しながら」平均を追う道具だった。 一方、 管理限界(μ ± 3σ)を引くには忘却しない平均と分散が要る。 全データを保持せずにこれを更新するのが Welford のオンラインアルゴリズムで、 1 件到着するたびに次の 3 行を実行するだけでよい。
実データで検証:東京都 A1101 の 12 年分(2012–2023)を 1 件ずつ Welford 更新で流すと、 平均 13,745,405.42 人・不偏標準偏差 314,571.94 人となり、 全データ一括計算(numpy)と完全に一致する。 素朴な「二乗和 − 平均の二乗」方式は桁の大きい値(千万人オーダー)で桁落ちを起こしやすいが、 Welford 法は数値的にも安定している。 EMA(忘却あり・トレンド追従)と Welford(忘却なし・ベースライン推定)を併走させ、 「Welford のベースライン ± 3σ を管理限界、 EMA を現在値」として比較するのが、 ストリーム異常検知の最小構成である。 さらに先へ進むなら、 分布自体が時間とともに変わるコンセプトドリフト下でのベースライン再学習が次のテーマになる。