Saltar a contenido

Comparación de corpus

compare_corpora(a, b, window=1000, features=None, labels=('A', 'B'), n_bootstrap=1000, seed=0, min_words=None)

Compara dos corpus de textos en español por todos los rasgos de un texto a la vez: qué estadísticas distinguen los corpus y en qué medida. Para palabras sueltas hace lo mismo keyness, para las distancias entre textos, delta.

Los textos se cortan seguidos en ventanas de exactamente window palabras con split_windows, así que las ventanas de todos los textos tienen la misma longitud; el resto incompleto al final de un texto y un texto más corto que una ventana se descartan salvo que min_words admita ventanas más cortas. Los rasgos de cada ventana los calcula text_features o una función propia, y las tablas de los dos corpus las compara compare_features del núcleo anyTS.

Galdós frente a Unamuno, tres novelas de cada uno del corpus de literatura: Marianela, Misericordia y Torquemada en la hoguera frente a Niebla, Abel Sánchez y La tía Tula.

from ests.corpus import compare_corpora
from ests.datasets import SpanishLiterature

sl = SpanishLiterature()
sl.download()
galdos = [
    record["text"]
    for record in sl.get_records(author="galdos")
    if record["title"] in ("Marianela", "Misericordia", "Torquemada en la hoguera")
]
unamuno = [
    record["text"]
    for record in sl.get_records(author="unamuno")
    if record["title"] in ("Niebla", "Abel Sánchez", "La tía Tula")
]

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()
                  median_Galdós  median_Unamuno  ci_low  ci_high  cohen_d  cliff_delta    auc  p_holm
diversity_mtldw         105.455          66.179  37.104   43.799    3.260        0.988  0.994     0.0
diversity_mamtld        103.316          64.814  35.107   45.657    3.083        0.981  0.990     0.0
diversity_mtld          103.655          63.851  38.546   47.911    3.110        0.981  0.990     0.0
diversity_mattr           0.817           0.769   0.046    0.057    2.952        0.979  0.990     0.0
diversity_msttr           0.819           0.769   0.047    0.059    2.929        0.979  0.989     0.0

                        median_Galdós  median_Unamuno  ci_low  ci_high  cohen_d  cliff_delta    auc  p_holm
basic_letters_per_word          4.441           4.130   0.243    0.358    1.695        0.784  0.892     0.0
readability_lix                37.906          29.721   7.097   11.023    1.084        0.694  0.847     0.0
morph_p_gerund                  0.069           0.037   0.026    0.035    1.533        0.742  0.871     0.0
morph_polarity_Neg              0.019           0.028  -0.011   -0.009   -1.122       -0.559  0.221     0.0
sents_mean                     16.393          11.494   3.078    6.688    0.833        0.610  0.805     0.0
punct_dash                     19.000          46.000 -39.000  -14.000   -1.287       -0.605  0.197     0.0
punct_exclamation               9.000          24.000 -23.000   -2.000   -1.251       -0.637  0.181     0.0
diversity_yule_k              103.620         112.500 -17.620   -2.660   -0.676       -0.351  0.324     0.0

{'n_Galdós': 155, 'n_Unamuno': 117, 'n_texts_Galdós': 3, 'n_texts_Unamuno': 3}

Galdós tiene el vocabulario más rico: 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 cercana a 0.9 o mayor, mientras que la K de Yule, que pondera las palabras frecuentes, da un efecto mediano (0.35), así que la diferencia está sobre todo en el vocabulario raro. Sus palabras y oraciones son más largas, con casi el doble de gerundios entre las formas verbales. Unamuno escribe en diálogo: dos veces y media más rayas y signos de exclamación por cada 1000 palabras, y más negaciones. La cohesión también los distingue (result.filter(like="cohesion_", axis=0)): por cada 1000 palabras Unamuno abre con un conector más del triple de oraciones (cohesion_connectors_inter, 20 frente a 6) - unas 2.4 veces más por oración, ya que sus oraciones son más cortas - y repite más palabras con contenido de una oración en las dos siguientes (0.108 frente a 0.065). Parte de la cohesión sigue la longitud de las oraciones: la ρ de Spearman con sents_mean por ventanas es de 0.76 en Galdós y 0.71 en Unamuno para cohesion_content_overlap_all, la repetición binaria en todos los pares de oraciones, y de -0.49 y -0.61 para los conectores que abren una oración, mientras que la proporción de palabras con contenido repetidas en las dos oraciones siguientes no depende de ella (0.02 y -0.01).

Los valores p toman las 272 ventanas de seis novelas por independientes (véase el aviso de abajo), y según la misma prueba dos novelas de Galdós, Marianela y Misericordia, difieren en 52 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.1 a 6.7 palabras, donde las ventanas solas darían de 3.9 a 6.1.

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
min_words int None Menor número de palabras de una ventana, como mucho window; None - una ventana entera, o una palabra cuando window es None

