Skip to content

Corpus comparison

ruts.corpus.compare_corpora(), ruts.corpus.compare_features(), ruts.corpus.corpus_features(), ruts.corpus.text_features(), ruts.corpus.split_windows(), ruts.corpus.sentence_rhythm()

Description

Comparison of two corpora across all text features at once: which statistics distinguish authors, genres, translations, human and generated texts - and by how much. For individual words keyness does the same, for distances between texts - delta.

The texts of both corpora are cut into windows of equal size (split_windows: the number of windows is the rounded ratio of the number of words to the window size, at least one, the parts are equal; a boundary goes before the opening marks of the first word of a window - dashes, quotation marks, brackets - so that punctuation is not lost) to remove the dependence of the features on text length. For every window the features are computed (text_features or your own function), for every feature the two sets of values are compared. The result is a DataFrame feature × statistics sorted by descending absolute Cliff's delta.

Features

text_features(text) returns 130 features prefixed by source:

Prefix Features Source
basic_ shares of unique, long, complex, simple, mono- and polysyllabic words, letters, spaces and punctuation marks; letters and syllables per word, words per sentence BasicStats
readability_ all readability formulas, consensus grade, reading time ReadabilityStats
diversity_ all lexical diversity measures DiversityStats
morph_ shares of parts of speech among the words (morph_pos_NOUN) and shares of values within each feature (morph_case_Gen, morph_tense_Past) MorphStats by pymorphy3
sents_ mean sentence length in words, standard deviation, coefficient of variation, autocorrelation of adjacent lengths (sentence_rhythm) - the rhythm of the text sentence_lengths
punct_ frequencies of punctuation marks by type per 1000 words and the share of the letter ё punctuation_profile

A 1000-word window takes about 0.1 s. Features over the spaCy parse (SyntaxStats, CohesionStats) are not in the default set - the function works on strings; they can be added with your own feature function via features, see the example below. corpus_features(texts, window, features) returns the feature matrix of the windows indexed by (text number, window number) - for your own classifiers, and compare_features(table_a, table_b, labels, n_bootstrap, seed) compares two such tables: compare_corpora is corpus_features for each corpus followed by compare_features. The split is needed when the features are computed once for several corpora and pairs have to be compared, for example all authors pairwise.

The shares of spaces, letters and punctuation marks (basic_p_spaces, basic_p_letters, basic_p_punctuations) count characters as they are: line indents, double and non-breaking spaces in the files reflect the typesetting of the edition, not the text. In a corpus from different sources collapse them beforehand, for example re.sub(r"[^\S\n]+", " ", text).

Statistics

For a feature with values \(x_1 \dots x_{n_A}\) in corpus A and \(y_1 \dots y_{n_B}\) in corpus B (undefined and infinite values dropped; with fewer than two values on a side - nan):

Column Description
mean_A, mean_B, median_A, median_B means and medians
median_diff, ci_low, ci_high the difference of medians and its 95% percentile bootstrap interval: both sets are resampled n_bootstrap times (bootstrap_median_diff)
cohen_d \(d = (\bar{x} - \bar{y}) / s\), \(s\) - pooled standard deviation; 0.2 - small effect, 0.5 - medium, 0.8 - large (calc_cohen_d)
cliff_delta \(\delta = P(x > y) - P(x < y)\) from −1 to 1; \(\lvert\delta\rvert\) < 0.147 - negligible effect, < 0.33 - small, < 0.474 - medium, otherwise large (Romano et al. 2006; calc_cliff_delta)
auc the feature as a single-feature classifier: the share of window pairs where the value in A is larger than in B, ties count as half; \(\delta = 2 \cdot AUC - 1\), 0.5 - the feature does not distinguish the corpora
u, p_value the Mann-Whitney U statistic and the two-sided p-value (scipy.stats.mannwhitneyu)
p_holm the p-value with Holm's correction for the number of features (holm_correction): a table of a hundred rows without correction invites false discoveries
n_A, n_B number of windows with a defined value

Cliff's delta and AUC are computed from the same U statistic and are therefore consistent with each other; Cohen's d is sensitive to outliers and non-normality, so it is best read next to delta.

Parameters

Parameter Type Default Description
a list[str] - Texts of the first corpus
b list[str] - Texts of the second corpus
window int 1000 Window size in words; None - whole texts
features callable None Text feature function; None - text_features
labels tuple[str, str] ("A", "B") Corpus names for the columns
n_bootstrap int 1000 Number of bootstrap samples
seed int 0 Random number generator seed; None - random

Usage example

Chekhov versus Tolstoy over the prose of the RussianLiterature dataset: 77 and 42 works, 292 and 1444 windows of 1000 words, about two minutes.

Example

Code:

from ruts.corpus import compare_corpora
from ruts.datasets import RussianLiterature

rl = RussianLiterature()
chekhov = list(rl.get_texts(genre="prose", author="Чехов"))
tolstoy = list(rl.get_texts(genre="prose", author="Толстой"))

result = compare_corpora(chekhov, tolstoy, window=1000, labels=("Чехов", "Толстой"))
columns = [
    "median_Чехов",
    "median_Толстой",
    "ci_low",
    "ci_high",
    "cohen_d",
    "cliff_delta",
    "auc",
    "p_holm",
]
result[columns].head(10).round(3)

Result:

                               median_Чехов  median_Толстой  ci_low  ci_high  cohen_d  cliff_delta    auc  p_holm
punct_ellipsis                       15.842           2.000  10.940   18.013    1.861        0.719  0.860     0.0
punct_exclamation                    14.881           3.996   9.145   12.378    1.704        0.670  0.835     0.0
morph_verb_form_Fin                   0.761           0.695   0.056    0.076    1.161        0.594  0.797     0.0
punct_yo_share                        0.007           0.000   0.007    0.008    1.065        0.557  0.778     0.0
readability_gunning_fog_index         5.995           7.970  -2.283   -1.681   -0.961       -0.540  0.230     0.0
readability_matskovsky_index          8.600          11.232  -3.148   -2.324   -0.947       -0.531  0.235     0.0
basic_words_per_sent                 11.122          15.136  -4.595   -3.400   -0.943       -0.531  0.235     0.0
sents_mean                           11.122          15.136  -4.636   -3.417   -0.943       -0.531  0.235     0.0
readability_dale_chall_index          5.187           6.676  -1.776   -1.229   -0.930       -0.527  0.237     0.0
readability_smog_index                5.711           7.294  -1.903   -1.322   -0.895       -0.516  0.242     0.0

Chekhov has several times more ellipses and exclamations per 1000 words, shorter sentences and a higher share of finite verb forms; Tolstoy is harder by every readability formula. Of the 130 features, 100 have a corrected p-value below 0.01, but only 14 show a large effect by Cliff's delta - with thousands of windows significance is cheap, the effect size matters more.

Your own features, for example syntactic ones by spaCy, are passed as a function:

Example

import spacy
from ruts import SyntaxStats
from ruts.corpus import compare_corpora, text_features

nlp = spacy.load("ru_core_news_sm")


def features(text):
    stats = SyntaxStats(nlp(text)).get_stats()
    return {**text_features(text), **{f"syntax_{key}": value for key, value in stats.items()}}


compare_corpora(chekhov, tolstoy, window=1000, features=features, labels=("Чехов", "Толстой"))