Saltar a contenido

Comparación de corpus

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

Descripción

Comparación de dos corpus por todos los rasgos de un texto a la vez: qué estadísticas distinguen autores, géneros, traducciones, textos humanos y generados, y en qué medida. Para palabras sueltas hace lo mismo keyness, para las distancias entre textos, delta.

Los textos de ambos corpus se dividen en ventanas de un tamaño parecido para que los rasgos no dependan de la longitud de un texto. En split_windows el número de ventanas es la razón del número de palabras al tamaño de una ventana redondeada al entero más próximo, al menos una, y las partes son iguales: un texto largo da ventanas a pocos puntos porcentuales del tamaño, un texto de una a dos ventanas da ventanas de 750 a 1499 palabras con una ventana de 1000, y un texto más corto que min_words - media ventana por defecto - no da ninguna, para que los textos cortos no enfrenten ventanas de tamaños muy distintos a las demás. Un límite pasa antes de los signos de apertura de la primera palabra de una ventana - rayas, comillas, paréntesis, los signos ¿ y ¡ - para que no se pierda ninguna puntuación, mientras que unas comillas rectas o una raya pegadas al final de la palabra anterior la cierran y se quedan con ella ("cuatro", —dijo Juan—). Los rasgos se calculan para cada ventana (text_features o una función propia), y para cada rasgo se comparan los dos conjuntos de valores. El resultado es un DataFrame rasgo × estadísticas ordenado por el valor absoluto descendente de la delta de Cliff.

Rasgos

text_features(text, nlp=None) devuelve 132 rasgos con un prefijo según su fuente:

Prefijo Rasgos Fuente
basic_ proporciones de palabras largas, complejas, simples, monosílabas y polisílabas, de letras, espacios y signos de puntuación; letras y sílabas por palabra BasicStats
readability_ todas las fórmulas de legibilidad y el grado de consenso ReadabilityStats
diversity_ todas las medidas de diversidad léxica DiversityStats
morph_ proporciones de los valores de cada rasgo morfológico (morph_pos_NOUN, morph_mood_Sub, morph_polarity_Neg) y los marcadores del español (morph_p_gerund, morph_p_mente_adverbs) MorphStats con el modelo de spaCy
sents_ longitud media de una oración en palabras, desviación típica, coeficiente de variación, autocorrelación de las longitudes vecinas (sentence_rhythm): el ritmo del texto SentsExtractor
punct_ frecuencias de los signos de puntuación por tipo por cada 1000 palabras y la proporción de los signos de apertura punctuation_profile

Cada rasgo se cuenta una sola vez. El tiempo de lectura solo sigue el número de palabras de una ventana y se deja fuera; la proporción de palabras únicas repite diversity_ttr, las palabras por oración repiten sents_mean y los marcadores de los modos repiten morph_mood_*, así que también se dejan fuera.

Las categorías gramaticales y los rasgos de un solo valor - polaridad, cortesía, posesivo, reflexivo - son proporciones de todas las palabras (morph_polarity_Neg es la proporción de las negaciones), los demás rasgos, proporciones de las palabras que los llevan (morph_mood_Sub es el subjuntivo entre los modos). Un valor que no aparece en una ventana da 0; un rasgo ausente de la ventana por completo (sin verbos no hay tiempo) da nan. Un valor de varios valores, como el modelo escribe PronType=Int,Rel de que y Case=Acc,Nom de usted, se reparte a partes iguales entre sus partes, así que las proporciones de un rasgo siguen sumando uno.

La morfología la analiza el modelo es_core_news_sm sin su analizador sintáctico, lo que se lleva la mayor parte del tiempo: una ventana de 1000 palabras tarda unos 0.07 s. El análisis de dependencias añadiría un tercio más para un solo marcador, morph_p_ser, que lee de él las cópulas; un pipeline pasado en nlp se ejecuta entero salvo el reconocedor de entidades, así que functools.partial(text_features, nlp=get_nlp()) devuelve el analizador sintáctico y morph_p_ser, y otro modelo entra en la comparación del mismo modo. Un texto más largo que el max_length del pipeline lanza SourceError, así que una novela se compara por ventanas y no entera.

corpus_features(texts, window, features) devuelve la matriz de rasgos de las ventanas con el índice (número del texto, número de la ventana), para clasificadores propios, y compare_features(table_a, table_b, labels, n_bootstrap, seed) compara dos tablas así: compare_corpora es corpus_features para cada corpus seguido de compare_features. La división sirve cuando los rasgos se calculan una vez para varios corpus y hay que comparar pares, por ejemplo, todos los autores dos a dos.