labels, n_bootstrap y seed pasan a compare_features. compare_corpora los comprueba antes con check_comparison_params, así que un nombre de corpus o una semilla erróneos fallan antes de analizar ningún texto.

Excepciones:

  • SourceTypeError - unos textos que no son una lista de cadenas, unos features que no se pueden llamar o no dan un diccionario de números por nombre;
  • SourceError - un corpus sin una ventana de min_words palabras o más, un texto con un carácter sin forma UTF-8;
  • ParameterError - un window o un min_words que no es un entero de al menos uno, un min_words mayor que window, unas etiquetas, un número de remuestras o una semilla que rechaza check_comparison_params;
  • DatasetNotFoundError - los rasgos por defecto sin el modelo instalado.

Rasgos

text_features(text, nlp=None)

Devuelve 163 rasgos de un texto con un prefijo según su fuente, 209 con un pipeline que tiene analizador sintáctico; un rasgo indefinido es nan.

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 salvo la Z de Orlov, inestable en una ventana 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
cohesion_ repetición de sustantivos, argumentos y palabras con contenido entre oraciones, información dada, cohesión temporal y densidades de los conectores por clase, tipo y lugar, salvo la repetición entre párrafos, que las ventanas cortan CohesionStats según el modelo de spaCy
syntax_ todas las estadísticas sintácticas, con un pipeline que tiene analizador sintáctico SyntaxStats

Se dejan fuera el tiempo de lectura, que solo sigue el número de palabras de una ventana, y los rasgos que repiten otros: la proporción de palabras únicas de BasicStats, que repite diversity_ttr, sus palabras por oración, que repiten sents_mean, y los marcadores de los modos de get_markers, que repiten morph_mood_*.

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 (PronType=Int,Rel de que) se reparte a partes iguales entre sus partes, así que las proporciones de un rasgo siguen sumando uno.

La morfología y la cohesión las analiza el modelo es_core_news_sm sin su analizador sintáctico, así que el marcador morph_p_ser, que lee las cópulas de las dependencias, y el grupo syntax_ se dejan fuera; un pipeline pasado en nlp se ejecuta entero salvo el reconocedor de entidades, así que functools.partial(text_features, nlp=get_nlp()) incorpora el analizador sintáctico, morph_p_ser y la sintaxis, y otro modelo entra en la comparación del mismo modo. El análisis toma las oraciones del grupo sents_ (set_sent_starts del núcleo), así que la cohesión y la sintaxis cuentan las mismas oraciones que el ritmo. Una novela se compara por ventanas y no entera, ya que el pipeline admite como mucho max_length caracteres.

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).

Parámetro Tipo Por defecto Descripción
text str - Cadena de texto
nlp Language None Pipeline para la morfología, la cohesión y, con analizador sintáctico, la sintaxis; None - el modelo por defecto sin su analizador sintáctico

Excepciones:

  • SourceTypeError - un texto que no es una cadena, un pipeline que no es un Language de spaCy;
  • SourceError - un texto sin palabras, con un carácter sin forma UTF-8 o más largo que el max_length del pipeline, un pipeline que no da las categorías gramaticales o los lemas;
  • DatasetNotFoundError - el modelo por defecto no está instalado.
corpus_features(texts, window=1000, features=text_features, min_words=None)

Devuelve la matriz de rasgos de las ventanas con el índice (número del texto, número de la ventana), también para clasificadores propios; el nivel text del índice es el que remuestrea el bootstrap de compare_features. Los textos sin palabras y las ventanas más cortas que min_words se omiten. compare_corpora es corpus_features para cada corpus seguido de compare_features, y la división permite calcular una vez los rasgos de varios corpus y compararlos por pares.

Parámetro Tipo Por defecto Descripción
texts list[str] - Textos del corpus
window int 1000 Tamaño de una ventana en palabras; None - los textos enteros
features Callable text_features Función de los rasgos de un texto
min_words int None Menor número de palabras de una ventana, como mucho window; None - una ventana entera, o una palabra cuando window es None

Excepciones:

  • SourceTypeError - unos textos que no son una lista de cadenas, unos features que no se pueden llamar o no dan un diccionario de números por nombre;
  • SourceError - un corpus sin una ventana de min_words palabras o más, un texto con un carácter sin forma UTF-8;
  • ParameterError - un window o un min_words que no es un entero de al menos uno, un min_words mayor que window;
  • DatasetNotFoundError - los rasgos por defecto sin el modelo instalado.

