Первичная версия: стилометрия реплик, LSA и протокол экспериментов

This commit is contained in:
2026-10-24 01:17:36 +03:00
commit c390224500
52 changed files with 10099 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
"""Кластеризация персонажей в стилометрическом пространстве."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Sequence
import numpy as np
import pandas as pd
from scipy.sparse import issparse
from sklearn.cluster import AgglomerativeClustering, KMeans
from sklearn.metrics import (
adjusted_rand_score,
calinski_harabasz_score,
davies_bouldin_score,
normalized_mutual_info_score,
silhouette_score,
)
from sklearn.preprocessing import normalize
from .pipelines import FeatureConfig, build_feature_union
@dataclass
class ClusteringResult:
metrics: pd.DataFrame
centroids: pd.DataFrame
def _centroid_matrix(matrix: np.ndarray, labels: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
classes = np.unique(labels)
centroids = np.vstack([matrix[labels == label].mean(axis=0) for label in classes])
return classes, centroids
def run_clustering(
X: pd.DataFrame,
y: Sequence,
feature_config: FeatureConfig,
n_clusters: int | None = None,
seed: int = 42,
) -> ClusteringResult:
"""Кластеризует центроиды персонажей и считает внешние метрики."""
y = np.asarray(y)
features = build_feature_union(feature_config)
matrix = features.fit_transform(X)
matrix = matrix.toarray() if issparse(matrix) else np.asarray(matrix)
matrix = normalize(matrix, norm="l2", copy=False)
classes, centroids = _centroid_matrix(matrix, y)
# Если число кластеров равно числу персонажей, кластеризация тривиальна,
# а силуэт не определён. По умолчанию берём примерно корень из числа
# классов: для 34 персонажей это 6 групп.
n_clusters = int(n_clusters or max(2, round(len(classes) ** 0.5)))
n_clusters = min(n_clusters, len(classes) - 1)
rows: list[dict[str, Any]] = []
for name, model in (
(
"agglomerative_cosine",
AgglomerativeClustering(
n_clusters=n_clusters, metric="cosine", linkage="average"
),
),
(
"kmeans",
KMeans(n_clusters=n_clusters, random_state=seed, n_init=20),
),
):
predicted = model.fit_predict(centroids)
n_predicted = len(set(predicted))
valid_partition = 2 <= n_predicted < len(centroids)
rows.append(
{
"method": name,
"n_clusters": n_clusters,
"adjusted_rand_index": float(adjusted_rand_score(classes, predicted)),
"normalized_mutual_info": float(normalized_mutual_info_score(classes, predicted)),
"silhouette_cosine": float(
silhouette_score(centroids, predicted, metric="cosine")
) if valid_partition else 0.0,
"calinski_harabasz": float(calinski_harabasz_score(centroids, predicted))
if valid_partition else 0.0,
"davies_bouldin": float(davies_bouldin_score(centroids, predicted))
if valid_partition else 0.0,
}
)
centroid_frame = pd.DataFrame(centroids, index=classes)
centroid_frame.index.name = "character"
return ClusteringResult(metrics=pd.DataFrame(rows), centroids=centroid_frame)