1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

世界各国の統計を散布図で探るツールを作った — 対数スケールとピアソン相関を自前で実装する

1
Posted at

「一人あたり GDP が高い国は平均寿命も長いのか?」みたいな問いを、48 カ国の統計データで散布図にして眺めるツールを作った。実装の hinge は 2 つ: (1) 人口 (シンガポール 5.6M 〜 インド 1,417M で 250 倍) や GDP のように桁が大きく異なる指標は対数スケールで配置・相関しないと点が片側に潰れる(2) ピアソンの相関係数を自前で実装して、軸を変えるたびに即時計算する。ライブラリ無し・vanilla JS で、計算層は Node テスト 34 個。

🌐 デモ: https://sen.ltd/portfolio/global-stats/
📦 GitHub: https://github.com/sen-ltd/global-stats

スクリーンショット

データモデル

48 カ国 × 5 指標 (人口・一人あたり GDP・平均寿命・一人あたり CO2・国土面積):

{ name: "日本", code: "JP", region: "アジア",
  population: 125.1, gdpPerCapita: 33800, lifeExpectancy: 84.5,
  co2PerCapita: 8.5, area: 378 },

指標の定義は別テーブルに切り出し、log フラグを持たせる:

export const METRICS = [
  { key: "population",     label: "人口 (百万人)",       log: true  },
  { key: "gdpPerCapita",   label: "一人あたり GDP (USD)", log: true  },
  { key: "lifeExpectancy", label: "平均寿命 (年)",       log: false },
  { key: "co2PerCapita",   label: "一人あたり CO2 (t)",  log: true  },
  { key: "area",           label: "国土面積 (千km²)",    log: true  },
];

寿命だけ log: false。これが効いてくる。

なぜ対数スケールが必要か

線形軸で「人口 vs GDP」を散布すると悲惨なことになる。人口はシンガポール 5.6M からインド 1,417M まで 250 倍、GDP/人 はエチオピア $1,030 からノルウェー $106,150 まで 100 倍の開きがある。線形軸だと:

  • ほとんどの点が原点付近の左下に潰れる
  • 中国・インドだけが右端に張り付く
  • 相関係数も大きな外れ値に引っ張られて歪む

解決策は対数変換。10 倍ごとに等間隔にすることで、桁の違う国を同じ視野で比較できる。寿命 (52〜85 年、せいぜい 1.6 倍) のような線形指標はそのまま。

export function normalize(value, metric, domainMin, domainMax) {
  if (metric.log) {
    const lv = Math.log10(value);
    const lmin = Math.log10(domainMin);
    const lmax = Math.log10(domainMax);
    if (lmax === lmin) return 0.5;
    return (lv - lmin) / (lmax - lmin);
  }
  // linear
  if (domainMax === domainMin) return 0.5;
  return (value - domainMin) / (domainMax - domainMin);
}

テストで「対数スケールでは幾何平均が中央 (0.5) に来る」ことを確認:

test("log: geometric midpoint → 0.5", () => {
  const m = getMetric("gdpPerCapita");
  // domain 1000..100000、幾何平均 = 10000 → 0.5
  assert.ok(Math.abs(normalize(10000, m, 1000, 100000) - 0.5) < 1e-9);
});

線形なら 50500 が中央だが、対数では幾何平均の 10000 が中央。この違いが「桁で見る」ということ。

ピアソン相関を自前で実装

2 軸を選んだら相関係数 r を出す。ピアソンの積率相関係数の定義そのまま:

export function pearson(xs, ys) {
  const n = xs.length;
  if (n < 2 || ys.length !== n) return null;
  const meanX = xs.reduce((a, b) => a + b, 0) / n;
  const meanY = ys.reduce((a, b) => a + b, 0) / n;
  let num = 0, denX = 0, denY = 0;
  for (let i = 0; i < n; i++) {
    const dx = xs[i] - meanX;
    const dy = ys[i] - meanY;
    num += dx * dy;
    denX += dx * dx;
    denY += dy * dy;
  }
  const den = Math.sqrt(denX * denY);
  if (den === 0) return null; // 分散ゼロ → 相関は未定義
  return num / den;
}

den === 0 (どちらかの分散がゼロ) を null で返すのが大事。0/0 = NaN を可視化に流すと軸ラベルが壊れる。未定義を明示的に扱う

エッジケースをテスト:

test("perfect positive correlation = 1", () => {
  assert.ok(Math.abs(pearson([1, 2, 3], [2, 4, 6]) - 1) < 1e-9);
});
test("no correlation ≈ 0", () => {
  // 対称な V 字は線形相関ゼロ
  assert.ok(Math.abs(pearson([-2, -1, 0, 1, 2], [4, 1, 0, 1, 4])) < 1e-9);
});
test("zero variance → null", () => {
  assert.equal(pearson([5, 5, 5], [1, 2, 3]), null);
});

