Corpus comparison¶
compare_corpora(a, b, window=1000, features=None, labels=('A', 'B'), n_bootstrap=1000, seed=0, min_words=None)
Compares two corpora of Russian texts by every feature of a text at once: which statistics tell authors, genres, translations, human and generated texts apart - and by how much. For single words keyness does the same, for the distances between texts - delta.
The texts are cut into windows of window words by split_windows, the features of every window come from text_features or a function of one's own, and compare_features of the anyTS core compares the tables of the two corpora.
Chekhov versus Tolstoy over the prose of the RussianLiterature dataset: 77 and 42 works, 249 and 1422 windows of 1000 words, about a minute.
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.loc["sents_mean", ["n_Чехов", "n_Толстой", "n_texts_Чехов", "n_texts_Толстой"]].to_dict()
median_Чехов median_Толстой ci_low ci_high cohen_d cliff_delta auc p_holm
cohesion_content_overlap_2_sent 0.067 0.109 -0.047 -0.035 -1.067 -0.837 0.081 0.0
cohesion_noun_overlap_2_sent 0.081 0.141 -0.069 -0.050 -1.077 -0.759 0.121 0.0
cohesion_content_overlap_prop_all 0.015 0.027 -0.014 -0.009 -0.531 -0.751 0.125 0.0
cohesion_content_overlap_adjacent 0.190 0.342 -0.189 -0.126 -1.254 -0.717 0.141 0.0
punct_ellipsis 15.000 2.000 6.000 25.000 1.868 0.706 0.853 0.0
cohesion_content_overlap_all 0.089 0.181 -0.120 -0.069 -1.081 -0.706 0.147 0.0
cohesion_noun_overlap_adjacent 0.106 0.218 -0.138 -0.087 -1.049 -0.668 0.166 0.0
cohesion_noun_overlap_all 0.045 0.095 -0.071 -0.035 -0.846 -0.658 0.171 0.0
punct_exclamation 14.000 4.000 7.000 14.000 1.671 0.657 0.829 0.0
cohesion_content_overlap_prop_adjacent 0.037 0.060 -0.026 -0.018 -0.838 -0.643 0.178 0.0
{'n_Чехов': 249, 'n_Толстой': 1422, 'n_texts_Чехов': 57, 'n_texts_Толстой': 41}
Cohesion sets the corpora apart most: Tolstoy repeats nouns and content words in adjacent and following sentences more often, and Chekhov has several times more ellipses and exclamations per 1000 words. The overlaps in adjacent and in all pairs of sentences partly repeat the sentence length (the Spearman ρ of cohesion_content_overlap_all with sents_mean over the windows is 0.90 for Chekhov and 0.75 for Tolstoy), while the share of the lemmas repeated in the next two sentences (cohesion_content_overlap_2_sent) repeats it only weakly (0.23 and 0.31). Twenty works of Chekhov and one of Tolstoy are shorter than a window, so the windows come from 57 and 41 works.
The p-values take the 1671 windows for independent (see the warning below): 111 of the 147 features have a corrected p-value below 0.01, but only 22 show a large effect by Cliff's delta. The interval of the difference of the medians resamples whole works and is the safer guide: it excludes zero for 81 of those 111 features, and for the length of a sentence it spans −5.2 to −1.6 words, where the windows alone would give −4.1 to −2.8.
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 |
min_words |
int |
None |
Smallest number of words in a window, at most window; None - a whole window, one word with window=None |
labels, n_bootstrap and seed go on to compare_features and are checked before any text is processed.
Raises:
SourceTypeError- texts that are not a list of strings,featuresthat cannot be called;SourceError- a corpus without a window ofmin_wordswords or more;ParameterError- awindowormin_wordsthat is not an integer of at least one,min_wordsabovewindow, labels, a number of samples or a seed thatcheck_comparison_paramsrejects.
Features¶
text_features(text, nlp=None)
Returns 147 features of a text prefixed by source, 192 with the pipeline nlp; an undefined feature is nan.
| Prefix | Features | Source |
|---|---|---|
basic_ |
shares of long, complex, simple, mono- and polysyllabic words, letters, spaces and punctuation marks; letters and syllables per word | BasicStats |
readability_ |
all readability formulas and the consensus grade | ReadabilityStats |
diversity_ |
lexical diversity measures, but for Orlov's Z, unstable on a window | DiversityStats |
morph_ |
shares of parts of speech and of single-valued features among the words (morph_pos_NOUN, morph_polarity_Neg) and shares of values within the other features (morph_case_Gen; one share for a two-valued feature, morph_number_Plur) |
MorphStats by ruMorph |
sents_ |
mean sentence length in words, standard deviation, coefficient of variation, autocorrelation of adjacent lengths (sentence_rhythm) |
sentence_lengths |
punct_ |
frequencies of punctuation marks by type per 1000 words and the share of the letter ё | punctuation_profile |
cohesion_ |
the overlap of the sentences, pronouns, givenness, temporal cohesion and connectors, without the overlaps between paragraphs (*_para) |
CohesionStats by ruMorph |
syntax_ |
all syntactic statistics of the parse, only with the pipeline nlp; the leaves, the subtrees, the longest dependency and the depth of the tree nearly repeat sents_mean |
SyntaxStats |
A 1000-word window takes about 0.04 s. The shares of spaces, letters and punctuation marks (basic_p_spaces, basic_p_letters, basic_p_punctuations) count the characters as they are, with the indents and double spaces of the edition; in a corpus from different sources collapse them beforehand, for example re.sub(r"[^\S\n]+", " ", text).
| Parameter | Type | Default | Description |
|---|---|---|---|
text |
str |
- | Text string |
nlp |
Language |
None |
spaCy pipeline with a parser and parts of speech for the syntax_ features; None - without them |
Raises:
SourceTypeError- a text that is not a string, annlpthat is not a spaCyLanguageobject;SourceError- a text without words, a text longer than themax_lengthof the pipeline, a pipeline without a parser or parts of speech.
corpus_features(texts, window=1000, features=text_features, min_words=None)
Returns the feature matrix of the windows indexed by (text number, window number), for your own classifiers as well; texts without words and windows shorter than min_words are skipped. compare_corpora is corpus_features for each corpus followed by compare_features; apart they compute the features once to compare several pairs of corpora, for example all authors pairwise.
| Parameter | Type | Default | Description |
|---|---|---|---|
texts |
list[str] |
- | Texts of the corpus |
window |
int |
1000 |
Window size in words; None - whole texts |
features |
Callable |
text_features |
Text feature function |
min_words |
int |
None |
Smallest number of words in a window, at most window; None - a whole window, one word with window=None |
Raises:
SourceTypeError- texts that are not a list of strings,featuresthat cannot be called;SourceError- a corpus without a window ofmin_wordswords or more;ParameterError- awindowormin_wordsthat is not an integer of at least one,min_wordsabovewindow.
Features of your own go to features as a function of a text, text_features with a pipeline among them; with the parse the same comparison takes about five minutes:
from functools import partial
from ruts import load_pipeline
from ruts.corpus import compare_corpora, text_features
nlp = load_pipeline()
result = compare_corpora(
chekhov,
tolstoy,
window=1000,
features=partial(text_features, nlp=nlp),
labels=("Чехов", "Толстой"),
)
result.filter(like="syntax_", axis=0)[columns].head(5).round(3)
median_Чехов median_Толстой ci_low ci_high cohen_d cliff_delta auc p_holm
syntax_converb_clauses_per_sent 0.101 0.207 -0.138 -0.086 -1.034 -0.608 0.196 0.0
syntax_participle_clauses_per_sent 0.040 0.117 -0.098 -0.056 -0.877 -0.586 0.207 0.0
syntax_nominal_chains_per_sent 0.075 0.164 -0.121 -0.062 -0.819 -0.579 0.210 0.0
syntax_tree_depth 3.023 3.634 -0.906 -0.405 -1.012 -0.541 0.229 0.0
syntax_mean_clause_len 4.965 5.808 -1.257 -0.479 -0.886 -0.534 0.233 0.0
Comparison of feature tables¶
compare_features(table_a, table_b, labels=('A', 'B'), n_bootstrap=1000, seed=0)
Compares two corpora feature by feature. Each corpus is a table of the features of its windows, parts of the texts of about equal size, so the features do not depend on text length. The result has one row per column, by descending absolute Cliff's delta, the features without statistics last. The bootstrap resamples whole texts by the index level text; without it every row is a text of its own. Every feature has a generator of its own, from the seed and the name of the feature, so its interval does not change with the other columns. A column missing from one table gives nan.
| Parameter | Type | Default | Description |
|---|---|---|---|
table_a |
DataFrame |
- | Features of the windows of the first corpus, one row per window |
table_b |
DataFrame |
- | Features of the windows of the second corpus |
labels |
tuple[str, str] |
('A', 'B') |
Names of the corpora for the columns, two strings that give distinct columns |
n_bootstrap |
int |
1000 |
Number of bootstrap samples |
seed |
int / Generator |
0 |
Seed of the random number generator, a non-negative integer, or a numpy Generator; None - a random one |
A table that is not a DataFrame, repeats the name of a column or has a column that is not numeric raises SourceTypeError; the other parameters are checked by check_comparison_params.
check_comparison_params(labels=('A', 'B'), n_bootstrap=1000, seed=0)
Checks the parameters of compare_features before the texts are processed: it raises ParameterError unless the labels are two strings that give distinct columns (("diff", "B") would repeat median_diff), the number of bootstrap samples is an integer of at least one and the seed is None, a non-negative integer or a numpy Generator.
Statistics¶
For a feature with the 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 the 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\) - the pooled standard deviation; 0.2 - a 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 - a negligible effect, < 0.33 - small, < 0.474 - medium, large otherwise (Romano et al. 2006; calc_cliff_delta) |
auc |
the feature as a classifier: the share of the pairs of windows with the value in A greater than in B, ties counted as half; \(\delta = 2 \cdot AUC - 1\), 0.5 - the feature does not tell the corpora apart |
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) |
n_A, n_B |
number of windows with a defined value |
n_texts_A, n_texts_B |
number of texts behind those windows |
The columns are anyts.corpus.COMPARISON_COLUMNS with the suffixes a and b replaced by the labels of the corpora. Cliff's delta and the AUC come from the same U statistic and agree; Cohen's d is sensitive to outliers and non-normality, so it is best read next to the delta.
Windows of one text are not independent
The test and the effect sizes take the windows for independent observations, which the windows of one text are not: with few texts the p-values are too small and reflect the chosen texts as much as the corpora. The bootstrap resamples whole texts (a cluster bootstrap), so its interval covers the spread between texts; it needs at least two texts on each side and is rough with few.
Functions of the statistics¶
The statistics of a row are available one by one from ruts.corpus:
calc_cohen_d(values_a, values_b)
Cohen's d with the pooled sample variances (ddof=1); nan with fewer than two values on a side or without spread.
| Parameter | Type | Default | Description |
|---|---|---|---|
values_a |
list[float] |
- | Values in the first corpus |
values_b |
list[float] |
- | Values in the second corpus |
A value that is neither a number nor a missing one (None, NA) and values that are not a flat list raise SourceTypeError.
calc_cliff_delta(values_a, values_b)
Cliff's delta, the share of the pairs where the first value is greater minus the share where it is smaller; nan for an empty set or an undefined value. Computed by sorting, in \(O((n_A + n_B) \log n_B)\) time and linear memory.
| Parameter | Type | Default | Description |
|---|---|---|---|
values_a |
list[float] |
- | Values in the first corpus |
values_b |
list[float] |
- | Values in the second corpus |
A value that is neither a number nor a missing one (None, NA) and values that are not a flat list raise SourceTypeError.
bootstrap_median_diff(values_a, values_b, n_bootstrap=1000, rng=None, confidence=0.95, texts_a=None, texts_b=None)
The percentile bootstrap interval of the difference of the medians. With texts_a and texts_b whole texts are resampled, the median of a draw taken over the values of its texts; with fewer than two texts on a side the interval is nan.
| Parameter | Type | Default | Description |
|---|---|---|---|
values_a |
list[float] |
- | Values in the first corpus |
values_b |
list[float] |
- | Values in the second corpus |
n_bootstrap |
int |
1000 |
Number of bootstrap samples |
rng |
Generator |
None |
Random number generator of numpy; None - a new one without a seed |
confidence |
float |
0.95 |
Confidence level |
texts_a |
list |
None |
Texts of the values in the first corpus; None - every value is resampled on its own |
texts_b |
list |
None |
Texts of the values in the second corpus |
A value that is neither a number nor a missing one, values that are not a flat list and texts that are not labels of one type raise SourceTypeError; an n_bootstrap that is not an integer of at least one, a confidence that is not a number in (0, 1), texts not as many as the values and an rng that is not a numpy Generator raise ParameterError.
holm_correction(p_values)
Holm's correction for multiple comparisons: the p-values are sorted in ascending order, the i-th is multiplied by (m − i + 1), where m is the number of defined values, then the running maximum is taken and capped at one; nan stays nan.
| Parameter | Type | Default | Description |
|---|---|---|---|
p_values |
list[float] |
- | p-values |
A p-value that is neither a number nor a missing one and p-values that are not a flat list raise SourceTypeError, and a p-value outside [0, 1] ParameterError.
from ruts.corpus import calc_cliff_delta, calc_cohen_d, holm_correction
round(calc_cohen_d([2, 4, 6, 8], [1, 3, 5, 7]), 3)
calc_cliff_delta([2, 4, 6, 8], [1, 3, 5, 7])
holm_correction([0.01, 0.04, 0.03]).round(3).tolist()
0.387
0.25
[0.03, 0.06, 0.06]
Windows of a text¶
split_windows(text, window=1000, min_words=None)
Cuts a text into consecutive windows of exactly window words, the words of WordsExtractor, so the features that depend on length are comparable between texts. The rest is a window only with at least min_words words (by default a whole window, so it is dropped). A boundary goes before the first word of a window and the quotes, brackets and dashes that open it, so the punctuation stays with its phrase.
| Parameter | Type | Default | Description |
|---|---|---|---|
text |
str |
- | Text string |
window |
int |
1000 |
Size of a window in words; None - the whole text |
min_words |
int |
None |
Smallest number of words in a window; None - a whole window, or one word without a window |
A text without words or shorter than min_words gives an empty list.
Raises:
SourceTypeError- a text that is not a string;ParameterError- awindowormin_wordsthat is not an integer of at least one,min_wordsabove the window.
from ruts.corpus import split_windows
split_windows("Раз два три четыре пять", 2, min_words=1)
split_windows("Он сказал: «Иди домой». Потом он вышел из комнаты", 4)
['Раз два', 'три четыре', 'пять']
['Он сказал: «Иди домой».', 'Потом он вышел из']
Rhythm of the sentences¶
sentence_rhythm(lengths)
Gives the rhythm of the sentences from their lengths in words: the mean sents_mean, the standard deviation sents_std, the coefficient of variation sents_cv and the lag-1 autocorrelation of neighbouring lengths sents_autocorr (Yule, 1939). The deviation needs two sentences, the autocorrelation three of unequal lengths, the coefficient of variation a mean above zero; otherwise, and without sentences, a feature is nan.
| Parameter | Type | Default | Description |
|---|---|---|---|
lengths |
list[int] |
- | Lengths of the sentences in words |
Raises:
SourceTypeError- lengths that are not a list of integers;ParameterError- a negative length.
from ruts.corpus import sentence_rhythm
{key: round(value, 3) for key, value in sentence_rhythm([4, 8, 2, 7]).items()}
{'sents_mean': 5.25, 'sents_std': 2.754, 'sents_cv': 0.525, 'sents_autocorr': -0.794}