Las proporciones de espacios, letras y signos de puntuación (basic_p_spaces, basic_p_letters, basic_p_punctuations) cuentan los caracteres tal cual: las sangrías, los espacios dobles y los de no separación de los archivos reflejan la composición de una edición y no el texto. En un corpus de fuentes distintas conviene colapsarlos antes, por ejemplo, con re.sub(r"[^\S\n]+", " ", text).

Estadísticas

Para un rasgo con los valores \(x_1 \dots x_{n_A}\) en el corpus A y \(y_1 \dots y_{n_B}\) en el corpus B (sin los valores indefinidos e infinitos; con menos de dos valores en un lado, nan):

Columna Descripción
mean_A, mean_B, median_A, median_B medias y medianas
median_diff, ci_low, ci_high la diferencia de las medianas y su intervalo bootstrap de percentiles al 95%: ambos conjuntos se remuestrean n_bootstrap veces (bootstrap_median_diff)
cohen_d \(d = (\bar{x} - \bar{y}) / s\), \(s\) - la desviación típica combinada; 0.2 - un efecto pequeño, 0.5 - mediano, 0.8 - grande (calc_cohen_d)
cliff_delta \(\delta = P(x > y) - P(x < y)\) de −1 a 1; \(\lvert\delta\rvert\) < 0.147 - un efecto despreciable, < 0.33 - pequeño, < 0.474 - mediano, grande en otro caso (Romano et al. 2006; calc_cliff_delta)
auc el rasgo como clasificador por sí solo: la proporción de pares de ventanas en los que el valor en A es mayor que en B, con los empates a la mitad; \(\delta = 2 \cdot AUC - 1\), 0.5 - el rasgo no distingue los corpus
u, p_value el estadístico U de Mann-Whitney y el valor p bilateral (scipy.stats.mannwhitneyu)
p_holm el valor p con la corrección de Holm por el número de rasgos (holm_correction): una tabla de cien filas sin corrección invita a falsos descubrimientos
n_A, n_B número de ventanas con un valor definido
n_texts_A, n_texts_B número de textos detrás de esas ventanas

La delta de Cliff y el AUC salen del mismo estadístico U y concuerdan entre sí; la d de Cohen es sensible a los valores atípicos y a la falta de normalidad, así que conviene leerla junto a la delta.

Las ventanas de un texto no son independientes

La prueba y los tamaños del efecto toman cada ventana por una observación independiente, y las ventanas de un texto no lo son: comparten su trama, sus personajes, su narrador y su edición. Con pocos textos en un corpus los valores p salen demasiado pequeños y reflejan los textos elegidos tanto como los corpus: en el ejemplo de abajo dos novelas de un mismo autor difieren en 42 rasgos según la misma prueba. El bootstrap, en cambio, remuestrea textos enteros, el nivel text del índice que pone corpus_features (un bootstrap por conglomerados), así que su intervalo tiene en cuenta la dispersión entre los textos; necesita al menos dos textos en cada lado y es aproximado con solo unos pocos. Una tabla propia sin ese nivel toma cada fila por un texto aparte.

Parámetros

Parámetro Tipo Por defecto Descripción
a list[str] - Textos del primer corpus
b list[str] - Textos del segundo corpus
window int 1000 Tamaño de una ventana en palabras; None - los textos enteros
features callable None Función de los rasgos de un texto; None - text_features
labels tuple[str, str] ("A", "B") Nombres de los corpus para las columnas
n_bootstrap int 1000 Número de remuestras bootstrap
seed int 0 Semilla del generador de números aleatorios; None - una aleatoria
min_words int None Menor número de palabras de una ventana; None - media ventana, o una cuando window es None

Ejemplo de uso

Galdós frente a Unamuno, tres novelas de cada uno de Project Gutenberg: Marianela, Misericordia y Torquemada en la hoguera frente a Niebla, Abel Sánchez y La tía Tula: 197 y 118 ventanas de 1000 palabras, menos de medio minuto tras la descarga.

Ejemplo

Código:

from urllib.request import urlopen

from ests.corpus import compare_corpora


def gutenberg(number):
    url = f"https://www.gutenberg.org/cache/epub/{number}/pg{number}.txt"
    text = urlopen(url).read().decode("utf-8")
    start = text.index("\n", text.index("*** START OF"))
    return text[start : text.index("*** END OF")]


galdos = [gutenberg(number) for number in (17340, 21831, 15206)]
unamuno = [gutenberg(number) for number in (49836, 44512, 44358)]

