PCA ищет ортогональные направления максимальной дисперсии данных. Компоненты — собственные векторы ковариационной матрицы . Первая компонента максимизирует при условии , далее компоненты ортогональны. Доля объясненной дисперсии для компоненты : .
Используемые библиотеки¶
Используем numpy, pandas, seaborn, matplotlib. Из sklearn — load_digits, StandardScaler, PCA.
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
sns.set_theme(style="whitegrid", palette="deep")
plt.rcParams["figure.dpi"] = 120
PRIMARY_COLOR = "#1f77b4"
SECONDARY_COLOR = "#ff7f0e"
HEATMAP_CMAP = "coolwarm"
Датасет: описание и частичная распечатка¶
Датасет Digits — 1797 изображений рукописных цифр 8×8 пикселей (64 признака). Это типичный пример данных высокой размерности, где PCA особенно полезен: 64 пиксельных признака содержат много шума и избыточной информации. PCA позволяет сжать данные до 2 компонент для визуализации и до ~40 компонент с сохранением >95% дисперсии.
digits = load_digits(as_frame=True)
data = digits.frame
print(f"Размерность: {data.shape}")
data.head()
Размерность: (1797, 65)
Предварительная обработка¶
StandardScaler обязателен перед PCA: без него признаки с большей дисперсией доминируют при вычислении собственных векторов ковариационной матрицы. После стандартизации каждый признак вносит одинаковый начальный вклад, и PCA выбирает направления истинной информационной структуры.
features = data.drop(columns=["target"])
scaler = StandardScaler()
X_scaled = scaler.fit_transform(features)
Тепловая карта корреляций¶
Для наглядности берём первые 12 пиксельных признаков. Соседние пиксели коррелируют (пространственная структура изображения) — именно эти корреляции PCA использует для нахождения главных компонент. Высокая корреляция признаков — признак того, что данные можно сжать без большой потери информации.
plt.figure(figsize=(6, 4))
subset_cols = features.columns[:12]
correlation = features[subset_cols].corr()
sns.heatmap(correlation, annot=False, cmap=HEATMAP_CMAP, linewidths=0.3)
plt.title("Корреляции (подмножество признаков)")
plt.tight_layout()
plt.show()

Обучение модели¶
PCA(n_components=3) сводит 64 признака к 3 главным компонентам. Это значительное сжатие (потеря информации присутствует), но достаточно для лучшего представления данных. Для практических задач выбирают n_components так, чтобы суммарная объяснённая дисперсия была ≥ 95%.
pca = PCA(n_components=3, random_state=42)
pca.fit(X_scaled)
/Users/fuodorov/Projects/ml-recipe-book/.venv/lib/python3.9/site-packages/sklearn/decomposition/_pca.py:606: RuntimeWarning: divide by zero encountered in matmul
C = X.T @ X
/Users/fuodorov/Projects/ml-recipe-book/.venv/lib/python3.9/site-packages/sklearn/decomposition/_pca.py:606: RuntimeWarning: overflow encountered in matmul
C = X.T @ X
/Users/fuodorov/Projects/ml-recipe-book/.venv/lib/python3.9/site-packages/sklearn/decomposition/_pca.py:606: RuntimeWarning: invalid value encountered in matmul
C = X.T @ X
Прогнозы модели¶
explained_variance_ratio_ — доля дисперсии, объяснённая каждой компонентой. PC1 и PC2 вместе объясняют обычно 25–35% для Digits (64 признака → 2 компоненты). Для понимания, сколько компонент нужно для 95% дисперсии, строят scree plot по всем компонентам.
X_pca = pca.transform(X_scaled)
explained = pca.explained_variance_ratio_
print("Explained Variance Ratio")
print(f"PC1: {explained[0]:.3f}")
print(f"PC2: {explained[1]:.3f}")
print(f"PC3: {explained[2]:.3f}")
Explained Variance Ratio
PC1: 0.120
PC2: 0.096
PC3: 0.084
/Users/fuodorov/Projects/ml-recipe-book/.venv/lib/python3.9/site-packages/sklearn/decomposition/_base.py:148: RuntimeWarning: divide by zero encountered in matmul
X_transformed = X @ self.components_.T
/Users/fuodorov/Projects/ml-recipe-book/.venv/lib/python3.9/site-packages/sklearn/decomposition/_base.py:148: RuntimeWarning: overflow encountered in matmul
X_transformed = X @ self.components_.T
/Users/fuodorov/Projects/ml-recipe-book/.venv/lib/python3.9/site-packages/sklearn/decomposition/_base.py:148: RuntimeWarning: invalid value encountered in matmul
X_transformed = X @ self.components_.T
Графики выходных результатов¶
График 1. Scree plot (explained variance bar). Показывает вклад PC1, PC2 и PC3. Если бы мы взяли больше компонент, мы бы видели «локоть» — место, где прирост объяснённой дисперсии резко замедляется.
График 2. 2D-проекция (scatter plot). Каждая точка — одна цифра, цвет — её класс (0–9). Хорошо разделённые цветовые облака означают, что 2 компоненты несут информацию о различии цифр. Ожидаем, что некоторые цифры (0, 1) будут хорошо изолированы, другие (3, 5, 8) — перекрываться.
plt.figure(figsize=(6, 4))
plt.bar(["PC1", "PC2", "PC3"], explained, color=PRIMARY_COLOR)
plt.ylabel("Доля объясненной дисперсии")
plt.title("PCA: explained variance")
plt.tight_layout()
plt.show()

2D проекции на различные пары осей¶
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# PC1 vs PC2
scatter1 = axes[0].scatter(X_pca[:, 0], X_pca[:, 1], c=digits.target, cmap="tab10", alpha=0.8)
axes[0].set_xlabel("PC1")
axes[0].set_ylabel("PC2")
axes[0].set_title("PC1 vs PC2")
axes[0].grid(True, alpha=0.3)
# PC1 vs PC3
scatter2 = axes[1].scatter(X_pca[:, 0], X_pca[:, 2], c=digits.target, cmap="tab10", alpha=0.8)
axes[1].set_xlabel("PC1")
axes[1].set_ylabel("PC3")
axes[1].set_title("PC1 vs PC3")
axes[1].grid(True, alpha=0.3)
# PC2 vs PC3
scatter3 = axes[2].scatter(X_pca[:, 1], X_pca[:, 2], c=digits.target, cmap="tab10", alpha=0.8)
axes[2].set_xlabel("PC2")
axes[2].set_ylabel("PC3")
axes[2].set_title("PC2 vs PC3")
axes[2].grid(True, alpha=0.3)
plt.suptitle("PCA: 2D проекции на разные пары компонент", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()

3D визуализация¶
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection="3d")
scatter = ax.scatter(
X_pca[:, 0],
X_pca[:, 1],
X_pca[:, 2],
c=digits.target,
cmap="tab10",
alpha=0.8,
)
ax.set_xlabel("PC1")
ax.set_ylabel("PC2")
ax.set_zlabel("PC3")
ax.set_title("PCA: проекция на 3 компоненты")
plt.legend(*scatter.legend_elements(), title="class", loc="upper left", bbox_to_anchor=(1.1, 1))
plt.tight_layout()
plt.show()
