導入
統計やデータサイエンスをかじっていると一回くらい
$$
r=\cos{\theta}
$$
という取ってつけたような式を見たことあるんじゃないか?と思います。正直、これがわかるようでわからないのでちょっと考えてみることにします。よくある説明は観測値$\left(x_{1},y_{1}\right),\left(x_{2},y_{2}\right), \cdots , \left(x_{n},y_{n}\right)$に対して
\left(
\begin{array}{c}
x_{1} -\bar{x}\\
x_{2} -\bar{x} \\
\cdots \\
x_{n} -\bar{x}
\end{array}
\right),\left(
\begin{array}{c}
y_{1} -\bar{y}\\
y_{2} -\bar{y} \\
\cdots \\
y_{n} -\bar{y}
\end{array}
\right)
というベクトルを考えた時に、このベクトルを規格化して内積を取ったら、上の式が出てくるという説明が多いですが、君どこから出てきたの??という疑問が率直にあります。ということで、ちょっと分解してこれについて考えてみようと思います。
先に結論を書くと、これは平均を差し引き変動だけに着目したベクトル同士のなす角を求めています。その中身について線形代数を用いて解説しています。
この記事は最低限、統計の話として
- 平均
- 分散
- 共分散・相関係数
線形代数の知識として
- ベクトルの大きさ(ノルム)
- ベクトルの内積
の定義がわかることを要求します。
平均と偏差は何を言っているのか?
ベクトルの導入
これからの議論のために、ベクトルを定義しておきます。
やや唐突ですが
\mathbf{x}=\left(
\begin{array}{c}
x_{1}\\
x_{2} \\
\cdots \\
x_{n}
\end{array}
\right)
とおき
\mathbf{1}=\left(
\begin{array}{c}
1\\
1 \\
\cdots \\
1
\end{array}
\right)
とおきます。観測値の平均を$\bar{x}=\frac{1}{n}\sum_{i=1}^{n}{x_{i}}$とし
\mathbf{\bar{x}}=\bar{x}\mathbf{1}=\left(
\begin{array}{c}
\bar{x}\\
\bar{x} \\
\cdots \\
\bar{x}
\end{array}
\right)
とおきます。
\mathbf{d}_{x}=\left(
\begin{array}{c}
x_{1} -\bar{x}\\
x_{2} -\bar{x} \\
\cdots \\
x_{n} -\bar{x}
\end{array}
\right) = \mathbf{x}-\mathbf{\bar{x}}
とします。各成分は観測値の平均と差になっていて、偏差と呼ばれる量になります。なので$x$の偏差ベクトルと名前をつけます。
幾何的な意味の考察
平均と偏差の方向
平均ベクトル
\mathbf{\bar{x}}=\bar{x}\mathbf{1}=\left(
\begin{array}{c}
\bar{x}\\
\bar{x} \\
\cdots \\
\bar{x}
\end{array}
\right)
は$\mathbf{1}$に平行なベクトルになります。二次元でいう$y=x$に平行なベクトルとなります。描画すると以下のようなイメージです。
----- setup -----
xbar = 1.5 # 平均値の例(自由に変更)
# vectors
one = np.array([1.0, 1.0])
mean_vec = xbar * one
# range for plotting y=x
t = np.linspace(-3, 3, 200)
fig, ax = plt.subplots(figsize=(6, 6))
# y = x line (direction of (1,1))
ax.plot(t, t, label=r"$y=x$ (direction of $\mathbf{1}$)")
# vector (1,1)
ax.quiver(
0, 0, one[0], one[1],
angles='xy', scale_units='xy', scale=1,
label=r"$\mathbf{1}=(1,1)$"
)
# mean vector
ax.quiver(
0, 0, mean_vec[0], mean_vec[1],
angles='xy', scale_units='xy', scale=1,
label=r"$\mathbf{\bar{x}}=\bar{x}\mathbf{1}$"
)
# axes
ax.axhline(0)
ax.axvline(0)
ax.set_aspect('equal')
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
ax.set_xlabel("1st component")
ax.set_ylabel("2nd component")
ax.set_title(r"Mean vector direction for $n=2$")
ax.legend()

