Исправлен .gitignore: пакет stylometry/features добавлен в репозиторий

This commit is contained in:
2026-10-25 15:52:25 +03:00
parent c390224500
commit 10e2444d23
15 changed files with 1000 additions and 7 deletions

View File

@@ -0,0 +1,192 @@
"""Агрегатор ручных стилометрических признаков.
Класс не обучается на данных: все формулы фиксированы. Поэтому его можно
безопасно применять ко всей таблице до кросс-валидации. Обучаемые
преобразования (TF-IDF, SVD, масштабирование) вынесены в другие классы и
настраиваются только на обучающих блоках.
"""
from __future__ import annotations
from typing import Callable, Iterable, Sequence
import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
from . import (
characters,
dialogue,
function_words,
length,
lexical,
pos_features,
pronouns,
punctuation,
readability,
syntax,
vocabulary,
)
FEATURE_GROUPS: dict[str, Callable[[pd.Series], dict[str, float]]] = {
"length": length.extract,
"lexical": lexical.extract,
"vocabulary": vocabulary.extract,
"characters": characters.extract,
"punctuation": punctuation.extract,
"function_words": function_words.extract,
"pronouns": pronouns.extract,
"pos": pos_features.extract,
"syntax": syntax.extract,
"readability": readability.extract,
"dialogue": dialogue.extract,
}
FEATURE_GROUP_PREFIXES: dict[str, str] = {
"length": "len_",
"lexical": "lex_",
"vocabulary": "vocab_",
"characters": "char_",
"punctuation": "punct_",
"function_words": "fw_",
"pronouns": "pron_",
"pos": "pos_",
"syntax": "syn_",
"readability": "read_",
"dialogue": "dlg_",
}
EXPLORATORY_GROUPS = {"vocabulary", "readability"}
def default_groups() -> list[str]:
return list(FEATURE_GROUPS)
def _validate_groups(groups: Sequence[str]) -> list[str]:
unknown = [group for group in groups if group not in FEATURE_GROUPS]
if unknown:
raise ValueError(
f"Неизвестные группы признаков: {unknown}. "
f"Доступны: {sorted(FEATURE_GROUPS)}"
)
return list(groups)
class StylometricVectorizer(BaseEstimator, TransformerMixin):
"""Считает матрицу ручных признаков по таблице реплик.
Ожидает DataFrame с колонками ``tokens``, ``upos``, ``ptb``, ``sentences``,
``text`` и ``placeholder_count``. Такие колонки создаёт
:class:`stylometry.tokenization.Tokenizer`.
"""
def __init__(self, groups: Sequence[str] | None = None) -> None:
self.groups = groups
def _selected_groups(self) -> list[str]:
groups = self.groups if self.groups is not None else default_groups()
return _validate_groups(groups)
@staticmethod
def _extract_row(row: pd.Series, groups: Sequence[str]) -> dict[str, float]:
features: dict[str, float] = {}
for group in groups:
features.update(FEATURE_GROUPS[group](row))
return features
def fit(self, X: pd.DataFrame, y=None) -> "StylometricVectorizer":
if not isinstance(X, pd.DataFrame):
raise TypeError("StylometricVectorizer ожидает pandas.DataFrame.")
if len(X) == 0:
raise ValueError("Пустая таблица признаков.")
groups = self._selected_groups()
sample = self._extract_row(X.iloc[0], groups)
self.feature_names_in_ = np.array(list(sample.keys()), dtype=object)
self.n_features_in_ = len(self.feature_names_in_)
return self
def transform(self, X: pd.DataFrame) -> np.ndarray:
if not isinstance(X, pd.DataFrame):
raise TypeError("StylometricVectorizer ожидает pandas.DataFrame.")
groups = self._selected_groups()
rows: list[list[float]] = []
names: list[str] | None = None
for _, row in X.iterrows():
features = self._extract_row(row, groups)
if names is None:
names = list(features.keys())
rows.append([float(features.get(name, 0.0)) for name in names])
if names is None:
raise ValueError("Не удалось вычислить признаки: таблица пуста.")
self.feature_names_in_ = np.array(names, dtype=object)
self.n_features_in_ = len(names)
return np.asarray(rows, dtype=float)
def get_feature_names_out(self, input_features=None) -> np.ndarray:
if not hasattr(self, "feature_names_in_"):
raise RuntimeError("Сначала вызовите fit или transform.")
return self.feature_names_in_
def feature_group(feature_name: str) -> str:
"""Определяет группу признака по его префиксу."""
for group, prefix in FEATURE_GROUP_PREFIXES.items():
if feature_name.startswith(prefix):
return group
return "other"
def describe_feature(feature_name: str) -> str:
"""Возвращает краткое описание признака для словаря и отчёта."""
group = feature_group(feature_name)
if group == "function_words":
if feature_name.startswith("fw_word_"):
word = feature_name.removeprefix("fw_word_").replace("_", " ")
return f"Относительная частота функционального слова «{word}»."
if feature_name.startswith("fw_cat_"):
category = feature_name.removeprefix("fw_cat_")
return f"Относительная частота категории функциональных слов «{category}»."
return "Суммарная относительная частота функциональных слов."
if group == "characters" and feature_name.startswith("char_freq_"):
letter = feature_name.removeprefix("char_freq_")
return f"Относительная частота буквы «{letter}» среди латинских букв."
if group == "pos" and feature_name.startswith("pos_upos_"):
tag = feature_name.removeprefix("pos_upos_")
return f"Доля универсального частеречного тега {tag}."
if group == "pos" and feature_name.startswith("pos_ptb_"):
tag = feature_name.removeprefix("pos_ptb_")
return f"Доля тега Penn Treebank {tag}."
if group == "lexical" and feature_name.startswith("lex_word_length_"):
size = feature_name.removeprefix("lex_word_length_")
return f"Доля слов длины {size} (в символах)."
descriptions = {
"length": "Признак длины или структуры реплики.",
"lexical": "Лексический признак длины слова или орфографии.",
"vocabulary": "Признак словарного богатства; нестабилен на коротких текстах.",
"characters": "Символьный признак: регистр, цифры, пунктуация, буквы.",
"punctuation": "Признак пунктуационного оформления.",
"function_words": "Признак функциональных слов.",
"pronouns": "Признак местоимений и референции.",
"pos": "Частеречный признак.",
"syntax": "Синтаксический признак-прокси.",
"readability": "Метрика читаемости; на коротких репликах приблизительна.",
"dialogue": "Признак разговорной речи и игровых реплик.",
}
return descriptions.get(group, "Стилометрический признак.")
def feature_dictionary(feature_names: Iterable[str]) -> pd.DataFrame:
"""Строит таблицу с описанием признаков для отчёта."""
rows = []
for name in feature_names:
group = feature_group(name)
rows.append(
{
"feature": name,
"group": group,
"description": describe_feature(name),
"status": "exploratory" if group in EXPLORATORY_GROUPS else "core",
}
)
return pd.DataFrame(rows)