Los rasgos sintácticos llegan con un pipeline que tiene analizador sintáctico, pasado en una función de los rasgos; los rasgos propios se pasan del mismo modo:

from functools import partial

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

features = partial(text_features, nlp=get_nlp())
result = compare_corpora(
    galdos, unamuno, window=1000, features=features, labels=("Galdós", "Unamuno")
)
result.filter(like="syntax_", axis=0)[columns].head(5).round(3)
                                    median_Galdós  median_Unamuno  ci_low  ci_high  cohen_d  cliff_delta    auc  p_holm
syntax_gerund_clauses_per_sent              0.145           0.045   0.088    0.119    1.460        0.848  0.924     0.0
syntax_complex_nominals_per_clause          0.809           0.550   0.213    0.307    1.158        0.688  0.844     0.0
syntax_tree_depth                           3.727           2.861   0.529    1.057    1.064        0.659  0.830     0.0
syntax_t_units_per_sent                     1.073           1.009   0.028    0.156    1.365        0.657  0.829     0.0
syntax_subtrees_per_sent                    6.742           4.494   1.484    2.921    0.932        0.656  0.828     0.0

Comparación de tablas de rasgos

compare_features(table_a, table_b, labels=('A', 'B'), n_bootstrap=1000, seed=0)

Compara dos corpus rasgo por rasgo. Cada corpus es una tabla de los rasgos de sus ventanas, partes de los textos de un tamaño parecido, así que los rasgos no dependen de la longitud de los textos. El resultado tiene una fila por columna, por el valor absoluto descendente de la delta de Cliff, con los rasgos sin estadísticas al final. El bootstrap remuestrea textos enteros según el nivel text del índice; sin él cada fila es un texto aparte. Cada rasgo tiene un generador propio, a partir de la semilla y del nombre del rasgo, así que su intervalo no cambia con las demás columnas. Una columna que falta en una de las tablas da nan.

Parámetro Tipo Por defecto Descripción
table_a DataFrame - Rasgos de las ventanas del primer corpus, una fila por ventana
table_b DataFrame - Rasgos de las ventanas del segundo corpus
labels tuple[str, str] ('A', 'B') Nombres de los corpus para las columnas, dos cadenas que den columnas distintas
n_bootstrap int 1000 Número de remuestras bootstrap
seed int / Generator 0 Semilla del generador de números aleatorios, un entero no negativo, o un Generator de numpy; None - una aleatoria

Una tabla que no es un DataFrame, que repite el nombre de una columna o que tiene una columna no numérica lanza SourceTypeError; los demás parámetros los comprueba check_comparison_params.

check_comparison_params(labels=('A', 'B'), n_bootstrap=1000, seed=0)

Comprueba los parámetros de compare_features antes de procesar los textos: lanza ParameterError salvo que las etiquetas sean dos cadenas que den columnas distintas (("diff", "B") repetiría median_diff), el número de remuestras bootstrap sea un entero de al menos uno y la semilla sea None, un entero no negativo o un Generator de numpy.

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: la proporción de pares de ventanas con el valor en A 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)
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

Las columnas son anyts.corpus.COMPARISON_COLUMNS con los sufijos a y b sustituidos por las etiquetas de los corpus. La delta de Cliff y el AUC salen del mismo estadístico U y concuerdan; 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 las ventanas por observaciones independientes, cosa que las ventanas de un texto no son: con pocos textos los valores p salen demasiado pequeños y reflejan los textos elegidos tanto como los corpus. El bootstrap remuestrea textos enteros (un bootstrap por conglomerados), así que su intervalo abarca la dispersión entre los textos; necesita al menos dos textos en cada lado y es aproximado con pocos.

Funciones de las estadísticas

Las estadísticas de una fila están disponibles una a una desde ests.corpus:

calc_cohen_d(values_a, values_b)

La d de Cohen con las varianzas muestrales combinadas (ddof=1); nan con menos de dos valores en un lado o sin dispersión.

Parámetro Tipo Por defecto Descripción
values_a list[float] - Valores en el primer corpus
values_b list[float] - Valores en el segundo corpus

Un valor que no es ni un número ni un valor ausente (None, NA) y unos valores que no son una lista plana lanzan SourceTypeError.

calc_cliff_delta(values_a, values_b)

La delta de Cliff, la proporción de pares en que el primer valor es mayor menos la proporción en que es menor; nan para un conjunto vacío o un valor indefinido. Se calcula por ordenación, en tiempo \(O((n_A + n_B) \log n_B)\) y memoria lineal.

Parámetro Tipo Por defecto Descripción
values_a list[float] - Valores en el primer corpus
values_b list[float] - Valores en el segundo corpus

Un valor que no es ni un número ni un valor ausente (None, NA) y unos valores que no son una lista plana lanzan SourceTypeError.

