散布図 関連の補強キーワード。 クリックで該当箇所へ:
「scatter plot」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「scatter plot」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。
これらのキーワードは「scatter plot の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。
🍰 まずはやさしく
2つのデータの関係を点で見せる図です。
データにどんな傾向があるか知るために使います。
勉強時間とテストの点数の関係などを調べます。
図の見方や使い方の基本を学びましょう。
散布図(scatter plot)は、 2変数の関係を視覚化する最基本ツール。 横軸 x、 縦軸 y にデータ点をプロットします。 統計分析を始めるときに 必ず最初に描くべき図 です。
判別ポイント:
必須習慣:相関係数を計算する前に、 必ず散布図を描く。 同じ r = 0.5 でも、 きれいな直線関係と、 外れ値による偽の相関は全く違います。 Anscombe の4組(r が同じでも散布図が全然違う有名な例)を見ると痛感します。
応用:(i) 単回帰の直線を重ねる、 (ii) hex bin で密度を可視化(点が多すぎる場合)、 (iii) seaborn の jointplot で周辺分布も同時に表示。
散布図(scatter plot)は、 2つの量的変数の関係を点で可視化する基本グラフ。 各点が1つの観測単位(人、 県、 商品など)を表します。
47都道府県の食料費(横軸)と教育費(縦軸)の関係。 点が右上がりに広がっていて、 食料費が高い県は教育費も高い傾向(r = 0.728)。
💡 散布図は相関分析・回帰分析の前段階として必ず描くべきグラフ。 「相関係数 r が大きい」と言う前に、 必ず散布図で関係の形を確認しましょう。
散布図の形から、 2変数の関係を診断できます:
| パターン | 特徴 | 適切な分析 |
|---|---|---|
| 強い正の相関 | 右上がりにまとまる | Pearson 相関、 線形回帰 |
| 弱い正の相関 | 右上がりだが散らばる | Pearson 相関 + 信頼区間 |
| 負の相関 | 右下がりにまとまる | 同上 |
| 無相関 | 円形に分布 | 独立性検定 |
| 非線形(U字、 周期) | 曲線パターン | 非線形回帰、 Spearman、 相関比 |
| 外れ値 | 少数の極端な点 | ロバスト回帰、 除去 or 別分析 |
点のサイズや色で3つ目の変数を表現するのがバブルチャート。 SSDSE データで食料費×教育費の関係に、 「総人口」を点のサイズと色で重ねます。
人口が多い大都市府県(東京、 神奈川など)は大きな円で表示され、 食料費・教育費も比較的高い位置に。 3次元の情報を2D上に圧縮できる強力な可視化。
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 37 38 39 | # ── この抜粋で使うデータを用意します ── import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].copy() # 2023 年の 47 都道府県 # 本文で使う短い呼び名は、実在する列の別名として作る df['総人口'] = df['A1101'] df['食料費'] = df['L322101'] df['教育費'] = df['L322108'] _BLOCKS = {'北海道': ['北海道'], '東北': ['青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県'], '関東': ['茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県'], '中部': ['新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県'], '近畿': ['三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県'], '中国': ['鳥取県', '島根県', '岡山県', '広島県', '山口県'], '四国': ['徳島県', '香川県', '愛媛県', '高知県'], '九州・沖縄': ['福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県']} _R = {p: g for g, ps in _BLOCKS.items() for p in ps} df['地域'] = df['Prefecture'].map(_R) x = df['食料費'].values y = df['教育費'].values sizes = df['総人口'].values / 1e4 # 点のサイズ用(万人) color_var = df['A1303'].values / 1e4 # 色の濃さ用(65歳以上人口・万人) plt.scatter(x, y, s=sizes * 0.1, # 点のサイズ c=color_var, # 点の色(連続値) cmap='viridis', alpha=0.6, edgecolors='white') plt.colorbar(label='65歳以上人口(万人)') # seaborn でより簡潔に import seaborn as sns sns.scatterplot(x='食料費', y='教育費', size='総人口', hue='地域', data=df) |
データ点が数千・数万になると、 通常の散布図では点が重なって何も見えません。 そんなときはhexbin(六角形ビン):
2D空間を六角形のビンで区切り、 各ビン内のデータ数を色で表現。 大量データの密度パターンが見えます。
alpha=0.1 程度にして重なりを濃淡で表現plt.hist2d(x, y)sns.kdeplot(x=x, y=y) で密度の等高線🍰 まずはやさしく
データの関係を視覚化(図にすること)する道具です。
分析を始めるときにまず描くべき図です。
都道府県の人口と出生数の関係などを調べます。
定義から注意点までを順番に解説します。
論文中に 「散布図」として登場する用語。
散布図 とは:2変数を x軸・y軸にプロットして関係を視覚化。相関や非線形パターン、外れ値の確認に必須。
本ページでは「scatter plot」を扱う。 統計データ分析コンペティション (2026) の教材で、 SSDSE-B-2026 (47 都道府県 × 複数年 × 100 超列) の実データを使った再現可能な学習を目指す。
「scatter plot」は統計・データサイエンスの体系における重要概念のひとつ。 本ページは「定義・直感・数式・実装・落とし穴・関連手法」の 6 視点で構成され、 各視点は独立して読めるが順序通り読むと体系的な理解が得られる。
🍰 まずはやさしく
散布図をさらに便利にした応用技です。
より多くの情報を一度に読み取るために使います。
地域ごとに点の色を変えてグループを分けます。
色の使い分けや曲線の描き方を学びましょう。
変数が3つ以上ある時、 すべてのペアの散布図を一気に表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 | # ── この抜粋で使うデータを用意します ── import pandas as pd import seaborn as sns df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].copy() # 2023 年の 47 都道府県 # 本文で使う短い呼び名は、実在する列の別名として作る df['食料費'] = df['L322101'] df['教育費'] = df['L322108'] df['住居費'] = df['L322102'] df['光熱費'] = df['L322103'] # 光熱・水道費(二人以上の世帯) sns.pairplot(df[['食料費', '教育費', '住居費', '光熱費']], kind='reg') |
地域・性別・カテゴリで色分けすると、 群間差が見える:
非線形な関係を平滑化した曲線で表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # ── この抜粋で使うデータを用意します ── # seaborn の lowess=True はブラウザ版では statsmodels を見つけられないので、 # 平滑化は statsmodels の lowess で直接計算して重ねる。 import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from statsmodels.nonparametric.smoothers_lowess import lowess df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].copy() # 2023 年の 47 都道府県 df['x'] = df['L322101'].astype(float) # 食料費 df['y'] = df['L322108'].astype(float) # 教育費 x, y = df['x'].values, df['y'].values sns.scatterplot(x='x', y='y', data=df) smoothed = lowess(y, x, frac=0.3) # (x, 平滑後 y) の 2 列 plt.plot(smoothed[:, 0], smoothed[:, 1], color='crimson', label='LOWESS') plt.legend() print('平滑化後の先頭 3 点:', smoothed[:3].round(1).tolist()) |
散布図 + ヒストグラム + 統計量を1枚に:
機械学習の前段階で、 各特徴量ペアの関係を散布図行列で確認。 「強い相関がある」「外れ値がある」「非線形だ」などを発見。
k-means や DBSCAN の結果を、 色分け散布図で確認。 2次元または PCA で2次元に圧縮してから描画:
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 import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 2023 年の 47 都道府県 # 家計の 4 費目を標準化して 4 次元の X にする X = StandardScaler().fit_transform( df[['L322101', 'L322102', 'L322103', 'L322108']].astype(float)) # 高次元データを2次元に pca = PCA(n_components=2) X_2d = pca.fit_transform(X) # クラスタリング km = KMeans(n_clusters=3, n_init=10, random_state=0) labels = km.fit_predict(X) plt.scatter(X_2d[:, 0], X_2d[:, 1], c=labels, cmap='viridis') print('各クラスタの県数:', pd.Series(labels).value_counts().sort_index().tolist()) |
回帰モデルの残差 vs 予測値を散布図にする。 ランダムなら適合良、 パターンがあればモデル改善の余地:
残差が正規分布に従うか確認する散布図:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # ── この抜粋で使う残差を用意します ── import pandas as pd import matplotlib.pyplot as plt from scipy import stats df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 2023 年の 47 都道府県 x = df['L322101'].astype(float).values # 食料費 y = df['L322108'].astype(float).values # 教育費 slope, intercept, *_ = stats.linregress(x, y) residuals = y - (slope * x + intercept) # 単回帰の残差 stats.probplot(residuals, dist='norm', plot=plt) # 点が直線上 → 正規分布 print('残差の平均:', round(residuals.mean(), 6)) |
機械学習モデルの評価でも散布図的な可視化が頻出。
💡 散布図1枚から、 統計分析の方針(線形か非線形か、 ロバスト推定が必要か、 外れ値除去が必要か)が決まります。 数値だけ見るより必ず可視化を見る。
散布図は「2 変数の関係を点でプロットする」可視化の王道。 1 点 = 1 観測(県・人・時刻)。 SSDSE-B-2026 で A1101 vs A1303(総人口 vs 高齢人口)を描くと、 ほぼ y=0.25x の直線上に並ぶが、 北海道のように高齢化率が突出して高い県は線の上側にずれて配置される。
散布図 は「可視化」カテゴリの中核概念。 初めて触れる読者は、 まずこの「🎨 直感」セクションだけ通読し、 必要になった時点で「📐 数式」「🐍 Python」「⚠️ 落とし穴」へ戻る読み方が定着しやすいです。
🍰 まずはやさしく
散布図の仕組みを数式で表したものです。
正確なルールを共通の言葉で伝えるために使います。
点と点の離れ具合を計算して関係性を出します。
数式の記号が何を意味するかを確認しましょう。
やさしい説明で掴んだ感覚を、ここで 散布図 の定義式に対応づけます。下の式は左辺 r が何で決まるかを右辺で書き下したもので、x̄(平均)、Σ(合計)、分数(割り算) が現れます。それぞれの記号が何の量を指すのかは、次の「🔬 数式を言葉で読み解く」で 1 つずつ確かめてください。
上の数式を眺めるだけでは身につかないので、 各記号がどんな役割を担っているかを言葉で押さえます。 「数式を音読する習慣」がつくと、 論文や教科書を読むスピードが体感で 2 倍ほど上がります。
47 都道府県データで散布図を体系的に作成し、 ジッタリング・色分け・回帰線・マージナル分布まで含む実例。
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 | import pandas as pd import matplotlib.pyplot as plt import seaborn as sns df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 基本散布図 fig, axes = plt.subplots(2, 2, figsize=(12, 10)) ax = axes[0, 0] ax.scatter(df['C5401'], df['L3221'], alpha=0.7, s=60) ax.set_xlabel('標準地価(円/㎡)'); ax.set_ylabel('消費支出(円)') ax.set_title('① 基本散布図') # 第3変数で色分け ax = axes[0, 1] sc = ax.scatter(df['C5401'], df['L3221'], c=df['A1101'], cmap='viridis', s=60, alpha=0.8) plt.colorbar(sc, ax=ax, label='総人口') ax.set_title('② 総人口で色分け') # 回帰線つき ax = axes[1, 0] sns.regplot(x='C5401', y='L3221', data=df, ax=ax, scatter_kws={'alpha':0.6}, line_kws={'color':'red'}) ax.set_title('③ 回帰線(95% CI 帯)') # 六角ビン(大きいデータ向け) ax = axes[1, 1] hb = ax.hexbin(df['C5401'], df['L3221'], gridsize=10, cmap='Blues') plt.colorbar(hb, ax=ax, label='密度') ax.set_title('④ 六角ビン') plt.tight_layout(); plt.savefig('scatter_4panel.png', dpi=110) |
| 項目 | 値 | 参考 | 解釈 |
|---|---|---|---|
| 基本 | 観測 | 47点 | 1点=1都道府県 |
| 傾向 | 相関係数 | r ≈ 0.31 | 地価高い県ほど消費支出も高い(弱い正相関) |
| 外れ | 東京 | 高地価・高消費 | 右上に位置 |
| 外れ | 埼玉・三重 | 低〜中地価・高消費 | 上部 |
👉 値は SSDSE-B-2026 の典型値。 同じ手順で他都道府県・他変数にも適用可能。
数式だけでは「実感」が湧きにくいので、 実データ data/raw/SSDSE-B-2026.csv(47 都道府県 × 16 年)で 1 度手計算してみると理解が定着します。
SSDSE-B-2026 (2023) で A1101 vs A1303 の Pearson 相関は r ≈ 0.991(非常に強い正相関、 人口の多い県ほど高齢人口も多いのは自明)。 ところが「高齢化率=A1303/A1101」を縦軸にすると相関は逆転し、 北海道 33.0%、 秋田県 39.1% などが上方に飛ぶ。 散布図は「軸の取り方で物語が変わる」典型例。
| 都道府県 | A1101 総人口 | A1303 65 歳以上 | L3221 消費支出 |
|---|---|---|---|
| 東京都 | 14,086,000 | 3,205,000 | 341,320 |
| 神奈川県 | 9,229,000 | 2,390,000 | 306,565 |
| 大阪府 | 8,763,000 | 2,424,000 | 271,246 |
| 愛知県 | 7,477,000 | 1,923,000 | 300,221 |
| 埼玉県 | 7,331,000 | 2,012,000 | 344,092 |
| 千葉県 | 6,257,000 | 1,756,000 | 306,943 |
上記は SSDSE-B-2026 (2023) からの抜粋。 手計算で確認した値が、 後述の Python 実装で得る値と一致することを確認すると、 「数式とコードの対応関係」がクリアに見えるようになります。
合成 2 変数で r を計算し、 散布の特徴を確認する。
1 2 3 4 | import numpy as np x = np.array([2,4,7,5,3]) y = np.array([3,7,12,9,4]) print(f"r: {np.corrcoef(x, y)[0,1]:.3f}") |
💬 手計算 (Step 2) r=0.990 と Python 出力が完全一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) plt.scatter(x, y, s=60, alpha=0.6, color='steelblue', edgecolors='white') plt.xlabel('X変数') plt.ylabel('Y変数') plt.title('散布図のタイトル') plt.grid(alpha=0.3) plt.show() # 回帰直線を追加 import numpy as np z = np.polyfit(x, y, 1) plt.plot(x, z[0]*x + z[1], 'r-', label=f'y={z[0]:.2f}x+{z[1]:.2f}') plt.legend() |
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 37 38 39 40 41 42 43 | # ── この抜粋で使うデータを用意します(SSDSE-B の 47 都道府県・最新年度)── # 英字の項目コード(C5401 など)も後のブロックで使うので skiprows=[1] で読み、 # 本文で使う短い呼び名は、実在する列の別名として足す。 import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()].copy() # 最新年度の 47 行 df['総人口'] = df['A1101'] df['食料費'] = df['L322101'] # 食料費(二人以上の世帯) df['教育費'] = df['L322108'] # 教育費(二人以上の世帯) df['住居費'] = df['L322102'] # 住居費(二人以上の世帯) # 「地方」は SSDSE に無い列なので、都道府県名から 8 地方区分を作る _BLOCKS = { '北海道': ['北海道'], '東北': ['青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県'], '関東': ['茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県'], '中部': ['新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県'], '近畿': ['三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県'], '中国': ['鳥取県', '島根県', '岡山県', '広島県', '山口県'], '四国': ['徳島県', '香川県', '愛媛県', '高知県'], '九州・沖縄': ['福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県'], } _R = {p: g for g, ps in _BLOCKS.items() for p in ps} df['地域'] = df['Prefecture'].map(_R) import seaborn as sns # 基本 sns.scatterplot(x='食料費', y='教育費', data=df) # 色・サイズ分け sns.scatterplot(x='食料費', y='教育費', hue='地域', size='総人口', data=df) # 回帰直線 + 信頼区間 sns.regplot(x='食料費', y='教育費', data=df) # 散布図行列 sns.pairplot(df[['食料費', '教育費', '住居費']], diag_kind='kde') # joint plot sns.jointplot(x='食料費', y='教育費', data=df, kind='reg') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import pandas as pd import plotly.express as px # このページは英字の項目コード(C5401 など)で通しているので、同じ流儀で読む df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()].copy() for _c in ('A1101', 'L3221', 'L322108', 'C5401'): df[_c] = pd.to_numeric(df[_c], errors='coerce') fig = px.scatter(df, x='L3221', y='L322108', # 食料費 と 教育費 size='A1101', # 総人口 hover_data=['Prefecture'], trendline='ols') fig.show() # マウスを載せると都道府県名が出る |
軸の範囲を狭めると関係が誇張、 広げると弱く見える。 必ずデータ全体を含む範囲で描画。
大量データでは alpha、 jitter、 hexbin で対処。
赤×緑の組合せは色覚多様性で識別できない人も。 viridis、 plasma などのカラーマップを推奨。
2変数の関係に見えるものが、 第3変数の影響かもしれません。 シンプソンのパラドックスに注意。
散布図で関係が見えても、 因果ではない。 「アイスクリーム売上 vs 水難事故」の典型例(共通因:気温)。
身長 vs 体重のような異なる単位の場合、 標準化後に比較するなどの工夫が必要なことも。
散布図は170年以上の歴史を持ち、 今もデータサイエンスの中核可視化として使われ続けています。
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 | from sklearn.linear_model import LinearRegression from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import Pipeline import numpy as np # 散布図に非線形回帰曲線を重ねる(多項式) # このブロックは英字の項目コードを使うので、ここで読み直す import pandas as pd import matplotlib.pyplot as plt d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = d[d['SSDSE-B-2026'] == d['SSDSE-B-2026'].max()] X = d[['C5401']].astype(float).values # 住宅地の標準価格 y = d['L3221'].astype(float).values # 消費支出 fig, ax = plt.subplots(figsize=(8, 5)) ax.scatter(X, y, alpha=0.6, s=50) x_grid = np.linspace(X.min(), X.max(), 200).reshape(-1, 1) for degree, color in [(1, 'red'), (2, 'green'), (3, 'blue')]: model = Pipeline([('poly', PolynomialFeatures(degree)), ('lr', LinearRegression())]) model.fit(X, y) ax.plot(x_grid, model.predict(x_grid), color=color, label=f'degree={degree}') ax.legend(); ax.set_xlabel('標準地価'); ax.set_ylabel('消費支出') plt.savefig('scatter_poly.png', dpi=110) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from scipy import stats from scipy.stats import gaussian_kde import numpy as np # 2D カーネル密度推定で散布図を補強 x = df['C5401'].values y = df['L3221'].values kde = gaussian_kde(np.vstack([x, y])) xx, yy = np.mgrid[x.min():x.max():100j, y.min():y.max():100j] positions = np.vstack([xx.ravel(), yy.ravel()]) zz = kde(positions).reshape(xx.shape) fig, ax = plt.subplots(figsize=(8, 6)) ax.contourf(xx, yy, zz, levels=10, cmap='Blues', alpha=0.7) ax.scatter(x, y, c='red', s=20, alpha=0.7, edgecolor='white') # 相関と回帰 r, p = stats.pearsonr(x, y) slope, intercept, _, _, _ = stats.linregress(x, y) xs = np.array([x.min(), x.max()]) ax.plot(xs, intercept + slope * xs, 'k--', label=f'r={r:.2f}, p={p:.3f}') ax.legend(); plt.savefig('scatter_kde.png', dpi=110) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # LOWESS の bandwidth 最適化(散布図に滑らかな曲線を重ねる) import statsmodels.api as sm import optuna def objective(trial): frac = trial.suggest_float('frac', 0.1, 0.9) smoothed = sm.nonparametric.lowess(y, x, frac=frac, return_sorted=False) return ((y - smoothed)**2).mean() # MSE 最小化 study = optuna.create_study(direction='minimize') study.optimize(objective, n_trials=30, show_progress_bar=False) print('Best frac:', study.best_params) # 最適 frac で散布図にプロット best_frac = study.best_params['frac'] smoothed = sm.nonparametric.lowess(y, x, frac=best_frac) fig, ax = plt.subplots(figsize=(8, 5)) ax.scatter(x, y, alpha=0.6) ax.plot(smoothed[:, 0], smoothed[:, 1], 'r-', label=f'LOWESS frac={best_frac:.2f}') ax.legend(); plt.savefig('scatter_lowess.png', dpi=110) |
| ライブラリ / 関数 | 用途 |
|---|---|
matplotlib.pyplot.scatter | 最も柔軟・低レベル |
seaborn.scatterplot | 回帰線・色分けが簡単 |
seaborn.regplot | 回帰線+CI 帯 |
seaborn.jointplot | マージナル分布つき散布図 |
plotly.express.scatter | インタラクティブな散布図 |
matplotlib.hexbin | 大量データの密度可視化 |
公的統計(SSDSE-B-2026)を題材に、 最小限の Python コードで 散布図 を動作させます。 まずはこのまま実行してみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # 散布図 を 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()) import matplotlib.pyplot as plt fig, axes = plt.subplots(1,2, figsize=(12,5)) axes[0].scatter(df['A1101'], df['A1303'], alpha=0.7) axes[0].set_xlabel('A1101 総人口'); axes[0].set_ylabel('A1303 65歳以上') axes[0].set_title('総人口 vs 高齢人口') axes[1].scatter(df['A1101'], df['A1303']/df['A1101'], alpha=0.7, color='red') axes[1].set_xlabel('A1101 総人口'); axes[1].set_ylabel('高齢化率') axes[1].set_title('総人口 vs 高齢化率') plt.tight_layout(); plt.savefig('scatter_demo.png', dpi=100) print('r1:', df[['A1101','A1303']].corr().iloc[0,1]) |
▶ 実行 を押せばこのページの中でそのまま動きます(ライブラリもデータも同梱済みで、 準備は要りません)。 手元の Python に移して動かすときは pip install matplotlib numpy pandas scikit-learn scipy seaborn statsmodels が必要です。 読んでいるデータは data/raw/SSDSE-B-2026.csv。 日本語を含むので encoding='cp932' の指定を落とさないでください。
散布図は「点を打つだけ」に見えて、 実際には 点が重なる・軸の桁が違う・3 つ目の変数を載せたいといった問題に すぐぶつかる。 ここでは SSDSE-B-2026 を題材に、 現場で使う 6 つの手当てを 実際に走るコードと実測値で確認する。 いずれも図を「きれいにする」ためではなく、 データが持っている情報を消さずに見せるための技術である。
図の軸ラベルは日本語のままで問題ない。 このサイトの実行環境には IPAex ゴシックが同梱されており、 matplotlib を import した時点で 自動的に登録される。 手元の環境で日本語が □ に化ける場合は、 日本語フォントが 入っていないので matplotlib.rcParams['font.family'] = 'IPAexGothic' 等を指定してほしい。
x と y だけでは 2 つの情報しか運べない。 マーカーの形をカテゴリに、 面積を連続量に割り当てると、 1 枚の散布図で 4 変数を同時に見せられる。 注意点は 2 つ。 面積は半径ではなく面積で比例させること (matplotlib の s は面積なのでそのまま渡してよい)、 そして 形は 3〜4 種類までにすること(それ以上は読者が凡例を覚えられない)。
📥 入力: SSDSE-B-2026 の 2023 年 47 都道府県。 x=総人口、 y=出生数、 形=東日本/西日本、 面積=高齢化率。
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 import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023].copy() d['aging'] = d['A1303'] / d['A1101'] * 100 # 高齢化率 (%) east = ['北海道','青森県','岩手県','宮城県','秋田県','山形県','福島県','茨城県','栃木県', '群馬県','埼玉県','千葉県','東京都','神奈川県','新潟県','富山県','石川県','福井県', '山梨県','長野県','岐阜県','静岡県','愛知県'] d['area'] = d['Prefecture'].apply(lambda p: '東日本' if p in east else '西日本') fig, ax = plt.subplots(figsize=(7, 5)) for area, mk in [('東日本', 'o'), ('西日本', '^')]: g = d[d['area'] == area] ax.scatter(g['A1101'] / 1e4, g['A4101'] / 1e3, marker=mk, s=(g['aging'] - 20) * 12, # 面積で高齢化率を表す alpha=0.7, edgecolor='white', label=f'{area} (n={len(g)})') ax.set_xlabel('総人口(万人)'); ax.set_ylabel('出生数(千人)') ax.set_title('マーカーの形=地域、 面積=高齢化率') ax.legend(); plt.tight_layout(); plt.savefig('scatter_marker.png', dpi=120) print(f'東日本 {len(d[d["area"]=="東日本"])} 県 / 西日本 {len(d[d["area"]=="西日本"])} 県') print(f'高齢化率の範囲: {d["aging"].min():.1f}% 〜 {d["aging"].max():.1f}%') print(f'マーカー面積の比 = {(d["aging"].max()-20) / (d["aging"].min()-20):.1f} 倍') print(f'東日本の平均高齢化率 {d[d["area"]=="東日本"]["aging"].mean():.2f}% / ' f'西日本 {d[d["area"]=="西日本"]["aging"].mean():.2f}%') |
📤 実行するとこの出力が得られる:
💬 結果の読み方: 形が地域、 面積が高齢化率を表す。 高齢化率は 22.8%〜39.1% の範囲だが、 20% を基準に引き算してから面積に割り当てているため、 面積比は 6.9 倍に拡大されて 違いが見えるようになった。 生の値をそのまま面積にすると 39.1/22.8 = 1.7 倍にしかならず、 目では区別できない。 一方これは誇張でもある ── 面積の比が実際の値の比と 一致しなくなるので、 凡例か本文で「基準を引いてある」と必ず断ること。 なお東日本 31.18% / 西日本 31.97% と平均はほぼ同じで、 形の違いには意味が乏しい。 差が無い変数を形に割り当てても情報は増えないという例でもある。
合計特殊出生率のように小数第 1 位までしか動かない変数を横軸に取ると、 多くの県がまったく同じ x 座標に落ちて重なってしまう。 わざと微小な乱数を足して 位置をずらす手法がジッタリングである。 ずらし幅は 刻み幅の 2 割程度に留めるのが目安で、 これを超えると隣の目盛りと 混ざって値を読み違える。
📥 入力: SSDSE-B-2026 の 2023 年 47 都道府県。 合計特殊出生率 (A4103) を小数第 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 25 26 27 28 29 30 31 32 33 | import pandas as pd import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023].copy() # 合計特殊出生率を小数第 1 位に丸めると、 値が 8 通りしか無い離散変数になる d['tfr_round'] = d['A4103'].round(1) n_unique = d['tfr_round'].nunique() dup = len(d) - n_unique print(f'丸めた出生率の異なる値: {n_unique} 通り(47 県中 {dup} 県が他県と同じ値)') print('最も混み合う値:', d['tfr_round'].value_counts().idxmax(), '→', int(d['tfr_round'].value_counts().max()), '県が完全に重なる') rng = np.random.default_rng(20260614) # 揺らし方を固定して毎回同じ図にする jitter = rng.uniform(-0.02, 0.02, len(d)) fig, axes = plt.subplots(1, 2, figsize=(11, 4.5), sharey=True) axes[0].scatter(d['tfr_round'], d['A1303'] / d['A1101'] * 100, alpha=0.8) axes[0].set_title('ジッターなし(点が重なる)') axes[1].scatter(d['tfr_round'] + jitter, d['A1303'] / d['A1101'] * 100, alpha=0.8) axes[1].set_title('ジッターあり(±0.02)') for ax in axes: ax.set_xlabel('合計特殊出生率(小数第 1 位に丸め)') axes[0].set_ylabel('高齢化率(%)') plt.tight_layout(); plt.savefig('scatter_jitter.png', dpi=120) # 揺らしても値の意味が変わっていないことを確認する print(f'ジッター幅 ±0.02 は刻み幅 0.1 の {0.02/0.1*100:.0f}% → 隣の目盛りとは混ざらない') print(f'ジッター後の平均 {(d["tfr_round"]+jitter).mean():.4f} / 元の平均 {d["tfr_round"].mean():.4f}') |
📤 実行するとこの出力が得られる:
💬 結果の読み方: 丸めた出生率は 7 通りの値しか取らず、 47 県中 40 県が他県と同じ値になる。 とくに 1.3 には 13 県が完全に重なるので、 ジッターなしの左図では「点が 1 個」にしか見えない。 ±0.02 のジッターを足した右図では 13 県が横に散らばり、 縦軸(高齢化率)方向のばらつきも 読み取れるようになる。 ずらし幅 0.02 は刻み幅 0.1 の 20% なので、 隣の目盛り(1.2 や 1.4)と混ざることはない。 平均も 1.2915 → 1.2931 とほぼ変わらず、 見た目を整えるだけで統計量を壊していないことが確認できる。 ジッターは「見せるため」の加工であり、 加工後のデータで計算してはいけない。
散布図に直線を 1 本引くだけで「関係がある」と見えてしまう。 引くのであれば 傾きの不確かさも同時に示すべきで、 それが 95% 信頼帯である。 帯はデータの中心で最も細く、 端に行くほど太くなる砂時計型になる。 これは「端のほうは根拠が薄い」ことを図が自分で告白している状態にほかならない。
📥 入力: SSDSE-B-2026 の 2023 年 47 都道府県。 x=総人口(万人)、 y=出生数(千人)。
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 | import pandas as pd import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from scipy import stats df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023] x = d['A1101'].values / 1e4 # 総人口(万人) y = d['A4101'].values / 1e3 # 出生数(千人) res = stats.linregress(x, y) print(f'回帰式 y = {res.slope:.4f} x + {res.intercept:.3f}') print(f'相関係数 r = {res.rvalue:.4f} 決定係数 R² = {res.rvalue**2:.4f}') print(f'傾きの標準誤差 = {res.stderr:.5f} p 値 = {res.pvalue:.2e}') # 回帰線と 95% 信頼帯 xs = np.linspace(x.min(), x.max(), 100) ys = res.slope * xs + res.intercept n = len(x) se = np.sqrt(((y - (res.slope*x + res.intercept))**2).sum() / (n-2)) band = stats.t.ppf(0.975, n-2) * se * np.sqrt(1/n + (xs-x.mean())**2 / ((x-x.mean())**2).sum()) fig, ax = plt.subplots(figsize=(7, 5)) ax.scatter(x, y, alpha=0.7, edgecolor='white') ax.plot(xs, ys, color='crimson', lw=2, label=f'回帰直線 R²={res.rvalue**2:.3f}') ax.fill_between(xs, ys-band, ys+band, color='crimson', alpha=0.15, label='回帰直線の 95% 信頼帯') ax.set_xlabel('総人口(万人)'); ax.set_ylabel('出生数(千人)') ax.legend(); plt.tight_layout(); plt.savefig('scatter_regline.png', dpi=120) print(f'人口 500 万人(x=500)の県の予測出生数 = {res.slope*500 + res.intercept:.2f} 千人') print(f'信頼帯の幅(データ中心 x={x.mean():.1f} 付近)= ±{band[np.argmin(abs(xs-x.mean()))]:.3f} 千人') print(f'信頼帯の幅(右端 x={x.max():.1f} 付近) = ±{band[-1]:.3f} 千人') |
📤 実行するとこの出力が得られる:
💬 結果の読み方: R² = 0.9909、 p = 1.5e-47 で、 直線は非常によく当てはまっている。 注目すべきは 信頼帯の幅がデータ中心(人口 264.6 万人付近)で ±0.487 千人、 右端(東京 1,408.6 万人)で ±2.071 千人と 4 倍以上に広がる点である。 東京は x 方向に大きく離れた 1 点しかないため、 その付近の直線の位置は ほぼ東京 1 県だけで決まっている。 R² が高いからといって図の右端まで 同じ信頼度で読んではいけない。 外挿(x の範囲外)に至っては帯すら描かれないが、 そこはデータが何も語っていない領域である。
点が多いと後から描いた点が前の点を隠し、 実際の密度が図から消える。 alpha を 1 未満にすると点が半透明になり、 重なった箇所ほど濃く見える。 目安は α ≈ 1/(1 箇所に重なる点数)で、 数百点なら 0.3、 数千点なら 0.05 程度から試す。
📥 入力: SSDSE-B-2026 の全年度 564 行(47 県 × 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 31 32 33 34 | import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) # 年度で絞らず 47 県 × 12 年 = 564 点を重ねて描く(点が重なる状況を作る) x = df['A1101'].values / 1e4 y = df['A4101'].values / 1e3 print(f'描画する点の数: {len(x)} 点(47 県 × 12 年)') fig, axes = plt.subplots(1, 3, figsize=(14, 4.2), sharex=True, sharey=True) for ax, a in zip(axes, [1.0, 0.3, 0.08]): ax.scatter(x, y, alpha=a, s=28, color='#1E88E5', edgecolor='none') ax.set_title(f'透明度 α = {a}') ax.set_xlabel('総人口(万人)') axes[0].set_ylabel('出生数(千人)') plt.tight_layout(); plt.savefig('scatter_alpha.png', dpi=120) # 「画面上で何点が重なるか」を数える。 データ値の一致ではなく、 # 描画領域を 60x60 の升目に切って「同じ升に落ちる点」を重なりとみなす。 import numpy as np gx = np.floor((x - x.min()) / (x.max() - x.min()) * 59).astype(int) gy = np.floor((y - y.min()) / (y.max() - y.min()) * 59).astype(int) cells = list(zip(gx, gy)) uniq = len(set(cells)) from collections import Counter c = Counter(cells) print(f'60x60 の升目に落とすと異なる位置は {uniq} 箇所 → {len(x) - uniq} 点が他の点に隠れる') print(f'重なり率 = {(len(x)-uniq)/len(x)*100:.1f}%') print(f'最も混み合う升には {max(c.values())} 点が重なっている') # alpha を重ねたときの見かけの濃さ for a, k in [(1.0, 3), (0.3, 3), (0.08, 3)]: print(f'alpha={a}: 同じ位置に {k} 点重なると不透明度 {1-(1-a)**k:.3f}') |
📤 実行するとこの出力が得られる:
💬 結果の読み方: 描画領域を 60×60 の升目に切って数えると、 564 点のうち 431 点(76.4%)は 他の点と同じ升に落ちている。 最も混み合う升には 41 点が重なっており、 α=1.0 ではその 41 点が「1 点」にしか見えない。 α=0.3 なら 3 点重なった時点で 不透明度 0.657、 α=0.08 なら 0.221 と、 重なりの数が濃淡として読めるようになる。 ただし α を下げすぎると孤立点(東京など重要な外れ値)が薄くなって見落とされる。 点数が数万を超えたら α では足りないので、 hexbin や 2 次元 KDE に切り替える。
散布図は 2 変数の関係を示すが、 それぞれの変数が どんな形の分布なのかは教えてくれない。 上と右にヒストグラムを 並べた図(マージナルプロット、 seaborn の jointplot 相当)にすると、 「関係」と「分布」を 1 枚で同時に確認できる。
📥 入力: SSDSE-B-2026 の 2023 年 47 都道府県。 x=総人口(万人)、 y=出生数(千人)。
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 | import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from scipy import stats df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023] x = d['A1101'].values / 1e4 # 総人口(万人) y = d['A4101'].values / 1e3 # 出生数(千人) # 散布図の上と右に、 各軸の分布(周辺分布)を並べる fig = plt.figure(figsize=(7.5, 6)) gs = fig.add_gridspec(2, 2, width_ratios=(4, 1), height_ratios=(1, 4), wspace=0.05, hspace=0.05) ax = fig.add_subplot(gs[1, 0]) ax_top = fig.add_subplot(gs[0, 0], sharex=ax) ax_right = fig.add_subplot(gs[1, 1], sharey=ax) ax.scatter(x, y, alpha=0.75, edgecolor='white') ax_top.hist(x, bins=15, color='#90CAF9') ax_right.hist(y, bins=15, orientation='horizontal', color='#90CAF9') ax_top.tick_params(labelbottom=False); ax_right.tick_params(labelleft=False) ax.set_xlabel('総人口(万人)'); ax.set_ylabel('出生数(千人)') plt.savefig('scatter_marginal.png', dpi=120, bbox_inches='tight') print('=== 散布図だけでは見えない、 各軸それ自体の形 ===') for name, v in [('総人口', x), ('出生数', y)]: print(f'{name}: 平均 {v.mean():7.2f} 中央値 {pd.Series(v).median():7.2f} ' f'歪度 {stats.skew(v):5.2f}') print() print(f'総人口: 平均 > 中央値 の差は {x.mean() - pd.Series(x).median():.2f} 万人') print('→ どちらの周辺分布も強い右裾を持つ(歪度 2 以上)。') print(' 散布図の点は左下に密集し、 右上に少数が伸びる形になる。') print(f'相関係数 r = {stats.pearsonr(x, y)[0]:.4f}(散布図が示す 2 変数の関係)') |
📤 実行するとこの出力が得られる:
💬 結果の読み方: 総人口は平均 264.58 万人に対し中央値 154.90 万人、 その差は 109.68 万人。 歪度は総人口 2.22・出生数 2.29 で、 どちらの周辺分布も強い右裾を持つ。 つまりこの散布図の点は左下に密集し、 右上に少数が長く伸びる形にならざるを得ない。 r = 0.9954 という高い相関も、 右上に離れた少数の県(東京・神奈川・大阪)が 引っ張って作っている可能性を疑う必要がある。 周辺分布を見ずに相関係数だけを 報告すると、 この構造を読者に隠すことになる。
人口のように最小と最大が 26 倍も違う量を線形軸で描くと、 小さい県が原点付近に固まって関係が読めない。 両軸を対数にすると、 「何倍か」が等間隔になり、 全県が均等に散らばる。 さらに両対数上の直線の傾きはべき乗則の指数そのものになり、 「人口が 2 倍になると何倍になるか」を直接読み取れる。
📥 入力: SSDSE-B-2026 の 2023 年 47 都道府県。 x=総人口、 y=一般診療所数 (I5102)。
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 | import pandas as pd import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from scipy import stats df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023] x = d['A1101'].values.astype(float) # 総人口(人) y = d['I5102'].values.astype(float) # 一般診療所数 fig, axes = plt.subplots(1, 2, figsize=(11, 4.5)) axes[0].scatter(x / 1e4, y, alpha=0.75, edgecolor='white') axes[0].set_title('通常の軸'); axes[0].set_xlabel('総人口(万人)') axes[1].scatter(x / 1e4, y, alpha=0.75, edgecolor='white') axes[1].set_xscale('log'); axes[1].set_yscale('log') axes[1].set_title('両対数の軸'); axes[1].set_xlabel('総人口(万人・対数)') axes[0].set_ylabel('一般診療所数'); axes[1].set_ylabel('一般診療所数(対数)') plt.tight_layout(); plt.savefig('scatter_logscale.png', dpi=120) r_lin = stats.pearsonr(x, y)[0] r_log = stats.pearsonr(np.log10(x), np.log10(y))[0] print(f'元のスケールでの相関 r = {r_lin:.4f}') print(f'両対数にしたあとの相関 r = {r_log:.4f}') print(f'人口の歪度 元 {stats.skew(x):.2f} → log10 {stats.skew(np.log10(x)):.2f}') print(f'診療所数の歪度 元 {stats.skew(y):.2f} → log10 {stats.skew(np.log10(y)):.2f}') # 両対数の傾き = べき乗則の指数 sl = stats.linregress(np.log10(x), np.log10(y)) print() print(f'両対数での回帰: log10(診療所数) = {sl.slope:.3f} * log10(人口) + {sl.intercept:.3f}') print(f'→ 診療所数 ∝ 人口^{sl.slope:.3f} (指数 1 なら人口に完全比例)') print(f'人口が 2 倍になると診療所数は {2**sl.slope:.3f} 倍') |
📤 実行するとこの出力が得られる:
💬 結果の読み方: 歪度が人口 2.22 → 0.79、 診療所数 3.03 → 0.96 と、 対数を取るだけで裾の重さが 大きく緩和される。 相関も 0.9717 → 0.9840 と上がるが、 これは関係が強くなったのではなく 直線で表せる形に近づいただけである。 そして両対数の傾き 0.966 が本題で、 これは「診療所数 ∝ 人口^0.966」、 つまり 人口が 2 倍になっても診療所数は 1.953 倍にしかならない ことを意味する。 指数が 1 をわずかに下回るので、 人口が多い県ほど 1 人あたりの診療所は少なめ。 線形軸の図からこの 0.966 という数字を読み取ることはできない。
alpha=0.3)、 ジッタリング、 六角ビン(hexbin)、 2D KDE などで対処。 n が数万を超えると単純散布図は無意味になることが多い。 SSDSE の n=47 では問題ないが、 個票データではほぼ必須。
散布図の代表的な事故は 「Overplotting で数万点が黒い塊化」「人口・所得を線形軸で描き地方県が原点に潰れる」「東京都 1 点で残り 46 県の構造が見えなくなる」「相関係数だけで判断し U 字や非線形を見落とす」の 4 つ。 SSDSE-B の n=47 では Overplotting は問題になりませんが、 個票データ (n=数万〜数百万) では透明度・hexbin・2D KDE への切替えが必須です。
散布図 がデータサイエンスの体系の中でどこに位置するかを、 3つの異なる視点で可視化します。 同じ情報でも見方を変えると気付きが変わります。
🌐 統計・データサイエンス › 記述統計 › 可視化 › 散布図
中心に 散布図 を置き、 そこから ヒストグラム・箱ひげ図・回帰直線・ヒートマップ・相関係数・単回帰 など 計 13 個の用語へ、 前提・兄弟・発展形といった関係を矢印で結んでいます。 線がどちら向きかを見れば、 先に読むべき用語とあとから読む用語が分かります。 ノードはドラッグで動かせ、 ホイールでズーム、 クリックでその用語のページへ遷移します。
大きな円が小さな円を包含する Circle Packing 図。 「散布図」は緑色でハイライト。
長方形を入れ子に分割した Treemap 図。 各分野の規模感を面積で比較。 「散布図」は緑色でハイライト。
| マップ | 分かること | こんな時に見る |
|---|---|---|
| 🔗 関係マップ | 手法間の横の関係(前提→発展→応用) | 「次に何を学べばよい?」 学習順序の判断 |
| ⭕ 包含マップ | 分類体系の入れ子構造(上位⊃下位) | 「この手法はどんなジャンルに属する?」 |
| 🌳 ツリーマップ | 分野の規模比較(面積=ボリューム) | 「データサイエンス全体の俯瞰像」 |
💡 3 つの図は同じ位置関係を別の角度から描いています。 関係マップは 散布図 の隣に何が並ぶかを、 包含マップとツリーマップは 統計・データサイエンス → 可視化 → 散布図 という入れ子の位置を示します。 「可視化には他に何があったか」を思い出したいときは包含マップを、 「次に何を読むか」を決めたいときは関係マップを開いてください。
「散布図」は 2 変数の関係を最初に視認する一手段 であり、 1 変量可視化で各軸の分布を理解した直後に位置し、 続く相関係数や回帰分析の判断材料となる。 上流の前処理から下流の因果推論まで一連の探索フローのハブを担う。
散布図を「最初に必ず描く」習慣を持つことで、 相関係数や回帰係数を盲信せず、 外れ値・非線形・群構造を視覚的に検知してから次の手法を選べる。
散布図を描くか別の可視化に切り替えるかは、 データの規模・変数数・分析目的で決まる。
| シナリオ | 重視する観点 | 候補手法 |
|---|---|---|
| 2 変数の関係を直感的に把握したい | 線形 / 非線形・外れ値の同時確認 | 散布図 + 相関係数 |
| n が 10,000 超で点が重なる | 密度の可視化・透明度・サンプリング | 2 次元ヒートマップ / hexbin / α=0.1 の散布図 |
| 3 変数以上を一度に見たい | ペアごとの関係を網羅 | seaborn.pairplot で散布図行列 |
| 群 (カテゴリ) 別に分布を比較したい | グループ間差の検出 | 散布図 + 色分け、 または 箱ひげ図 |
| 時間変化を見たい | 時系列軌跡の把握 | 線グラフ、 または時間色グラデーション付き散布図 |
| 因果を主張したい | 無作為割付・操作変数 | 散布図は「関係の発見」までで、 因果推論へ |
下の白い盤面が散布図です。 空白をクリック(スマホはタップ)で点を追加、 点をドラッグで移動できます。 点を動かすたびに、 ピアソンの相関係数 r と回帰直線がリアルタイムに更新されます。 「点をどう並べると r が大きくなる/小さくなるのか」を、 実際に手を動かして体感してください。
スライダーで「向き(正/負)」と「強さ(0〜±1)」を指定して点群を生成し、 見た目と r の対応を確かめましょう。
相関係数 r は −1 から +1 の値をとり、 符号が関係の向き(右上がりなら +、 右下がりなら −)、 絶対値が直線への沿い具合の強さを表します。 点をきれいな一直線に並べると |r| は 1 に近づき、 円状にばらまくと 0 に近づきます。 上の盤面で、 まず点を右上がりの直線状に置いてから、 1点ずつ縦方向にずらしてみると、 r が徐々に下がる様子が見えます。
散布図の強みは、 表や相関係数という1つの数値では潰れてしまう情報を、 1枚の絵に同時に載せられる点にあります。 点群を眺めるとき、 次の5つを順に読み取る習慣をつけると診断力が上がります。
この直感を数値で裏づけるのが基本手順です。 まず散布図で「形」を見て、 それから 相関・共分散・回帰直線 を重ねる。 逆順(数値だけ先に見る)だと、 曲線関係や外れ値を見落とします。 探索の全体像は 探索的データ分析(EDA) と 2変量可視化 のページも参照してください。
散布図の誤読は「見えたパターンをそのまま信じる」ことから生まれます。 ここでは特に事故が多い3つを、 SSDSE-B-2026 の実測値と架空の教科書例で掘り下げます。
SSDSE-B-2026(2023年・47都道府県)で「大学数(E6102)」と「一般病院数(I510120)」の散布図を描くと、 強い正の相関 $r = +0.849$ が出ます。 「大学が多い県は病院も多い」——一見もっともらしい発見に見えます。 しかしこれは、 両者がともに総人口(第3変数)と強く相関しているために生じた見かけの相関です(大学数×総人口 $r=+0.901$、 病院数×総人口 $r=+0.900$)。
人口の効果を取り除くため、 それぞれを人口100万人あたりに直して散布図を描き直すと、 相関は $r = -0.06$(ほぼ無相関)へと消えます。 「県の大きさ」を揃えた瞬間に関係が蒸発するのが、 第3変数(交絡)の典型的な振る舞いです。
| 都道府県 | 大学数 | 一般病院数 | 大学数/百万人 | 病院数/百万人 |
|---|---|---|---|---|
| 東京都 | 144 | 588 | 10.2 | 41.7 |
| 大阪府 | 58 | 463 | 6.6 | 52.8 |
| 高知県 | 5 | 107 | 7.5 | 160.7 |
| 鳥取県 | 3 | 39 | 5.6 | 72.6 |
| 島根県 | 2 | 37 | 3.1 | 56.9 |
絶対数で見ると東京が右上、 鳥取・島根が左下に並んで直線に見えます。 だが人口あたりに直すと、 むしろ人口の少ない高知が病院密度で突出するなど順序が入れ替わり、 直線関係は消えます。 交絡の詳細は 交絡・疑似相関、 因果の扱いは 因果 を参照。
同じデータで「年平均気温(B4101)」と「合計特殊出生率(A4103)」を散布図にすると $r = +0.50$ の中程度の正相関が出ます(沖縄県:23.8℃・1.60、 北海道:11.0℃・1.06)。 しかし「気温が高いと子どもが増える」わけではありません。 気候・産業構造・年齢構成・地域文化など多数の要因が背後にあり、 気温はその代理指標にすぎない可能性が高い。 散布図で見えるのは関連までで、 因果を主張するには実験・自然実験・操作変数などの追加デザインが要ります(因果)。
SSDSE-B-2026 は都道府県単位に集約されたデータです。 県平均どうしの散布図で見えた関係は、 個人レベルでも同じとは限りません。 集団の相関を個人にあてはめる誤りを生態学的誤謬と呼びます。 逆に、 全体では見えた関係が群別に分けると消える/逆転する現象(シンプソンのパラドックス)もあります。 集約の性質は 集約 のページで、 群構造は下記「色分け」で確認しましょう。
下記は Anscombe が 1973 年に示した架空の4組(教科書上の合成データ)です。 4組とも平均・分散・$r$・回帰直線がほぼ一致するのに、 散布図の形はまったく異なります。 「$r$ が同じ=関係が同じ」ではないことの決定的な反例で、 計算の前に必ず描く理由そのものです。 上の「🎮 触って理解する」盤面の「アンスコム②」プリセット(架空の放物線データ)でも、 きれいなU字なのに $r\approx0.8$ のままになる様子を体験できます。
散布図は拡張の宝庫です。 目的別に道具を選べるよう、 「点が多すぎる問題」と「変数が多すぎる問題」に分けて整理します。
| 手法 | やること | 向いている状況 |
|---|---|---|
| 透明度 α | alpha=0.1〜0.3 で重なりを濃淡に | 中規模(数千点)。 透明度参照 |
| ジッター | 離散値に微小な乱れを加えて重なりをほどく | 整数・カテゴリ格子状データ |
| ヘックスビン | plt.hexbin で六角格子の件数を色に | 大規模(数万点超)の密度可視化 |
| 2D KDE / 等高線 | sns.kdeplot(x,y) で密度の等高線 | 滑らかな密度の山を見たいとき |
| 2Dヒストグラム | plt.hist2d/ヒートマップ化 | 格子ごとの件数を厳密に見たいとき |
seaborn.pairplot。 変数が10超なら相関行列ヒートマップを併用。sns.regplot(..., lowess=True))。 直線回帰で見落とす曲がりを可視化。xscale('log')。 両対数で直線ならべき乗則。 0・負値は対数を取れない点に注意。SSDSE-B-2026 の $n=47$ では過剰プロットは起きませんが、 個票($n$=数万〜)に移った瞬間に単純散布図は黒い塊になります。 「規模が変わったら道具も変える」を原則に。 前提知識は 1変量可視化・ヒストグラム、 並列手法は 箱ひげ図、 発展は 回帰分析・重回帰 を参照。