Estilometría¶
Medidas de estilometría y de atribución de autoría: distancias entre textos por las frecuencias de las palabras más frecuentes (la Delta de Burrows y sus variantes, como en stylo) y a lo largo de un texto por sus ventanas (la Delta móvil), marcadores de palabras preferidas y evitadas (Zeta) y sus proporciones en los segmentos de los corpus, la distancia ji cuadrado de Kilgarriff entre corpus y la curva de Mendenhall. Las funciones trabajan con listas de unidades de un texto: formas de palabra en minúsculas (la elección habitual para Delta), lemas o N-gramas de caracteres; las mayúsculas y minúsculas y la lematización corresponden a los extractores.
Las funciones son las del núcleo anyTS, con el perfil de las palabras funcionales del español añadido como rasgo de un autor. Las unidades de un texto las dan WordsExtractor - formas de palabra y lemas - y CharNgramsExtractor.
Delta de Burrows¶
frequency_table(corpus, n_mfw=100, culling=0.0)
Construye la tabla de frecuencias relativas de un corpus, un diccionario «nombre de un texto → unidades»: las filas son los textos, las columnas las n_mfw unidades más frecuentes por frecuencia relativa media descendente (alfabéticamente si empatan); culling conserva las unidades que aparecen al menos en la proporción de textos dada, como en stylo.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
corpus |
dict[str, list[str]] |
- | Unidades de los textos por los nombres de los textos |
n_mfw |
int |
100 |
Número de las unidades más frecuentes; None - todas |
culling |
float |
0.0 |
Menor proporción de textos en la que aparece una unidad |
Un corpus que no es un mapeo de listas de cadenas lanza SourceTypeError; un corpus vacío, un texto sin unidades y un filtrado que no deja ninguna unidad lanzan SourceError, y un n_mfw que no es un entero de al menos uno y un culling que no es un número dentro de [0, 1], ParameterError.
z_scores(table)
Estandariza las columnas de una tabla de frecuencias con la desviación típica muestral, como scale() de R; una columna con la misma frecuencia en todos los textos da ceros.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
table |
DataFrame |
- | Tabla de frecuencias relativas |
delta(corpus, n_mfw=100, variant='burrows', culling=0.0)
Calcula a partir de las puntuaciones z una matriz simétrica de distancias (un DataFrame con los nombres de los textos); hacen falta al menos tres textos: con dos, las puntuaciones z degeneran en ±1/√2 y las distancias no dependen de las frecuencias.
Las variantes (anyts.constants.DELTA_VARIANTS), con las fórmulas de las fuentes de stylo; \(n\) es el número de unidades, \(z_A\) y \(z_B\) los vectores de puntuaciones z de los textos:
| Variante | Clave | Fórmula | Fuente |
|---|---|---|---|
| Delta de Burrows | burrows |
\(\frac{1}{n} \sum_i \lvert z_{A,i} - z_{B,i} \rvert\) | Burrows (2002), dist.delta |
| Delta cuadrática | quadratic |
\(\frac{1}{n} \sqrt{\sum_i (z_{A,i} - z_{B,i})^2}\) | Argamon (2008), dist.argamon |
| Delta de Eder | eder |
\(\sum_i \frac{n - i + 2}{n} \lvert z_{A,i} - z_{B,i} \rvert\), \(i\) - rango de la unidad por frecuencia | Eder, dist.eder |
| Delta coseno | cosine |
\(1 - \frac{z_A \cdot z_B}{\lVert z_A \rVert \lVert z_B \rVert}\) | Smith y Aldridge (2011), Evert et al. (2015), dist.wurzburg |
La Delta coseno es la que mejor agrupa los textos por autor en los experimentos de Evert et al. El número habitual de unidades es de 100 a 500 palabras más frecuentes, de 100 a 200 para los N-gramas de caracteres.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
corpus |
dict[str, list[str]] |
- | Unidades de los textos por los nombres de los textos |
n_mfw |
int |
100 |
Número de las unidades más frecuentes; None - todas |
variant |
str |
'burrows' |
Variante de Delta de anyts.constants.DELTA_VARIANTS |
culling |
float |
0.0 |
Menor proporción de textos en la que aparece una unidad |
El corpus, n_mfw y culling lanzan los errores de frequency_table; menos de tres textos lanzan SourceError, y una variante desconocida, ParameterError.
delta_profiles(reference, samples, n_mfw=100, variant='burrows', culling=0.0, statistics=None)
Distancias de Delta para la atribución de autoría. Las unidades más frecuentes, el filtrado y las estadísticas de las puntuaciones z se toman de los textos de referencia (los perfiles de los autores) o de un conjunto aparte statistics, como las ventanas de entrenamiento cuando los perfiles son demasiado pocos para estimar la dispersión de las frecuencias; los textos a examinar samples reciben las mismas unidades y el mismo escalado. El resultado son las distancias de los textos examinados (filas) a los de referencia (columnas); la referencia más cercana es el autor presunto. A diferencia de delta, un texto examinado no influye ni en las unidades ni en el escalado, así que su resultado no depende de los demás textos examinados.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
reference |
dict[str, list[str]] |
- | Unidades de los textos de referencia por los nombres de los textos |
samples |
dict[str, list[str]] |
- | Unidades de los textos a examinar por los nombres de los textos |
n_mfw |
int |
100 |
Número de las unidades más frecuentes; None - todas |
variant |
str |
'burrows' |
Variante de Delta de anyts.constants.DELTA_VARIANTS |
culling |
float |
0.0 |
Menor proporción de textos en la que aparece una unidad |
statistics |
dict[str, list[str]] |
None |
Textos de las unidades y de las estadísticas del escalado; None - los textos de referencia |
Los corpus, n_mfw y culling lanzan los errores de frequency_table; menos de tres textos para las estadísticas lanzan SourceError, y una variante desconocida, ParameterError.
from ests import WordsExtractor
from ests.corpus import delta, delta_profiles, frequency_table
texts = {
"A": (
"El gato estaba en la ventana y miraba los pájaros. "
"Los pájaros se fueron y el gato durmió en la ventana."
),
"B": "El perro estaba en el suelo y dormía. Después el perro comió y otra vez dormía en el suelo.",
"C": "Mañana el gato volverá a la ventana y mirará los pájaros, pero el perro dormirá.",
}
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("El gato despertó en la ventana y otra vez miraba los pájaros.")}
delta_profiles(corpus, sample, n_mfw=5).round(3)
el y en perro gato
A 0.095 0.095 0.095 0.000 0.095
B 0.211 0.105 0.105 0.105 0.000
C 0.133 0.067 0.000 0.067 0.067
A B C
A 0.000 1.312 1.110
B 1.312 0.000 1.428
C 1.110 1.428 0.000
A B C
A 0.000 1.637 1.241
B 1.637 0.000 1.594
C 1.241 1.594 0.000
A B C
? 0.249 1.464 0.942
Delta móvil¶
rolling_delta(reference, text, window=5000, step=1000, n_mfw=100, variant='burrows', culling=0.0, statistics=None)
La Delta a lo largo de un texto, a la manera de rolling.delta de stylo: los textos de referencia y el texto a examinar se cortan en ventanas de window unidades cada step unidades, y el resto más corto que una ventana se descarta. Las unidades más frecuentes, el filtrado y las estadísticas de las puntuaciones z se toman de las ventanas de los textos de referencia o de un conjunto aparte statistics, y cada ventana del texto se compara por la variante de Delta con el centroide de las ventanas de cada texto de referencia. El resultado son las distancias de las ventanas (filas, por la posición de su centro en unidades) a las referencias (columnas); donde la referencia más cercana cambia a lo largo del texto, cambia también el estilo: otro autor de una parte de un texto escrito en colaboración, un pasaje tomado de otro texto. A diferencia de stylo, que suma las diferencias de las frecuencias brutas, las distancias son las de delta_profiles sobre puntuaciones z.
Escalados por sus propias ventanas, los centroides de las referencias se equilibran entre sí, y la Delta coseno pierde su sentido: las distancias coseno de una ventana a las referencias promedian 1 (ponderadas por sus ventanas y las longitudes de sus centroides); con dos referencias suman 2 y las líneas son simétricas entre sí, así que un pasaje de un tercer autor parece a medio camino entre ellas. Con pocas referencias, la Delta coseno necesita unas statistics de más textos - otros autores, otras obras; las Deltas de Burrows y de Eder no tienen esa limitación. Con statistics basta una sola referencia.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
reference |
dict[str, list[str]] |
- | Unidades de los textos de referencia por los nombres de los textos |
text |
list[str] |
- | Unidades del texto a examinar |
window |
int |
5000 |
Tamaño de una ventana en unidades |
step |
int |
1000 |
Distancia entre los inicios de las ventanas en unidades |
n_mfw |
int |
100 |
Número de las unidades más frecuentes; None - todas |
variant |
str |
'burrows' |
Variante de Delta de anyts.constants.DELTA_VARIANTS |
culling |
float |
0.0 |
Menor proporción de ventanas en la que aparece una unidad |
statistics |
dict[str, list[str]] |
None |
Textos cuyas ventanas dan las unidades y las estadísticas del escalado; None - los textos de referencia |
Un corpus que no es un mapeo de listas de cadenas y unas unidades del texto que no son una lista de cadenas lanzan SourceTypeError. La falta de textos de referencia, menos de dos sin statistics, un texto sin unidades o más corto que una ventana, menos de tres ventanas de las estadísticas y un filtrado que no deja ninguna unidad lanzan SourceError. Una variante desconocida, un window, un step o un n_mfw que no es un entero de al menos uno, un step mayor que la ventana y un culling que no es un número dentro de [0, 1] lanzan ParameterError.
from ests.corpus import rolling_delta
rolling_delta(corpus, corpus["A"] + corpus["B"], window=6, step=3, n_mfw=10).round(3)
A B C
3 0.830 1.181 0.954
6 0.709 1.166 0.845
9 0.957 1.225 0.942
12 0.799 1.067 0.784
15 0.761 0.687 0.757
18 0.830 1.181 0.954
21 1.048 0.919 0.954
24 1.216 0.610 1.123
27 1.003 0.381 1.087
30 1.221 0.377 0.999
33 0.980 0.425 0.757
36 1.003 0.381 1.087
Zeta¶
zeta(target, comparison, segment_size=2000, top_n=None)
Marcadores de palabras preferidas y evitadas según Burrows (2007) y Craig y Kinney (2009). Cada texto de ambos corpus se divide en segmentos de unas segment_size palabras (la longitud dividida por el tamaño, redondeada al entero más próximo con las mitades hacia arriba, al menos un segmento); \(DP\) es la proporción de segmentos de un corpus con la palabra. Zeta es \(DP_{target} - DP_{comparison}\), de −1 a 1 (zeta.craig en stylo; la Zeta clásica de Craig, \(DP_{target} + (1 - DP_{comparison})\), es mayor en uno); la Zeta logarítmica es \(\log_2 \frac{DP_{target}}{DP_{comparison}}\) (Schöch et al. 2018), con una proporción nula sustituida por medio segmento. La lista va de las palabras que prefiere el corpus objetivo a las que evita.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
target |
list[str] / list[list[str]] |
- | Palabras del corpus objetivo: un texto o una lista de textos |
comparison |
list[str] / list[list[str]] |
- | Palabras del corpus de comparación |
segment_size |
int |
2000 |
Tamaño de un segmento en palabras |
top_n |
int |
None |
Número de palabras desde el principio de la lista; None - todas |
El resultado es una lista de tuplas con nombre ZetaScore(word, dp_target, dp_comparison, zeta, log_zeta) por Zeta descendente, por Zeta logarítmica descendente y alfabéticamente si empatan. Un corpus que no es ni una lista de cadenas ni una lista de tales listas lanza SourceTypeError, y un corpus sin palabras, SourceError; un segment_size o un top_n que no es un entero de al menos uno lanza ParameterError.
from ests.corpus import zeta
zeta(corpus["A"], corpus["B"], segment_size=5, top_n=2)
zeta(corpus["A"], corpus["B"], segment_size=5)[-1]
[ZetaScore(word='gato', dp_target=0.5, dp_comparison=0.0, zeta=0.5, log_zeta=2.0), ZetaScore(word='la', dp_target=0.5, dp_comparison=0.0, zeta=0.5, log_zeta=2.0)]
ZetaScore(word='suelo', dp_target=0.0, dp_comparison=0.5, zeta=-0.5, log_zeta=-2.0)
zeta_segments(corpora, preferred, avoided, segment_size=2000)
Las proporciones de los marcadores de Zeta en los segmentos de los corpus, tal como oppose de stylo sitúa los segmentos: cada texto se divide en segmentos como en zeta, y un segmento recibe las proporciones de las palabras de las listas preferred y avoided presentes en él. Con los marcadores de un corpus objetivo frente a un corpus de comparación, los segmentos del objetivo quedan arriba a la izquierda del plano de las dos proporciones y los de la comparación abajo a la derecha; los segmentos de un texto de autoría disputada muestran a qué grupo se acerca (zeta_scatter_plot).
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
corpora |
dict[str, list[str] / list[list[str]]] |
- | Palabras de los corpus por nombre - un texto o una lista de textos cada uno |
preferred |
Collection[str] |
- | Palabras que prefiere el corpus objetivo - el principio de la lista de zeta |
avoided |
Collection[str] |
- | Palabras que evita el corpus objetivo - el final de la lista |
segment_size |
int |
2000 |
Tamaño de un segmento en palabras |
El resultado es un DataFrame de los segmentos en el orden de los corpus y de sus textos, con las columnas corpus (el nombre del corpus), preferred y avoided (las proporciones de las listas). Unos corpus que no son un mapeo y un corpus o una lista de marcadores que no es una lista de cadenas lanzan SourceTypeError; la falta de corpus, una lista de marcadores vacía y un corpus sin palabras, SourceError, y un segment_size que no es un entero de al menos uno, ParameterError.
from ests.corpus import zeta_segments
scores = zeta(corpus["A"], corpus["B"], 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 A 0.333 0.125
1 A 0.333 0.000
2 A 0.333 0.125
3 A 0.444 0.000
4 B 0.000 0.250
5 B 0.000 0.500
6 B 0.000 0.500
7 B 0.000 0.375
8 C 0.111 0.125
9 C 0.333 0.000
10 C 0.111 0.250
Ji cuadrado de Kilgarriff¶
kilgarriff_chi2(words_a, words_b, n_mfw=500)
La distancia entre dos corpus según Kilgarriff (2001): para las n_mfw palabras más frecuentes del corpus conjunto las frecuencias esperadas en los corpus son proporcionales a sus tamaños, \(\chi^2 = \sum (O - E)^2 / E\) sobre las palabras y ambos corpus. Cuanto mayor es el valor, más difieren los corpus; crece con el tamaño de los corpus, así que solo son comparables los pares de tamaños iguales.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
words_a |
list[str] |
- | Palabras del primer corpus |
words_b |
list[str] |
- | Palabras del segundo corpus |
n_mfw |
int |
500 |
Número de las palabras más frecuentes del corpus conjunto |
Unas palabras que no son una lista de cadenas lanzan SourceTypeError, un corpus sin palabras, SourceError, y un n_mfw que no es un entero de al menos uno, ParameterError.
from ests.corpus import kilgarriff_chi2
round(kilgarriff_chi2(corpus["A"], corpus["B"], n_mfw=5), 3)
3.119
Curva de Mendenhall¶
mendenhall_curve(words)
Las proporciones de las palabras de cada longitud en caracteres por longitud ascendente (Mendenhall 1887), un perfil del autor comparable entre textos sea cual sea su tamaño.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
words |
list[str] |
- | Palabras del texto |
Unas palabras que no son una lista de cadenas lanzan SourceTypeError, y la falta de palabras, SourceError.
mendenhall_distance(words_a, words_b)
La distancia de Jensen-Shannon con base 2 entre las curvas de Mendenhall de dos textos, de 0 (las distribuciones coinciden) a 1.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
words_a |
list[str] |
- | Palabras del primer texto |
words_b |
list[str] |
- | Palabras del segundo texto |
Unas palabras que no son una lista de cadenas lanzan SourceTypeError, y un texto sin palabras, SourceError.
from ests.corpus import mendenhall_curve, mendenhall_distance
{length: round(share, 3) for length, share in mendenhall_curve(corpus["A"]).items()}
round(mendenhall_distance(corpus["A"], corpus["B"]), 3)
{1: 0.095, 2: 0.333, 3: 0.095, 4: 0.095, 6: 0.19, 7: 0.19}
0.415
Perfil de las palabras funcionales¶
function_words_profile(source, nlp=None)
Las proporciones de las adposiciones, las conjunciones coordinantes y subordinantes, las partículas, los pronombres, los determinantes y las interjecciones (ests.constants.FUNCTION_UD_POS: ADP, CCONJ, SCONJ, PART, PRON, DET, INTJ) entre las palabras del texto. Las palabras funcionales no dependen del tema, así que su perfil es un rasgo clásico de autoría desde Mosteller y Wallace (1964).
Las categorías gramaticales son las de la anotación de un Doc que las lleva. Las palabras de una lista o de un Doc sin categorías gramaticales las etiqueta el modelo en su contexto, así que han de pasarse en el orden del texto; la puntuación de la lista ayuda al etiquetado y no se cuenta. El pipeline es el modelo es_core_news_sm o el que se pasa en nlp, sin el analizador sintáctico, el lematizador ni el reconocedor de entidades. En las Universal Dependencies del español la negación no es un adverbio y no una partícula, así que PART es rara.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
source |
list[str] / Doc |
- | Palabras del texto u objeto Doc |
nlp |
Language |
None |
Pipeline para una lista de palabras; None - el modelo por defecto |
Excepciones:
SourceTypeError- unas palabras que no son una lista de cadenas, un pipeline que no es unLanguagede spaCy;SourceError- una fuente sin palabras, un pipeline que no etiqueta las categorías gramaticales (spacy.blank("es")), un carácter sin forma UTF-8;DatasetNotFoundError- el modelo por defecto no está instalado.
from ests.corpus import function_words_profile
{pos: round(share, 3) for pos, share in function_words_profile(corpus["A"]).items()}
{'ADP': 0.095, 'CCONJ': 0.095, 'SCONJ': 0.0, 'PART': 0.0, 'PRON': 0.048, 'DET': 0.286, 'INTJ': 0.0}