Перейти к содержанию

Стилометрия

Меры стилометрии и атрибуции авторства: расстояния между текстами по частотам самых частых слов (дельта Барроуза и ее варианты, как в 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}