pyests 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ests/__init__.py +56 -0
- ests/basic_stats.py +329 -0
- ests/constants.py +383 -0
- ests/diversity_stats.py +1582 -0
- ests/exceptions.py +83 -0
- ests/extractors.py +377 -0
- ests/py.typed +0 -0
- ests/readability_stats.py +749 -0
- ests/syllables.py +343 -0
- ests/utils.py +266 -0
- pyests-0.1.0.dist-info/METADATA +395 -0
- pyests-0.1.0.dist-info/RECORD +14 -0
- pyests-0.1.0.dist-info/WHEEL +4 -0
- pyests-0.1.0.dist-info/licenses/LICENSE.txt +17 -0
ests/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Spanish Texts Statistics (esTS)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026
|
|
4
|
+
# Author: Sergey Shkarin <kouki.sergey@gmail.com>
|
|
5
|
+
# URL: <https://github.com/SergeyShk/esTS>
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
|
|
10
|
+
from .basic_stats import BasicStats
|
|
11
|
+
from .diversity_stats import DiversityStats
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
DataFileError,
|
|
14
|
+
DatasetNotFoundError,
|
|
15
|
+
DownloadError,
|
|
16
|
+
EstsError,
|
|
17
|
+
ParameterError,
|
|
18
|
+
SourceError,
|
|
19
|
+
SourceTypeError,
|
|
20
|
+
UnknownStatError,
|
|
21
|
+
)
|
|
22
|
+
from .extractors import CharNgramsExtractor, SentsExtractor, WordsExtractor
|
|
23
|
+
from .readability_stats import ReadabilityStats
|
|
24
|
+
|
|
25
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
26
|
+
|
|
27
|
+
# Metadata
|
|
28
|
+
|
|
29
|
+
# The distribution is pyests, the package it installs is ests
|
|
30
|
+
try:
|
|
31
|
+
__version__ = version("pyests")
|
|
32
|
+
except PackageNotFoundError:
|
|
33
|
+
__version__ = "0.0.0"
|
|
34
|
+
__description__ = (
|
|
35
|
+
"A library for extracting statistics from texts in Spanish. Requires Python 3.11+"
|
|
36
|
+
)
|
|
37
|
+
__author__ = "Sergey Shkarin"
|
|
38
|
+
__author_email__ = "kouki.sergey@gmail.com"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"BasicStats",
|
|
42
|
+
"CharNgramsExtractor",
|
|
43
|
+
"DataFileError",
|
|
44
|
+
"DatasetNotFoundError",
|
|
45
|
+
"DiversityStats",
|
|
46
|
+
"DownloadError",
|
|
47
|
+
"EstsError",
|
|
48
|
+
"ParameterError",
|
|
49
|
+
"ReadabilityStats",
|
|
50
|
+
"SentsExtractor",
|
|
51
|
+
"SourceError",
|
|
52
|
+
"SourceTypeError",
|
|
53
|
+
"UnknownStatError",
|
|
54
|
+
"WordsExtractor",
|
|
55
|
+
"__version__",
|
|
56
|
+
]
|
ests/basic_stats.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import unicodedata
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from spacy.tokens import Doc, Span
|
|
8
|
+
|
|
9
|
+
from .constants import (
|
|
10
|
+
BASIC_STATS_DESC,
|
|
11
|
+
COMPLEX_SYL_FACTOR,
|
|
12
|
+
LONG_WORD_LETTER_FACTOR,
|
|
13
|
+
PUNCTUATION_TYPES,
|
|
14
|
+
PUNCTUATIONS,
|
|
15
|
+
SPACES,
|
|
16
|
+
)
|
|
17
|
+
from .exceptions import SourceError, SourceTypeError
|
|
18
|
+
from .extractors import SentsExtractor, WordsExtractor
|
|
19
|
+
from .syllables import count_syllables
|
|
20
|
+
from .utils import count_letters, iter_doc_words
|
|
21
|
+
|
|
22
|
+
ELLIPSIS_PATTERN = re.compile(r"…|\.{3,}|(?<=[?!])\.{2}")
|
|
23
|
+
# A hyphen after whitespace or at the start of a line, or before a space, is
|
|
24
|
+
# a dash, the way the raya is typed in plain-text corpora (-Hola -dijo Juan);
|
|
25
|
+
# a hyphen before a digit is a sign, a hyphen at the end of a line inside
|
|
26
|
+
# a word (pala-\nbra) is a hyphen
|
|
27
|
+
DASH_PATTERN = re.compile(r"(?:(?<=\s)|^)-(?!\d)|-(?=[ \t]|\Z)", re.MULTILINE)
|
|
28
|
+
_DELETE_SPACES = str.maketrans("", "", "".join(SPACES))
|
|
29
|
+
PUNCTUATION_CHARS = {
|
|
30
|
+
",": "comma",
|
|
31
|
+
".": "period",
|
|
32
|
+
"?": "question",
|
|
33
|
+
"¿": "question",
|
|
34
|
+
"!": "exclamation",
|
|
35
|
+
"¡": "exclamation",
|
|
36
|
+
":": "colon",
|
|
37
|
+
";": "semicolon",
|
|
38
|
+
"—": "dash",
|
|
39
|
+
"–": "dash",
|
|
40
|
+
"-": "hyphen",
|
|
41
|
+
"«": "angle_quotes",
|
|
42
|
+
"»": "angle_quotes",
|
|
43
|
+
'"': "straight_quotes",
|
|
44
|
+
"“": "straight_quotes",
|
|
45
|
+
"”": "straight_quotes",
|
|
46
|
+
"‘": "straight_quotes",
|
|
47
|
+
"’": "straight_quotes",
|
|
48
|
+
"(": "parentheses",
|
|
49
|
+
")": "parentheses",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BasicStats:
|
|
54
|
+
"""
|
|
55
|
+
Class for computing the basic statistics of a text
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> from ests import BasicStats
|
|
59
|
+
>>> text = "Hay tres clases de mentiras: mentiras, malditas mentiras y estadísticas"
|
|
60
|
+
>>> bs = BasicStats(text)
|
|
61
|
+
>>> bs.get_stats()
|
|
62
|
+
{'c_letters': {1: 1, 2: 1, 3: 1, 4: 1, 6: 1, 8: 4, 12: 1},
|
|
63
|
+
'c_syllables': {1: 4, 2: 1, 3: 4, 5: 1},
|
|
64
|
+
'n_sents': 1,
|
|
65
|
+
'n_words': 10,
|
|
66
|
+
'n_unique_words': 8,
|
|
67
|
+
'n_long_words': 5,
|
|
68
|
+
'n_complex_words': 5,
|
|
69
|
+
'n_simple_words': 5,
|
|
70
|
+
'n_monosyllable_words': 4,
|
|
71
|
+
'n_polysyllable_words': 6,
|
|
72
|
+
'n_chars': 71,
|
|
73
|
+
'n_letters': 60,
|
|
74
|
+
'n_spaces': 9,
|
|
75
|
+
'n_syllables': 23,
|
|
76
|
+
'n_punctuations': 2,
|
|
77
|
+
'c_punctuations': {'comma': 1,
|
|
78
|
+
'period': 0,
|
|
79
|
+
'question': 0,
|
|
80
|
+
'exclamation': 0,
|
|
81
|
+
'ellipsis': 0,
|
|
82
|
+
'colon': 1,
|
|
83
|
+
'semicolon': 0,
|
|
84
|
+
'dash': 0,
|
|
85
|
+
'hyphen': 0,
|
|
86
|
+
'angle_quotes': 0,
|
|
87
|
+
'straight_quotes': 0,
|
|
88
|
+
'parentheses': 0,
|
|
89
|
+
'other': 0}}
|
|
90
|
+
|
|
91
|
+
Arguments:
|
|
92
|
+
source (str|Doc): Data source (a string or a Doc object); for a Doc the
|
|
93
|
+
words come from the tokens and the sentences from the annotation,
|
|
94
|
+
without sentence boundaries they come from sents_extractor
|
|
95
|
+
sents_extractor (SentsExtractor): Sentence extraction tool; an extractor
|
|
96
|
+
passed explicitly is used whatever the source, on the text of a Doc
|
|
97
|
+
words_extractor (WordsExtractor): Word extraction tool; an extractor
|
|
98
|
+
passed explicitly is used whatever the source, on the text of a Doc
|
|
99
|
+
normalize (bool): Compute the normalized statistics
|
|
100
|
+
complex_syl_factor (int): Minimum number of syllables in a complex word
|
|
101
|
+
long_word_letter_factor (int): Minimum number of letters in a long word
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
c_letters (dict[int, int]): Distribution of words by number of letters
|
|
105
|
+
c_syllables (dict[int, int]): Distribution of words by number of syllables
|
|
106
|
+
n_sents (int): Number of sentences
|
|
107
|
+
n_words (int): Number of words
|
|
108
|
+
n_unique_words (int): Number of unique words
|
|
109
|
+
n_long_words (int): Number of long words
|
|
110
|
+
n_complex_words (int): Number of complex words
|
|
111
|
+
n_simple_words (int): Number of simple words
|
|
112
|
+
n_monosyllable_words (int): Number of monosyllabic words
|
|
113
|
+
n_polysyllable_words (int): Number of polysyllabic words
|
|
114
|
+
n_chars (int): Number of characters
|
|
115
|
+
n_letters (int): Number of letters
|
|
116
|
+
n_spaces (int): Number of spaces
|
|
117
|
+
n_syllables (int): Number of syllables
|
|
118
|
+
n_punctuations (int): Number of punctuation marks
|
|
119
|
+
c_punctuations (dict[str, int]): Distribution of punctuation marks by type
|
|
120
|
+
p_unique_words (float): Normalized number of unique words
|
|
121
|
+
p_long_words (float): Normalized number of long words
|
|
122
|
+
p_complex_words (float): Normalized number of complex words
|
|
123
|
+
p_simple_words (float): Normalized number of simple words
|
|
124
|
+
p_monosyllable_words (float): Normalized number of monosyllabic words
|
|
125
|
+
p_polysyllable_words (float): Normalized number of polysyllabic words
|
|
126
|
+
p_letters (float): Normalized number of letters
|
|
127
|
+
p_spaces (float): Normalized number of spaces
|
|
128
|
+
p_punctuations (float): Normalized number of punctuation marks
|
|
129
|
+
|
|
130
|
+
Methods:
|
|
131
|
+
get_stats: Getting the computed statistics of the text
|
|
132
|
+
print_stats: Printing the computed statistics of the text with descriptions
|
|
133
|
+
count_words_by_syllables: Number of words with at least the given number of syllables
|
|
134
|
+
count_words_by_letters: Number of words with at least the given number of letters
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
SourceTypeError: If the source is neither a string nor a Doc object
|
|
138
|
+
SourceError: If the source has no words
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
source: str | Doc,
|
|
144
|
+
sents_extractor: SentsExtractor | None = None,
|
|
145
|
+
words_extractor: WordsExtractor | None = None,
|
|
146
|
+
normalize: bool = False,
|
|
147
|
+
complex_syl_factor: int = COMPLEX_SYL_FACTOR,
|
|
148
|
+
long_word_letter_factor: int = LONG_WORD_LETTER_FACTOR,
|
|
149
|
+
):
|
|
150
|
+
sents: Iterable[Span] | Iterable[str]
|
|
151
|
+
if isinstance(source, Doc):
|
|
152
|
+
text = source.text
|
|
153
|
+
if sents_extractor is not None:
|
|
154
|
+
sents = sents_extractor.extract(text)
|
|
155
|
+
elif source.has_annotation("SENT_START"):
|
|
156
|
+
sents = source.sents
|
|
157
|
+
else:
|
|
158
|
+
sents = SentsExtractor().extract(text)
|
|
159
|
+
if words_extractor is not None:
|
|
160
|
+
words = words_extractor.extract(text)
|
|
161
|
+
else:
|
|
162
|
+
words = tuple(word for _, _, word in iter_doc_words(source))
|
|
163
|
+
elif isinstance(source, str):
|
|
164
|
+
text = source
|
|
165
|
+
sents = (sents_extractor or SentsExtractor()).extract(text)
|
|
166
|
+
words = (words_extractor or WordsExtractor()).extract(text)
|
|
167
|
+
else:
|
|
168
|
+
raise SourceTypeError("The data source is set incorrectly")
|
|
169
|
+
if not words:
|
|
170
|
+
raise SourceError("The data source has no words")
|
|
171
|
+
|
|
172
|
+
letters_per_word = tuple(count_letters(word) for word in words)
|
|
173
|
+
syllables_per_word = tuple(count_syllables(word) for word in words)
|
|
174
|
+
self.c_letters = dict(sorted(Counter(letters_per_word).items()))
|
|
175
|
+
self.c_syllables = dict(sorted(Counter(syllables_per_word).items()))
|
|
176
|
+
self.n_sents = sum(1 for sent in sents)
|
|
177
|
+
self.n_words = len(words)
|
|
178
|
+
self.n_unique_words = len({word.lower() for word in words})
|
|
179
|
+
self.n_long_words = self.count_words_by_letters(long_word_letter_factor)
|
|
180
|
+
self.n_complex_words = self.count_words_by_syllables(complex_syl_factor)
|
|
181
|
+
self.n_simple_words = sum(
|
|
182
|
+
count for spw, count in self.c_syllables.items() if complex_syl_factor > spw > 0
|
|
183
|
+
)
|
|
184
|
+
self.n_monosyllable_words = self.c_syllables.get(1, 0)
|
|
185
|
+
self.n_polysyllable_words = (
|
|
186
|
+
self.n_words - self.c_syllables.get(1, 0) - self.c_syllables.get(0, 0)
|
|
187
|
+
)
|
|
188
|
+
self.n_chars = len(text) - text.count("\n") - text.count("\r")
|
|
189
|
+
self.n_letters = sum(map(str.isalpha, text))
|
|
190
|
+
self.n_spaces = len(text) - len(text.translate(_DELETE_SPACES))
|
|
191
|
+
self.n_syllables = sum(syllables_per_word)
|
|
192
|
+
punctuations = count_punctuations(text)
|
|
193
|
+
self.n_punctuations = sum(punctuations.values())
|
|
194
|
+
self.c_punctuations = punctuations
|
|
195
|
+
|
|
196
|
+
if normalize:
|
|
197
|
+
self.p_unique_words = self.n_unique_words / self.n_words
|
|
198
|
+
self.p_long_words = self.n_long_words / self.n_words
|
|
199
|
+
self.p_complex_words = self.n_complex_words / self.n_words
|
|
200
|
+
self.p_simple_words = self.n_simple_words / self.n_words
|
|
201
|
+
self.p_monosyllable_words = self.n_monosyllable_words / self.n_words
|
|
202
|
+
self.p_polysyllable_words = self.n_polysyllable_words / self.n_words
|
|
203
|
+
self.p_letters = self.n_letters / self.n_chars
|
|
204
|
+
self.p_spaces = self.n_spaces / self.n_chars
|
|
205
|
+
self.p_punctuations = self.n_punctuations / self.n_chars
|
|
206
|
+
|
|
207
|
+
def count_words_by_syllables(self, min_syllables: int) -> int:
|
|
208
|
+
"""
|
|
209
|
+
Getting the number of words with at least the given number of syllables
|
|
210
|
+
|
|
211
|
+
Arguments:
|
|
212
|
+
min_syllables (int): Minimum number of syllables in a word
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
int: Number of words
|
|
216
|
+
"""
|
|
217
|
+
return sum(count for spw, count in self.c_syllables.items() if spw >= min_syllables)
|
|
218
|
+
|
|
219
|
+
def count_words_by_letters(self, min_letters: int) -> int:
|
|
220
|
+
"""
|
|
221
|
+
Getting the number of words with at least the given number of letters
|
|
222
|
+
|
|
223
|
+
Arguments:
|
|
224
|
+
min_letters (int): Minimum number of letters in a word
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
int: Number of words
|
|
228
|
+
"""
|
|
229
|
+
return sum(count for cpw, count in self.c_letters.items() if cpw >= min_letters)
|
|
230
|
+
|
|
231
|
+
def get_stats(self) -> dict[str, Any]:
|
|
232
|
+
"""
|
|
233
|
+
Getting the computed statistics of the text
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
dict[str, Any]: Dictionary of the computed statistics - a copy,
|
|
237
|
+
editing it does not change the object
|
|
238
|
+
"""
|
|
239
|
+
return {
|
|
240
|
+
key: dict(value) if isinstance(value, dict) else value
|
|
241
|
+
for key, value in vars(self).items()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def print_stats(self):
|
|
245
|
+
"""Printing the computed statistics of the text with descriptions"""
|
|
246
|
+
print(f"{'Statistic':^20}|{'Value':^10}")
|
|
247
|
+
print("-" * 30)
|
|
248
|
+
stats = self.get_stats()
|
|
249
|
+
for stat, value in BASIC_STATS_DESC.items():
|
|
250
|
+
print(f"{value:20}|{stats[stat]:^10}")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def count_punctuations(text: str) -> dict[str, int]:
|
|
254
|
+
"""
|
|
255
|
+
Counting punctuation marks by type
|
|
256
|
+
|
|
257
|
+
Description:
|
|
258
|
+
The types of PUNCTUATION_TYPES: commas, periods, question and
|
|
259
|
+
exclamation marks (the inverted ¿ and ¡ included, so "¿Qué?" has two
|
|
260
|
+
question marks), ellipses (the character …, three or more periods,
|
|
261
|
+
or two periods after ? and ! count as one mark whose periods are not
|
|
262
|
+
periods: "¿Quién?.." is a question and an ellipsis), colons,
|
|
263
|
+
semicolons, dashes (— and –, as well as a hyphen after whitespace
|
|
264
|
+
or at the start of a line, or before a space, the way the raya is
|
|
265
|
+
typed in plain-text corpora: "-Hola -dijo Juan", "- Se fueron -
|
|
266
|
+
dijo"), hyphens inside words, before digits and at the end of
|
|
267
|
+
a line inside a word (teórico-práctico, 1990-1995, -5, pala-\nbra),
|
|
268
|
+
guillemets «», straight and curly quotes "“”‘’ of the three
|
|
269
|
+
levels of the orthography, parentheses and the other marks: every
|
|
270
|
+
remaining character of PUNCTUATIONS or of the Unicode categories P
|
|
271
|
+
and S, the same set that is_punctuation removes from the words,
|
|
272
|
+
so that no mark is lost between the words and the types
|
|
273
|
+
|
|
274
|
+
Arguments:
|
|
275
|
+
text (str): Text string
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
dict[str, int]: Number of marks of each type in the order of PUNCTUATION_TYPES
|
|
279
|
+
"""
|
|
280
|
+
counts = dict.fromkeys(PUNCTUATION_TYPES, 0)
|
|
281
|
+
rest, counts["ellipsis"] = ELLIPSIS_PATTERN.subn("", text)
|
|
282
|
+
rest, counts["dash"] = DASH_PATTERN.subn("", rest)
|
|
283
|
+
chars = Counter(rest)
|
|
284
|
+
for char, kind in PUNCTUATION_CHARS.items():
|
|
285
|
+
counts[kind] += chars[char]
|
|
286
|
+
counts["other"] = sum(
|
|
287
|
+
count
|
|
288
|
+
for char, count in chars.items()
|
|
289
|
+
if char not in PUNCTUATION_CHARS
|
|
290
|
+
and (char in PUNCTUATIONS or unicodedata.category(char)[0] in "PS")
|
|
291
|
+
)
|
|
292
|
+
return counts
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def punctuation_profile(text: str, n_words: int | None = None) -> dict[str, float]:
|
|
296
|
+
"""
|
|
297
|
+
Computing the punctuation profile - frequencies of marks by type per 1000 words
|
|
298
|
+
|
|
299
|
+
Description:
|
|
300
|
+
Frequencies of the types of PUNCTUATION_TYPES (count_punctuations)
|
|
301
|
+
per 1000 words and the share of inverted marks among all question
|
|
302
|
+
and exclamation marks (inverted_share): 0.5 when every question and
|
|
303
|
+
exclamation opens with ¿ or ¡ as the orthography requires, lower
|
|
304
|
+
when the writer drops them, as in informal texts and messages.
|
|
305
|
+
The profile is an editorial and stylometric feature: it depends on
|
|
306
|
+
the formatting of the text (typographic quotes and dashes, inverted
|
|
307
|
+
marks) and is easy to fake, so it is best read separately from
|
|
308
|
+
linguistic features
|
|
309
|
+
|
|
310
|
+
Arguments:
|
|
311
|
+
text (str): Text string
|
|
312
|
+
n_words (int): Number of words; if not given, the words are extracted
|
|
313
|
+
with WordsExtractor
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
dict[str, float]: Frequencies of the types per 1000 words and
|
|
317
|
+
inverted_share; nan without words or without question and
|
|
318
|
+
exclamation marks
|
|
319
|
+
"""
|
|
320
|
+
if n_words is None:
|
|
321
|
+
n_words = len(WordsExtractor().extract(text))
|
|
322
|
+
counts = count_punctuations(text)
|
|
323
|
+
profile = {
|
|
324
|
+
kind: count / n_words * 1000 if n_words else float("nan") for kind, count in counts.items()
|
|
325
|
+
}
|
|
326
|
+
n_marks = counts["question"] + counts["exclamation"]
|
|
327
|
+
n_inverted = text.count("¿") + text.count("¡")
|
|
328
|
+
profile["inverted_share"] = n_inverted / n_marks if n_marks else float("nan")
|
|
329
|
+
return profile
|