result = compare_corpora(galdos, unamuno, window=1000, labels=("Galdós", "Unamuno"))
columns = [
    "median_Galdós",
    "median_Unamuno",
    "ci_low",
    "ci_high",
    "cohen_d",
    "cliff_delta",
    "auc",
    "p_holm",
]
result[columns].head(5).round(3)

features = [
    "basic_letters_per_word",
    "readability_lix",
    "morph_p_gerund",
    "morph_polarity_Neg",
    "sents_mean",
    "punct_dash",
    "punct_exclamation",
    "diversity_yule_k",
]
result.loc[features, columns].round(3)

result.loc["sents_mean", ["n_Galdós", "n_Unamuno", "n_texts_Galdós", "n_texts_Unamuno"]].to_dict()

Resultado:

                  median_Galdós  median_Unamuno  ci_low  ci_high  cohen_d  cliff_delta    auc  p_holm
diversity_mtldw         106.450          65.982  36.809   46.110    3.338        0.990  0.995     0.0
diversity_mamtld        104.846          64.544  36.425   45.306    3.235        0.986  0.993     0.0
diversity_mattr           0.819           0.769   0.046    0.054    3.196        0.984  0.992     0.0
diversity_mtld          104.833          64.280  36.841   45.884    3.103        0.982  0.991     0.0
diversity_msttr           0.818           0.771   0.046    0.051    2.952        0.975  0.988     0.0

                        median_Galdós  median_Unamuno  ci_low  ci_high  cohen_d  cliff_delta    auc  p_holm
basic_letters_per_word          4.474           4.127   0.264    0.444    1.808        0.811  0.906   0.000
readability_lix                39.707          29.664   7.377   14.943    1.276        0.738  0.869   0.000
morph_p_gerund                  0.069           0.036   0.030    0.035    1.452        0.725  0.862   0.000
morph_polarity_Neg              0.017           0.029  -0.016   -0.010   -1.314       -0.617  0.192   0.000
sents_mean                     17.362          11.438   3.301    8.697    1.002        0.660  0.830   0.000
punct_dash                     14.896          45.682 -45.745  -19.686   -1.461       -0.660  0.170   0.000
punct_exclamation               9.009          23.845 -24.826   -3.270   -1.317       -0.641  0.179   0.000
diversity_yule_k              106.152         112.396 -14.536   -1.802   -0.577       -0.292  0.354   0.001

{'n_Galdós': 197, 'n_Unamuno': 118, 'n_texts_Galdós': 3, 'n_texts_Unamuno': 3}

Galdós tiene el vocabulario más rico: en el 98.5% de los pares de ventanas la suya tiene la mayor proporción de palabras distintas (el AUC de diversity_ttr es 0.985). Las medidas basadas en el número de palabras distintas - el TTR y sus transformaciones, MATTR, MTLD, los hápax - distinguen a los autores con una delta por encima de 0.9, mientras que el índice de Simpson y la K de Yule, que ponderan las palabras frecuentes, lo hacen mucho menos (0.29, un efecto pequeño) y la Vm de Herdan apenas (0.11): la diferencia está sobre todo en el vocabulario raro y no en la repetición de las palabras frecuentes. Sus palabras y oraciones son más largas, y usa casi el doble de gerundios entre las formas verbales. Unamuno escribe en diálogo y en negaciones: tres veces más rayas por cada 1000 palabras, más del doble de signos de exclamación y de interrogación y más negaciones entre las palabras.

De los 132 rasgos, 97 tienen un valor p corregido por debajo de 0.01 y 67 muestran un efecto grande según la delta de Cliff, pero la prueba toma las 315 ventanas por independientes, y salen de seis novelas: según la misma prueba Marianela y Torquemada en la hoguera, dos novelas de Galdós, difieren en 42 rasgos. El intervalo de la diferencia de las medianas remuestrea novelas enteras y es la guía más segura - para la longitud de una oración va de 3.3 a 8.7 palabras, donde las ventanas solas darían de 5.1 a 7.4 -, aunque tres textos por lado también son pocos para un bootstrap, y una comparación de autores pide tantos textos como se puedan reunir.

Los rasgos propios, por ejemplo los sintácticos, se pasan como una función:

Ejemplo

from ests import SyntaxStats
from ests.corpus import compare_corpora, text_features
from ests.utils import get_nlp

nlp = get_nlp()


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(galdos, unamuno, window=1000, features=features, labels=("Galdós", "Unamuno"))