bootstrap_median_diff(values_a, values_b, n_bootstrap=1000, rng=None, confidence=0.95, texts_a=None, texts_b=None)

El intervalo bootstrap de percentiles de la diferencia de las medianas. Con texts_a y texts_b se remuestrean textos enteros, y la mediana de una extracción se toma sobre los valores de sus textos; con menos de dos textos en un lado el intervalo es nan.

Parámetro Tipo Por defecto Descripción
values_a list[float] - Valores en el primer corpus
values_b list[float] - Valores en el segundo corpus
n_bootstrap int 1000 Número de remuestras bootstrap
rng Generator None Generador de números aleatorios de numpy; None - uno nuevo sin semilla
confidence float 0.95 Nivel de confianza
texts_a list None Textos de los valores en el primer corpus; None - cada valor se remuestrea por separado
texts_b list None Textos de los valores en el segundo corpus

Un valor que no es ni un número ni un valor ausente, unos valores que no son una lista plana y unos textos que no son etiquetas de un mismo tipo lanzan SourceTypeError; un n_bootstrap que no es un entero de al menos uno, un confidence que no es un número en (0, 1), unos textos que no son tantos como los valores y un rng que no es un Generator de numpy lanzan ParameterError.

holm_correction(p_values)

La corrección de Holm para comparaciones múltiples: los valores p se ordenan de menor a mayor, el i-ésimo se multiplica por (m − i + 1), donde m es el número de valores definidos, luego se toma el máximo acumulado y se limita a uno; nan sigue siendo nan.

Parámetro Tipo Por defecto Descripción
p_values list[float] - Valores p

Un valor p que no es ni un número ni un valor ausente y unos valores p que no son una lista plana lanzan SourceTypeError, y un valor p fuera de [0, 1], ParameterError.

from ests.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]

Ventanas de un texto

split_windows(text, window=1000, min_words=None)

Corta un texto en ventanas consecutivas de exactamente window palabras, así que los rasgos que dependen de la longitud son comparables entre textos. El resto es una ventana solo con al menos min_words palabras (por defecto una ventana entera, así que se descarta). Un límite pasa antes de la primera palabra de una ventana y de sus signos de apertura; unas comillas rectas o una raya pegadas a la última palabra de la ventana anterior se quedan con ella, y también tras un signo de cierre cuando las sigue un espacio ("cuatro", —dijo Juan—, "adiós."); una raya pegada a la palabra siguiente abre su réplica (cuatro.—¿Cinco?). A las ventanas se les quitan los espacios de los extremos; window=None da el texto entero. Es el split_windows del núcleo con las palabras de WordsExtractor y los signos de ests.constants: las comillas, los paréntesis, las rayas, ¿ y ¡ abren una frase (OPENING_MARKS), las comillas rectas y las rayas son simétricas (SYMMETRIC_MARKS); los signos que cierran una frase son los del núcleo.

Parámetro Tipo Por defecto Descripción
text str - Cadena de texto
window int 1000 Tamaño de una ventana en palabras; None - el texto entero
min_words int None Menor número de palabras de una ventana; None - una ventana entera, o una palabra cuando window es None

Un texto sin palabras o más corto que min_words da una lista vacía.

Excepciones:

  • SourceTypeError - un texto que no es una cadena;
  • SourceError - un carácter sin forma UTF-8;
  • ParameterError - un window o un min_words que no es un entero de al menos uno, un min_words mayor que la ventana.
from ests.corpus import split_windows

split_windows("Uno dos tres cuatro cinco", 2, min_words=1)
split_windows("El gato duerme mucho. ¿Dónde está el perro?", 4)
split_windows('Dijo "adiós" y se fue ya', 2)
['Uno dos', 'tres cuatro', 'cinco']
['El gato duerme mucho.', '¿Dónde está el perro?']
['Dijo "adiós"', 'y se', 'fue ya']

Ritmo de las oraciones

sentence_rhythm(lengths, *, messages=MESSAGES)

Da el ritmo de las oraciones a partir de sus longitudes en palabras: la media sents_mean, la desviación típica sents_std, el coeficiente de variación sents_cv y la autocorrelación de retardo 1 de las longitudes vecinas sents_autocorr (Yule, 1939). La desviación necesita dos oraciones, la autocorrelación tres de longitudes desiguales y el coeficiente de variación una media mayor que cero; si no, y sin oraciones, un rasgo es nan.

Parámetro Tipo Por defecto Descripción
lengths list[int] - Longitudes de las oraciones en palabras
messages dict[str, str] MESSAGES Mensajes de los errores por clave

Unas longitudes que no son una lista de enteros lanzan SourceTypeError, y una longitud negativa, ParameterError.

from ests.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}