V 字のテストが地味に重要: 「相関ゼロ = 無関係」ではなく「線形相関ゼロ」。完全な放物線でも線形相関は 0 になる。ピアソンが捉えるのは線形関係だけ、という限界を理解した上でのテスト。

相関も対数空間で取る

ここが一番のポイント。散布図を対数で表示するなら、相関係数も対数変換した値で計算しないと一致しない。べき乗則 (y = ax^b) の関係は log-log 空間で直線 (log y = b·log x + log a) になるので、対数変換後にピアソンを取ると関係の強さが正しく出る:

export function metricCorrelation(keyX, keyY, pool) {
  const mx = getMetric(keyX), my = getMetric(keyY);
  const xs = [], ys = [];
  for (const c of pool) {
    let x = c[keyX], y = c[keyY];
    if (mx.log) x = Math.log10(x); // power-law → linear
    if (my.log) y = Math.log10(y);
    xs.push(x); ys.push(y);
  }
  return pearson(xs, ys);
}

実データで確認できる相関:

test("GDP vs life expectancy is a strong positive correlation", () => {
  const r = metricCorrelation("gdpPerCapita", "lifeExpectancy");
  assert.ok(r > 0.5, `got ${r}`);
});

実際に出る値は r ≈ 0.84 (強い正の相関)。「プレストン曲線」として知られる、所得と寿命の有名な関係がデータから再現される。GDP は対数・寿命は線形なので、片対数で相関を取っているのもポイント (寿命は所得の対数に比例するという経済学の知見と整合する)。

散布図の Y 軸反転

SVG は左上原点なので、「値が大きいほど上」にするには Y を反転する:

export function scatterPoints(keyX, keyY, pool) {
  // ...
  return pool.map((c) => ({
    country: c,
    cx: normalize(c[keyX], mx, dx.min, dx.max),
    cy: 1 - normalize(c[keyY], my, dy.min, dy.max), // ← 反転
  }));
}

これもテストで保証:

test("y is inverted: highest life-expectancy country has smallest cy", () => {
  const top = pts.reduce((a, b) => (b.country.lifeExpectancy > a.country.lifeExpectancy ? b : a));
  for (const p of pts) {
    if (p.country.code !== top.country.code) {
      assert.ok(p.cy >= top.cy - 1e-9);
    }
  }
});

データ整合性テスト

公的データのハードコードなので、整合性テストをセットで:

test("no duplicate ISO codes", () => {
  const codes = COUNTRIES.map((c) => c.code);
  assert.deepEqual(codes.filter((c, i) => codes.indexOf(c) !== i), []);
});
test("every metric field is present and positive", () => {
  for (const c of COUNTRIES) {
    for (const m of METRICS) {
      assert.ok(typeof c[m.key] === "number" && c[m.key] > 0);
    }
  }
});
test("life expectancy in a sane range (40-90)", () => {
  for (const c of COUNTRIES) {
    assert.ok(c.lifeExpectancy >= 40 && c.lifeExpectancy <= 90);
  }
});

対数スケールを使う以上「全指標が正の値」は単なる整合性チェックではなく前提条件 (log10(0) = -∞、log10(負) = NaN)。テストで守る価値がある。

設計

data.js  ← 48 カ国 × 5 指標 (出典: World Bank / UN / OWID ~2022)
core.js  ← pearson, normalize (log対応), scatter scaling, 地域集計 (DOM-free, 34 tests)
app.js   ← SVG 散布図 + ソート可能テーブル

試してみる

軸を「CO2 vs GDP」にすると正の相関 (豊かな国ほど排出が多い) が見える。「人口 vs 寿命」にすると無相関 (大国でも小国でも寿命は変わらない) になる。色は地域分け。

まとめ

  • 桁が大きく異なる指標 (人口・GDP・面積) は対数スケールで散布しないと点が潰れる。線形指標 (寿命) はそのまま。log フラグで指標ごとに切り替える。
  • ピアソン相関は定義通り実装できる。分散ゼロは null で返して NaN を可視化に流さない。
  • 対数表示するなら相関も対数変換した値で計算する。べき乗則が log-log で直線になるため。
  • ピアソンは線形相関のみ。V 字テストでその限界を明示する。
  • 対数スケール採用時は「全指標が正」は整合性チェックではなく前提条件。テストで守る。
  • GDP と寿命は r≈0.84。プレストン曲線がデータから再現される。

これは SEN 合同会社の OSS ポートフォリオ #262 です。https://sen.ltd/portfolio/

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?