Стилометрия¶
Меры стилометрии и атрибуции авторства: расстояния между текстами по частотам самых частых слов (дельта Барроуза и ее варианты, как в stylo) и вдоль текста по его окнам (скользящая дельта), маркеры предпочитаемых и избегаемых слов (Zeta) и их доли в сегментах корпусов, расстояние хи-квадрат Килгарриффа между корпусами и кривая Менденхолла. Функции работают со списками единиц текста: словоформ в нижнем регистре (обычный выбор для дельты), лемм или символьных N-грамм - регистр и лемматизация на стороне экстракторов.
Функции - это функции ядра anyTS, к которым добавлен профиль служебных слов русского языка как признак автора. Единицы текста дают WordsExtractor - словоформы и леммы - и CharNgramsExtractor.
Дельта Барроуза¶
frequency_table(corpus, n_mfw=100, culling=0.0)
Строит таблицу относительных частот корпуса - словаря «имя текста → единицы»: строки - тексты, столбцы - n_mfw самых частых единиц по убыванию средней относительной частоты (при равенстве - по алфавиту); culling оставляет единицы, встречающиеся хотя бы в заданной доле текстов, как в stylo.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
corpus |
dict[str, list[str]] |
- | Единицы текстов по именам текстов |
n_mfw |
int |
100 |
Число самых частых единиц; None - все |
culling |
float |
0.0 |
Наименьшая доля текстов, в которых встречается единица |
Корпус, который не словарь списков строк, дает SourceTypeError; пустой корпус, текст без единиц и отсев, после которого не остается ни одной единицы, - SourceError, а n_mfw, который не целое число не меньше единицы, и culling, который не число в пределах [0, 1], - ParameterError.
z_scores(table)
Стандартизирует столбцы таблицы частот с выборочным стандартным отклонением, как scale() в R; столбец с одинаковой частотой во всех текстах дает нули.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
table |
DataFrame |
- | Таблица относительных частот |
delta(corpus, n_mfw=100, variant='burrows', culling=0.0)
Считает по z-оценкам симметричную матрицу расстояний (DataFrame с именами текстов); нужно не меньше трех текстов - на двух z-оценки вырождаются в ±1/√2 и расстояния не зависят от частот.
Варианты (anyts.constants.DELTA_VARIANTS), формулы по исходникам stylo; \(n\) - число единиц, \(z_A\) и \(z_B\) - векторы z-оценок текстов:
| Вариант | Ключ | Формула | Источник |
|---|---|---|---|
| Дельта Барроуза | burrows |
\(\frac{1}{n} \sum_i \lvert z_{A,i} - z_{B,i} \rvert\) | Burrows (2002), dist.delta |
| Квадратичная дельта | quadratic |
\(\frac{1}{n} \sqrt{\sum_i (z_{A,i} - z_{B,i})^2}\) | Argamon (2008), dist.argamon |
| Дельта Эдера | eder |
\(\sum_i \frac{n - i + 2}{n} \lvert z_{A,i} - z_{B,i} \rvert\), \(i\) - ранг единицы по частоте | Eder, dist.eder |
| Косинусная дельта | cosine |
\(1 - \frac{z_A \cdot z_B}{\lVert z_A \rVert \lVert z_B \rVert}\) | Smith и Aldridge (2011), Evert и др. (2015), dist.wurzburg |
Косинусная дельта лучше всего кластеризует тексты по авторам в экспериментах Evert и др. Число единиц обычно от 100 до 500 самых частых слов, для символьных N-грамм - 100-200.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
corpus |
dict[str, list[str]] |
- | Единицы текстов по именам текстов |
n_mfw |
int |
100 |
Число самых частых единиц; None - все |
variant |
str |
'burrows' |
Вариант дельты из anyts.constants.DELTA_VARIANTS |
culling |
float |
0.0 |
Наименьшая доля текстов, в которых встречается единица |
Корпус, n_mfw и culling дают ошибки frequency_table; меньше трех текстов - SourceError, а неизвестный вариант - ParameterError.
delta_profiles(reference, samples, n_mfw=100, variant='burrows', culling=0.0, statistics=None)
Расстояния по дельте для атрибуции авторства. Самые частые единицы, отсев и статистики z-оценок берутся из эталонных текстов (профилей авторов) или из отдельного набора statistics, например из обучающих окон, когда профилей слишком мало, чтобы оценить разброс частот; проверяемые тексты samples получают те же единицы и нормировку. Результат - расстояния от проверяемых текстов (строки) до эталонных (столбцы); ближайший эталон - предполагаемый автор. В отличие от delta, проверяемый текст не влияет ни на единицы, ни на нормировку, так что его результат не зависит от других проверяемых текстов.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
reference |
dict[str, list[str]] |
- | Единицы эталонных текстов по именам текстов |
samples |
dict[str, list[str]] |
- | Единицы проверяемых текстов по именам текстов |
n_mfw |
int |
100 |
Число самых частых единиц; None - все |
variant |
str |
'burrows' |
Вариант дельты из anyts.constants.DELTA_VARIANTS |
culling |
float |
0.0 |
Наименьшая доля текстов, в которых встречается единица |
statistics |
dict[str, list[str]] |
None |
Тексты для единиц и статистик нормировки; None - эталонные тексты |
Корпуса, n_mfw и culling дают ошибки frequency_table; меньше трех текстов для статистик - SourceError, а неизвестный вариант - ParameterError.
from ruts import WordsExtractor
from ruts.corpus import delta, delta_profiles, frequency_table
texts = {
"А": "Кот сидел на окне и смотрел на птиц. Птицы улетели, и кот уснул на окне.",
"Б": "Собака лежала на полу и дремала. Потом собака ела и снова дремала на полу.",
"В": "Завтра кот снова будет сидеть на окне и смотреть на птиц, а собака будет дремать.",
}
we = WordsExtractor(lowercase=True)
corpus = {name: we.extract(text) for name, text in texts.items()}
frequency_table(corpus, n_mfw=5).round(3)
delta(corpus, n_mfw=5).round(3)
delta(corpus, n_mfw=5, variant="cosine").round(3)
sample = {"?": we.extract("Кот проснулся на окне и снова смотрел на птиц.")}
delta_profiles(corpus, sample, n_mfw=5).round(3)
на и собака кот окне
А 0.200 0.133 0.000 0.133 0.133
Б 0.143 0.143 0.143 0.000 0.000
В 0.133 0.067 0.067 0.067 0.067
А Б В
А 0.000 1.563 1.277
Б 1.563 0.000 1.033
В 1.277 1.033 0.000
А Б В
А 0.000 1.782 1.452
Б 1.782 0.000 1.202
В 1.452 1.202 0.000
А Б В
? 0.364 1.66 1.16
Скользящая дельта¶
rolling_delta(reference, text, window=5000, step=1000, n_mfw=100, variant='burrows', culling=0.0, statistics=None)
Дельта вдоль текста, как rolling.delta в stylo: эталонные тексты и проверяемый текст режутся на окна по window единиц через каждые step единиц, остаток короче окна отбрасывается. Самые частые единицы, отсев и статистики z-оценок берутся из окон эталонных текстов или отдельного набора statistics, и каждое окно текста сравнивается вариантом дельты с центроидом окон каждого эталонного текста. Результат - расстояния от окон (строки, по позиции их середины в единицах) до эталонов (столбцы); где вдоль текста меняется ближайший эталон, там меняется и стиль - другой автор части текста, написанного в соавторстве, заимствованный фрагмент. В отличие от stylo, где складываются разности исходных частот, расстояния считаются по z-оценкам, как в delta_profiles.
При нормировке по собственным окнам центроиды эталонов уравновешивают друг друга, и косинусная дельта теряет смысл: косинусные расстояния окна до эталонов в среднем (с весами по числу их окон и длинам их центроидов) равны 1, при двух эталонах их сумма равна 2 и линии зеркальны, так что фрагмент третьего автора выглядит лежащим посередине между ними. При немногих эталонах косинусной дельте нужны статистики statistics по большему числу текстов - других авторов, других произведений; дельта Барроуза и дельта Эдера так не ограничены. С statistics достаточно одного эталона.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
reference |
dict[str, list[str]] |
- | Единицы эталонных текстов по именам текстов |
text |
list[str] |
- | Единицы проверяемого текста |
window |
int |
5000 |
Размер окна в единицах |
step |
int |
1000 |
Расстояние между началами окон в единицах |
n_mfw |
int |
100 |
Число самых частых единиц; None - все |
variant |
str |
'burrows' |
Вариант дельты из anyts.constants.DELTA_VARIANTS |
culling |
float |
0.0 |
Наименьшая доля окон, в которых встречается единица |
statistics |
dict[str, list[str]] |
None |
Тексты, окна которых дают единицы и статистики нормировки; None - эталонные тексты |
Корпус, который не словарь списков строк, и единицы текста, которые не список строк, дают SourceTypeError. Отсутствие эталонных текстов, меньше двух эталонных текстов без statistics, текст без единиц или короче окна, меньше трех окон для статистик и отсев, после которого не остается ни одной единицы, дают SourceError. Неизвестный вариант, window, step или n_mfw, который не целое число не меньше единицы, step больше окна и culling, который не число в пределах [0, 1], дают ParameterError.
from ruts.corpus import rolling_delta
rolling_delta(corpus, corpus["А"] + corpus["Б"], window=6, step=3, n_mfw=10).round(3)
А Б В
3 0.197 1.132 0.801
6 0.296 1.132 0.702
9 0.292 1.138 0.801
12 0.197 1.132 0.801
15 0.988 1.230 1.048
18 0.804 0.330 1.015
21 1.298 0.165 1.509
24 1.298 0.165 1.509
Zeta¶
zeta(target, comparison, segment_size=2000, top_n=None)
Маркеры предпочитаемых и избегаемых слов по Burrows (2007) и Craig и Kinney (2009). Каждый текст обоих корпусов делится на сегменты примерно по segment_size слов (отношение длины к размеру, округленное до целого с половиной вверх, не меньше одного сегмента); \(DP\) - доля сегментов корпуса, в которых встречается слово. Zeta - \(DP_{target} - DP_{comparison}\), от −1 до 1 (zeta.craig в stylo; классическая Zeta Крейга, \(DP_{target} + (1 - DP_{comparison})\), больше на единицу); логарифмическая Zeta - \(\log_2 \frac{DP_{target}}{DP_{comparison}}\) (Schöch и др. 2018), нулевая доля заменяется на половину сегмента. Список идет от слов, которые предпочитает целевой корпус, к избегаемым.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
target |
list[str] / list[list[str]] |
- | Слова целевого корпуса - один текст или список текстов |
comparison |
list[str] / list[list[str]] |
- | Слова корпуса сравнения |
segment_size |
int |
2000 |
Размер сегмента в словах |
top_n |
int |
None |
Количество слов с начала списка; None - все |
Результат - список именованных кортежей ZetaScore(word, dp_target, dp_comparison, zeta, log_zeta) по убыванию Zeta, при равенстве - по убыванию логарифмической Zeta и по алфавиту. Корпус, который не список строк и не список таких списков, дает SourceTypeError, а корпус без слов - SourceError; segment_size или top_n, который не целое число не меньше единицы, - ParameterError.
from ruts.corpus import zeta
zeta(corpus["А"], corpus["Б"], segment_size=5, top_n=2)
zeta(corpus["А"], corpus["Б"], segment_size=5)[-1]
[ZetaScore(word='кот', dp_target=0.6666666666666666, dp_comparison=0.0, zeta=0.6666666666666666, log_zeta=2.0), ZetaScore(word='окне', dp_target=0.6666666666666666, dp_comparison=0.0, zeta=0.6666666666666666, log_zeta=2.0)]
ZetaScore(word='собака', dp_target=0.0, dp_comparison=0.6666666666666666, zeta=-0.6666666666666666, log_zeta=-2.0)
zeta_segments(corpora, preferred, avoided, segment_size=2000)
Доли маркеров Zeta в сегментах корпусов - так сегменты располагает oppose в stylo: каждый текст делится на сегменты, как в zeta, и для сегмента считается, какая доля слов списков preferred и avoided в нем встречается. С маркерами целевого корпуса против корпуса сравнения сегменты целевого корпуса лежат на плоскости двух долей слева вверху, а сегменты корпуса сравнения - справа внизу; сегменты спорного текста показывают, к какой группе он ближе (zeta_scatter_plot).
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
corpora |
dict[str, list[str] / list[list[str]]] |
- | Слова корпусов по именам - у каждого один текст или список текстов |
preferred |
Collection[str] |
- | Слова, которые предпочитает целевой корпус, - начало списка zeta |
avoided |
Collection[str] |
- | Слова, которых избегает целевой корпус, - конец списка |
segment_size |
int |
2000 |
Размер сегмента в словах |
Результат - DataFrame сегментов в порядке корпусов и их текстов со столбцами corpus (имя корпуса), preferred и avoided (доли списков). Корпуса, которые не словарь, и корпус или список маркеров, который не список строк, дают SourceTypeError; отсутствие корпусов, пустой список маркеров и корпус без слов - SourceError, а segment_size, который не целое число не меньше единицы, - ParameterError.
from ruts.corpus import zeta_segments
scores = zeta(corpus["А"], corpus["Б"], segment_size=5)
preferred = [score.word for score in scores if score.zeta > 0]
avoided = [score.word for score in scores if score.zeta < 0]
zeta_segments(corpus, preferred, avoided, segment_size=5).round(3)
corpus preferred avoided
0 А 0.444 0.000
1 А 0.556 0.000
2 А 0.444 0.000
3 Б 0.111 0.429
4 Б 0.000 0.571
5 Б 0.111 0.429
6 В 0.111 0.143
7 В 0.222 0.000
8 В 0.111 0.143
Хи-квадрат Килгарриффа¶
kilgarriff_chi2(words_a, words_b, n_mfw=500)
Расстояние между двумя корпусами по Kilgarriff (2001): для n_mfw самых частых слов объединенного корпуса ожидаемые частоты в корпусах пропорциональны их объемам, \(\chi^2 = \sum (O - E)^2 / E\) по словам и обоим корпусам. Чем больше значение, тем сильнее корпуса различаются; величина растет с объемом корпусов, поэтому сравнимы только пары равных объемов.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
words_a |
list[str] |
- | Слова первого корпуса |
words_b |
list[str] |
- | Слова второго корпуса |
n_mfw |
int |
500 |
Число самых частых слов объединенного корпуса |
Слова, которые не список строк, дают SourceTypeError, корпус без слов - SourceError, а n_mfw, который не целое число не меньше единицы, - ParameterError.
from ruts.corpus import kilgarriff_chi2
round(kilgarriff_chi2(corpus["А"], corpus["Б"], n_mfw=5), 3)
6.018
Кривая Менденхолла¶
mendenhall_curve(words)
Доли слов каждой длины в символах по возрастанию длины (Mendenhall 1887) - профиль автора, сравнимый между текстами независимо от их объема.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
words |
list[str] |
- | Слова текста |
Слова, которые не список строк, дают SourceTypeError, а отсутствие слов - SourceError.
mendenhall_distance(words_a, words_b)
Расстояние Йенсена-Шеннона с основанием 2 между кривыми Менденхолла двух текстов, от 0 (распределения совпадают) до 1.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
words_a |
list[str] |
- | Слова первого текста |
words_b |
list[str] |
- | Слова второго текста |
Слова, которые не список строк, дают SourceTypeError, а текст без слов - SourceError.
from ruts.corpus import mendenhall_curve, mendenhall_distance
{length: round(share, 3) for length, share in mendenhall_curve(corpus["А"]).items()}
round(mendenhall_distance(corpus["А"], corpus["Б"]), 3)
{1: 0.133, 2: 0.2, 3: 0.133, 4: 0.2, 5: 0.2, 7: 0.133}
0.353
Профиль служебных слов¶
function_words_profile(source)
Доли предлогов, сочинительных и подчинительных союзов, частиц, местоимений, детерминативов и междометий (ruts.constants.FUNCTION_UD_POS: ADP, CCONJ, SCONJ, PART, PRON, DET, INTJ) среди слов текста - по первому разбору ruMorph для списка слов и по разметке для Doc с частями речи; знаки препинания не считаются. Служебные слова не зависят от темы, поэтому их профиль - классический признак авторства (школа Марусенко).
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
source |
list[str] / Doc |
- | Слова текста или объект Doc |
Исключения:
SourceTypeError- источник, который не список строк и неDoc;SourceError- источник без слов.
from ruts.corpus import function_words_profile
{pos: round(share, 3) for pos, share in function_words_profile(corpus["А"]).items()}
{'ADP': 0.2, 'CCONJ': 0.133, 'SCONJ': 0.0, 'PART': 0.0, 'PRON': 0.0, 'DET': 0.0, 'INTJ': 0.0}