168 lines
6.2 KiB
Python
168 lines
6.2 KiB
Python
"""Построение графиков для отчёта и презентации."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Sequence
|
|
|
|
import matplotlib
|
|
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt # noqa: E402
|
|
import numpy as np # noqa: E402
|
|
import pandas as pd # noqa: E402
|
|
from sklearn.decomposition import PCA # noqa: E402
|
|
from sklearn.manifold import TSNE # noqa: E402
|
|
|
|
|
|
def _save(fig: plt.Figure, path: str | Path) -> Path:
|
|
path = Path(path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
fig.tight_layout()
|
|
fig.savefig(path, dpi=200, bbox_inches="tight")
|
|
plt.close(fig)
|
|
return path
|
|
|
|
|
|
def plot_class_distribution(
|
|
frame: pd.DataFrame,
|
|
character_column: str = "character",
|
|
path: str | Path = "results/figures/class_distribution.png",
|
|
) -> Path:
|
|
counts = frame[character_column].value_counts().sort_values()
|
|
fig, ax = plt.subplots(figsize=(8, max(6, len(counts) * 0.25)))
|
|
ax.barh(counts.index, counts.values)
|
|
ax.set_xlabel("Число реплик")
|
|
ax.set_ylabel("Персонаж")
|
|
ax.set_title("Распределение реплик по персонажам")
|
|
return _save(fig, path)
|
|
|
|
|
|
def plot_scree(
|
|
explained_variance_ratio: Sequence[float],
|
|
path: str | Path = "results/figures/lsa_scree.png",
|
|
title: str = "Доля объяснённой дисперсии компонент LSA",
|
|
) -> Path:
|
|
values = np.asarray(explained_variance_ratio, dtype=float)
|
|
cumulative = np.cumsum(values)
|
|
fig, ax = plt.subplots(figsize=(7, 4.5))
|
|
ax.plot(np.arange(1, len(values) + 1), cumulative, marker="o", markersize=3)
|
|
ax.set_xlabel("Число компонент")
|
|
ax.set_ylabel("Накопленная доля дисперсии")
|
|
ax.set_title(title)
|
|
ax.grid(alpha=0.3)
|
|
return _save(fig, path)
|
|
|
|
|
|
def plot_confusion_matrix(
|
|
matrix: pd.DataFrame,
|
|
path: str | Path = "results/figures/confusion_matrix.png",
|
|
title: str = "Матрица ошибок",
|
|
annotate: bool | None = None,
|
|
) -> Path:
|
|
values = matrix.to_numpy(dtype=float)
|
|
n_classes = values.shape[0]
|
|
if annotate is None:
|
|
annotate = n_classes <= 20
|
|
fig, ax = plt.subplots(figsize=(max(8, n_classes * 0.35), max(7, n_classes * 0.32)))
|
|
image = ax.imshow(values, cmap="Blues", aspect="auto")
|
|
ax.set_xticks(np.arange(n_classes))
|
|
ax.set_yticks(np.arange(n_classes))
|
|
ax.set_xticklabels(matrix.columns, rotation=90, fontsize=7)
|
|
ax.set_yticklabels(matrix.index, fontsize=7)
|
|
ax.set_xlabel("Предсказанный класс")
|
|
ax.set_ylabel("Истинный класс")
|
|
ax.set_title(title)
|
|
if annotate:
|
|
for row in range(n_classes):
|
|
for column in range(n_classes):
|
|
value = values[row, column]
|
|
if value:
|
|
ax.text(
|
|
column, row, f"{value:.2f}" if value < 1 else f"{int(value)}",
|
|
ha="center", va="center", fontsize=6,
|
|
color="white" if value > values.max() * 0.5 else "black",
|
|
)
|
|
fig.colorbar(image, ax=ax, fraction=0.046, pad=0.04)
|
|
return _save(fig, path)
|
|
|
|
|
|
def plot_pca(
|
|
matrix: np.ndarray,
|
|
labels: Sequence[str],
|
|
path: str | Path = "results/figures/pca.png",
|
|
title: str = "PCA признакового пространства",
|
|
) -> Path:
|
|
values = np.asarray(matrix, dtype=float)
|
|
if values.shape[0] < 2:
|
|
raise ValueError("Для PCA нужно хотя бы два объекта.")
|
|
coordinates = PCA(n_components=2, random_state=42).fit_transform(values)
|
|
fig, ax = plt.subplots(figsize=(9, 7))
|
|
unique_labels = sorted(set(labels))
|
|
cmap = plt.get_cmap("tab20")
|
|
color_map = {label: cmap(index % 20) for index, label in enumerate(unique_labels)}
|
|
for label in unique_labels:
|
|
mask = np.asarray(labels) == label
|
|
ax.scatter(
|
|
coordinates[mask, 0], coordinates[mask, 1],
|
|
s=12, alpha=0.7, label=label, color=color_map[label],
|
|
)
|
|
ax.set_xlabel("Первая главная компонента")
|
|
ax.set_ylabel("Вторая главная компонента")
|
|
ax.set_title(title)
|
|
ax.legend(fontsize=6, ncol=2, loc="best", frameon=False)
|
|
return _save(fig, path)
|
|
|
|
|
|
def plot_tsne(
|
|
matrix: np.ndarray,
|
|
labels: Sequence[str],
|
|
path: str | Path = "results/figures/tsne.png",
|
|
title: str = "t-SNE признакового пространства",
|
|
perplexity: float = 30.0,
|
|
) -> Path:
|
|
values = np.asarray(matrix, dtype=float)
|
|
if values.shape[0] < 10:
|
|
raise ValueError("Для t-SNE нужно хотя бы десять объектов.")
|
|
perplexity = min(perplexity, max(5.0, values.shape[0] / 4.0))
|
|
coordinates = TSNE(
|
|
n_components=2,
|
|
perplexity=perplexity,
|
|
init="pca",
|
|
learning_rate="auto",
|
|
random_state=42,
|
|
).fit_transform(values)
|
|
fig, ax = plt.subplots(figsize=(9, 7))
|
|
unique_labels = sorted(set(labels))
|
|
cmap = plt.get_cmap("tab20")
|
|
color_map = {label: cmap(index % 20) for index, label in enumerate(unique_labels)}
|
|
for label in unique_labels:
|
|
mask = np.asarray(labels) == label
|
|
ax.scatter(
|
|
coordinates[mask, 0], coordinates[mask, 1],
|
|
s=10, alpha=0.7, label=label, color=color_map[label],
|
|
)
|
|
ax.set_xlabel("t-SNE 1")
|
|
ax.set_ylabel("t-SNE 2")
|
|
ax.set_title(title)
|
|
ax.legend(fontsize=6, ncol=2, loc="best", frameon=False)
|
|
return _save(fig, path)
|
|
|
|
|
|
def plot_feature_importance(
|
|
names: Sequence[str],
|
|
values: Sequence[float],
|
|
path: str | Path = "results/figures/feature_importance.png",
|
|
title: str = "Важность признаков",
|
|
top_n: int = 25,
|
|
) -> Path:
|
|
frame = pd.DataFrame({"feature": list(names), "value": np.asarray(values, dtype=float)})
|
|
frame["abs_value"] = frame["value"].abs()
|
|
frame = frame.sort_values("abs_value", ascending=False).head(top_n).iloc[::-1]
|
|
fig, ax = plt.subplots(figsize=(9, max(5, len(frame) * 0.28)))
|
|
ax.barh(frame["feature"], frame["value"])
|
|
ax.set_xlabel("Вклад признака")
|
|
ax.set_ylabel("Признак")
|
|
ax.set_title(title)
|
|
return _save(fig, path)
|