Исправлен .gitignore: пакет stylometry/features добавлен в репозиторий
This commit is contained in:
192
lab1/stylometry/features/stylometry.py
Normal file
192
lab1/stylometry/features/stylometry.py
Normal 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)
|
||||
Reference in New Issue
Block a user