ここで、$\mathbf{1}$と$\mathbf{d}_{x}$の内積を取ります。
\begin{align}
\left<\mathbf{1}, \mathbf{d}_{x}\right> &= \left<\mathbf{1}, \mathbf{x}-\mathbf{\bar{x}}\right> \\
&=\sum_{i=1}^{n}1 \times\left( x_{i}-\bar{x} \right) \\
&=n\bar{x}-n\bar{x} \\
&=0
\end{align}
から平均方向と偏差ベクトルが直交することがわかります。
\begin{align}
\mathbf{x}&=\mathbf{\bar{x}} + \left( \mathbf{x}-\mathbf{\bar{x}} \right) \\
&=\mathbf{\bar{x}} + \mathbf{d}_{x}
\end{align}
となり、これは観測値を平均方向と偏差方向の直交系に分解することができることを示唆しています。$n=2$で描画すると以下のようなイメージです。
# ====== Example data (n=2) ======
x1, x2 = 2.5, 0.5 # change these freely
x = np.array([x1, x2], dtype=float)
xbar = x.mean()
x_bar_vec = np.array([xbar, xbar], dtype=float)
d = x - x_bar_vec # deviation vector
# ====== Plot ======
fig, ax = plt.subplots(figsize=(6, 6))
origin = np.array([0.0, 0.0])
pts = np.vstack([origin, x, x_bar_vec, d])
m = np.max(np.abs(pts)) * 1.35 + 0.5
ax.set_xlim(-m, m)
ax.set_ylim(-m, m)
ax.axhline(0)
ax.axvline(0)
# Direction of 1 = (1,1): the line y=x
t = np.linspace(-m, m, 200)
ax.plot(t, t)
# Vectors: x, xbar, and deviation (from xbar to x)
ax.annotate("", xy=x, xytext=origin, arrowprops=dict(width=1, headwidth=10, headlength=12))
ax.annotate("", xy=x_bar_vec, xytext=origin, arrowprops=dict(width=1, headwidth=10, headlength=12))
ax.annotate("", xy=x, xytext=x_bar_vec, arrowprops=dict(width=1, headwidth=10, headlength=12))
# Labels
ax.text(x[0], x[1], r"$\mathbf{x}=(x_1,x_2)$", ha="left", va="bottom")
ax.text(x_bar_vec[0], x_bar_vec[1], r"$\mathbf{\bar{x}}=(\bar{x},\bar{x})$", ha="left", va="bottom")
ax.text((x[0]+x_bar_vec[0])/2, (x[1]+x_bar_vec[1])/2, r"$\mathbf{x}-\mathbf{\bar{x}}$", ha="left", va="bottom")
ax.text(0.02, 0.98, rf"$x_1={x1},\,x_2={x2},\,\bar{{x}}={xbar:.3g}$",
transform=ax.transAxes, va="top")
ax.set_aspect("equal", adjustable="box")
ax.set_xlabel("1st component")
ax.set_ylabel("2nd component")
ax.set_title(r"n=2: $\mathbf{x}$, $\mathbf{\bar{x}}$, and deviation $\mathbf{x}-\mathbf{\bar{x}}$")
すなわち観測値を平均方向と偏差方向の直交系に分離でき偏差ベクトルとはその偏差方向の成分ということになります。
平均がデータ自体の大きさの成分、偏差方向がデータのばらつきの成分と解釈ができるので、平均方向を引くことでばらつきのみを考えていると解釈ができます。
分散について
分散は
s_{x}^{2}= \frac{1}{n}\sum_{i=1}^{n}{\left(x_{i}-\bar{x} \right)^{2}}=\frac{1}{n}\left\|\mathbf{d}_{x} \right\|^{2}
であるから、偏差方向の長さに対応していることがわかります。これはばらつきの大きさを表している量と解釈することができます。
相関係数について
今度は二次元の観測値$\left(x_{1},y_{1}\right),\left(x_{2},y_{2}\right), \cdots , \left(x_{n},y_{n}\right)$を考えることにします。同様に
\mathbf{y}=\left(
\begin{array}{c}
y_{1}\\
y_{2} \\
\cdots \\
y_{n}
\end{array}
\right)
として、
$y$の平均を$\bar{y}=\frac{1}{n}\sum_{i=1}^{n}{y_{i}}$とし
\mathbf{\bar{y}}=\bar{y}\mathbf{1}=\left(
\begin{array}{c}
\bar{y}\\
\bar{y} \\
\cdots \\
\bar{y}
\end{array}
\right)
とおきます。
\mathbf{d}_{y}=\left(
\begin{array}{c}
y_{1} -\bar{y}\\
y_{2} -\bar{y} \\
\cdots \\
y_{n} -\bar{y}
\end{array}
\right) = \mathbf{y}-\mathbf{\bar{y}}
として、$y$の分散を
s_{y}^{2}= \frac{1}{n}\sum_{i=1}^{n}{\left(y_{i}-\bar{y} \right)^{2}}=\frac{1}{n}\left\|\mathbf{d}_{y} \right\|^{2}
とします。
共分散
共分散$s_{xy}$は
$$
s_{xy}=\frac{1}{n}\sum_{i=1}^{n}{\left(x_{i}-\bar{x} \right)\left(y_{i}-\bar{y} \right)}=\frac{1}{n}\left<\mathbf{d}_{x}, \mathbf{d}_{y} \right>
$$
とかけます。すなわち、ばらつき方向の内積と捉えることができ、これをスケーリングしたものがのちに解説する相関係数になります。
相関係数
これはさらに相関係数は
r = \frac{\sum_{i=1}^{n}{\left(x_{i}-\bar{x} \right)\left(y_{i}-\bar{y} \right)}}{\sqrt{\sum_{i=1}^{n}{\left(x_{i}-\bar{x} \right)^{2}}}\sqrt{\sum_{i=1}^{n}{\left(y_{i}-\bar{y} \right)^{2}}}}=\frac{\left<\mathbf{d}_{x},\mathbf{d}_{y} \right>}{\left\|\mathbf{d}_{x} \right\|\left\|\mathbf{d}_{y} \right\|}=\left<\frac{\mathbf{d}_{x}}{\left\|\mathbf{d}_{x} \right\|}, \frac{\mathbf{d}_{y}}{\left\|\mathbf{d}_{y} \right\|} \right>
とかけます。
\frac{\mathbf{d}_{x}}{\left\|\mathbf{d}_{x} \right\|},\frac{\mathbf{d}_{y}}{\left\|\mathbf{d}_{y} \right\|}
はそれぞれ、$x,y$の偏差方向の単位ベクトルなので二つの観測値の偏差方向(平均方向と直交)する方向の内積となります。すなわち二つの観測値で平均の方向成分を除いたベクトルのなす角を考えているのが相関係数となります。
観測値が2つしかない場合は平均方向を除くと一直線上に偏差ベクトルが乗るのでこの二つのベクトルは同じ方法か逆方向に向くかしかなく、相関係数は必ず$\pm 1$となります。以下に3つのデータで、2次元に射影した可視化を示します。3D空間のベクトルを、平均方法
$\mathbf{1}=\begin{array}{c}
(1,1,1)
\end{array}$ に直交する平面へ正射影し、その平面を可視化した図となります。
# =========================
# Inputs (n=3)
# =========================
x = np.array([1.0, 2.0, 4.0], dtype=float)
y = np.array([2.0, 0.5, 3.0], dtype=float)
# centered vectors
xbar, ybar = x.mean(), y.mean()
dx = x - xbar
dy = y - ybar
# correlation and angle in R^n
dot = float(dx @ dy)
nx = float(np.linalg.norm(dx))
ny = float(np.linalg.norm(dy))
r = dot / (nx * ny) if nx > 0 and ny > 0 else np.nan
theta = float(np.arccos(np.clip(r, -1, 1))) if np.isfinite(r) else np.nan
print(f"x = {x}, x̄ = {xbar:.3g}, dx = {dx}")
print(f"y = {y}, ȳ = {ybar:.3g}, dy = {dy}")
print(f"corr r = {r:.6g}, theta = {theta:.6g} rad")
# =========================
# Orthonormal basis of the subspace orthogonal to 1 (for n=3)
# u1 = (1,-1,0)/sqrt2, u2 = (1,1,-2)/sqrt6
# This preserves angles inside the centered subspace.
# =========================
u1 = np.array([1, -1, 0], dtype=float) / np.sqrt(2)
u2 = np.array([1, 1,-2], dtype=float) / np.sqrt(6)
def proj2(v):
return np.array([u1 @ v, u2 @ v], dtype=float)
dx2 = proj2(dx)
dy2 = proj2(dy)
# =========================
# Plot centered vectors in 2D coordinates + angle arc
# =========================
fig, ax = plt.subplots(figsize=(6, 6))
origin = np.array([0.0, 0.0])
pts = np.vstack([origin, dx2, dy2])
m = np.max(np.abs(pts)) * 1.6 + 0.5
ax.set_xlim(-m, m)
ax.set_ylim(-m, m)
ax.axhline(0)
ax.axvline(0)
ax.set_aspect("equal", adjustable="box")
ax.annotate("", xy=dx2, xytext=origin, arrowprops=dict(width=1, headwidth=10, headlength=12))
ax.annotate("", xy=dy2, xytext=origin, arrowprops=dict(width=1, headwidth=10, headlength=12))
ax.text(dx2[0], dx2[1], r"$\mathbf{d}_x$", ha="left", va="bottom")
ax.text(dy2[0], dy2[1], r"$\mathbf{d}_y$", ha="left", va="bottom")
# angle arc
if np.isfinite(theta) and np.linalg.norm(dx2) > 0 and np.linalg.norm(dy2) > 0:
ang_x = float(np.arctan2(dx2[1], dx2[0]))
ang_y = float(np.arctan2(dy2[1], dy2[0]))
d_ang = (ang_y - ang_x + np.pi) % (2*np.pi) - np.pi
ang_end = ang_x + d_ang
arc_r = 0.35 * m
ts = np.linspace(ang_x, ang_end, 200)
ax.plot(arc_r * np.cos(ts), arc_r * np.sin(ts))
mid = 0.5 * (ang_x + ang_end)
ax.text(arc_r * np.cos(mid), arc_r * np.sin(mid), r"$\theta$", ha="left", va="bottom")
ax.text(
0.02, 0.98,
rf"$r=\cos\theta={r:.3g}$" "\n" rf"$\theta={theta:.3g}\,\mathrm{{rad}}$",
transform=ax.transAxes, va="top"
)
ax.set_title(r"Centered vectors (projected): $r=\cos\theta$")
ax.set_xlabel("coord along $u_1$")
ax.set_ylabel("coord along $u_2$")
まとめ
すなわちこの話は
- 平均を引くことで、データの大きさを排除しデータのばらつきのみにする
- 分散は偏差ベクトルの長さ(ばらつきの大きさ)に対応する量
- データのばらつきを偏差ベクトルの大きさで規格化し、その余弦(コサイン類似度)を求める
という3つのステップで構成されていることがわかりました。
この考え方は最小二乗法による回帰直線の導出の導出や、主成分分析の分散最大化の考え方と同じような考え方です。この記事の中身の理解があればこれらの話が格段に理解しやすくなると思います。
実際、回帰分析では
「$\mathbf{d}_y$ を $\mathbf{d}_x$ の方向にどれだけ射影できるか」を計算しており、
主成分分析では
「偏差ベクトルの分散が最大になる方向」を探しています。
データラーニングギルドとは?
データラーニングギルド は、株式会社データラーニングが運営する、
データサイエンスを中心とした学習者・現役データサイエンティスト・エンジニアのためのコミュニティです。
学びの共有・キャリア形成・横のつながりを大切にし、
勉強会、LT会、技術相談、キャリア支援、案件紹介など、
「データ領域で挑戦したい人を応援する活動」を幅広く行っています。
初心者から実務者まで、誰もが成長できる場づくりを目指しています。

