pyests 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (45) hide show
  1. {pyests-0.2.0 → pyests-0.3.0}/PKG-INFO +174 -9
  2. {pyests-0.2.0 → pyests-0.3.0}/README.es.md +169 -7
  3. {pyests-0.2.0 → pyests-0.3.0}/README.md +169 -7
  4. {pyests-0.2.0 → pyests-0.3.0}/ests/__init__.py +4 -0
  5. {pyests-0.2.0 → pyests-0.3.0}/ests/basic_stats.py +24 -12
  6. {pyests-0.2.0 → pyests-0.3.0}/ests/components.py +77 -1
  7. {pyests-0.2.0 → pyests-0.3.0}/ests/constants.py +114 -5
  8. pyests-0.3.0/ests/corpus/__init__.py +61 -0
  9. pyests-0.3.0/ests/corpus/collocations.py +319 -0
  10. pyests-0.3.0/ests/corpus/compare.py +787 -0
  11. pyests-0.3.0/ests/corpus/dispersion.py +334 -0
  12. pyests-0.3.0/ests/corpus/keyness.py +432 -0
  13. pyests-0.3.0/ests/corpus/kwic.py +169 -0
  14. pyests-0.3.0/ests/corpus/stylometry.py +635 -0
  15. pyests-0.3.0/ests/datasets/__init__.py +4 -0
  16. pyests-0.3.0/ests/datasets/dataset.py +185 -0
  17. pyests-0.3.0/ests/datasets/freq_dict.py +497 -0
  18. pyests-0.3.0/ests/datasets/spanish_literature.py +352 -0
  19. pyests-0.3.0/ests/exceptions.py +93 -0
  20. pyests-0.3.0/ests/lexical_stats.py +352 -0
  21. pyests-0.3.0/ests/resources/README.md +15 -0
  22. pyests-0.3.0/ests/resources/google_books_top10000.txt +10000 -0
  23. pyests-0.3.0/ests/utils.py +777 -0
  24. pyests-0.3.0/ests/visualizers/__init__.py +25 -0
  25. pyests-0.3.0/ests/visualizers/corpus.py +229 -0
  26. pyests-0.3.0/ests/visualizers/fingerprinting.py +148 -0
  27. pyests-0.3.0/ests/visualizers/sentences.py +113 -0
  28. pyests-0.3.0/ests/visualizers/stylometry.py +182 -0
  29. pyests-0.3.0/ests/visualizers/vocabulary.py +87 -0
  30. pyests-0.3.0/ests/visualizers/word_tree.py +350 -0
  31. pyests-0.3.0/ests/visualizers/zipf.py +128 -0
  32. {pyests-0.2.0 → pyests-0.3.0}/pyproject.toml +9 -2
  33. pyests-0.2.0/ests/exceptions.py +0 -85
  34. pyests-0.2.0/ests/utils.py +0 -384
  35. {pyests-0.2.0 → pyests-0.3.0}/.gitignore +0 -0
  36. {pyests-0.2.0 → pyests-0.3.0}/LICENSE.txt +0 -0
  37. {pyests-0.2.0 → pyests-0.3.0}/ests/cohesion_stats.py +0 -0
  38. {pyests-0.2.0 → pyests-0.3.0}/ests/diversity_stats.py +0 -0
  39. {pyests-0.2.0 → pyests-0.3.0}/ests/extractors.py +0 -0
  40. {pyests-0.2.0 → pyests-0.3.0}/ests/morph_stats.py +0 -0
  41. {pyests-0.2.0 → pyests-0.3.0}/ests/py.typed +0 -0
  42. {pyests-0.2.0 → pyests-0.3.0}/ests/readability_stats.py +0 -0
  43. {pyests-0.2.0 → pyests-0.3.0}/ests/resources/connectors.tsv +0 -0
  44. {pyests-0.2.0 → pyests-0.3.0}/ests/syllables.py +0 -0
  45. {pyests-0.2.0 → pyests-0.3.0}/ests/syntax_stats.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyests
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Spanish Texts Statistics
5
5
  Project-URL: Homepage, https://github.com/SergeyShk/esTS
6
6
  Project-URL: Repository, https://github.com/SergeyShk/esTS
@@ -28,9 +28,12 @@ Classifier: Topic :: Text Processing
28
28
  Classifier: Topic :: Text Processing :: Linguistic
29
29
  Classifier: Typing :: Typed
30
30
  Requires-Python: >=3.11
31
+ Requires-Dist: graphviz>=0.20.3
32
+ Requires-Dist: matplotlib>=3.8
31
33
  Requires-Dist: numpy>=1.26
34
+ Requires-Dist: pandas>=2.1
32
35
  Requires-Dist: scipy>=1.11
33
- Requires-Dist: simplemma>=1.0
36
+ Requires-Dist: simplemma<3,>=2.0
34
37
  Requires-Dist: spacy>=3.7
35
38
  Description-Content-Type: text/markdown
36
39
 
@@ -60,13 +63,14 @@ Description-Content-Type: text/markdown
60
63
  <a href="https://github.com/SergeyShk/esTS/actions/workflows/ci.yml"><img src="https://github.com/SergeyShk/esTS/actions/workflows/ci.yml/badge.svg" alt="Build"></a>
61
64
  <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
62
65
  <a href="https://github.com/SergeyShk/esTS/blob/master/LICENSE.txt"><img src="https://img.shields.io/github/license/sergeyshk/esTS.svg" alt="License"></a>
66
+ <a href="https://doi.org/10.5281/zenodo.22924655"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.22924655.svg" alt="DOI"></a>
63
67
  </p>
64
68
 
65
69
  ---
66
70
 
67
- **esTS** computes for Spanish texts what usually requires assembling several separate tools: basic statistics, readability, lexical diversity, morphology, syntax and cohesion - by published formulas with the coefficients and the scales of their authors, and by the parts of speech and the features of Universal Dependencies.
71
+ **esTS** computes for Spanish texts what usually requires assembling several separate tools: basic statistics, readability, lexical diversity, lexical sophistication, morphology, syntax and cohesion - by published formulas with the coefficients and the scales of their authors, and by the parts of speech and the features of Universal Dependencies.
68
72
 
69
- The library works both with raw strings and with `Doc` objects of [spaCy](https://github.com/explosion/spaCy): sentences, words and character N-grams are extracted by rules, syllables and stress follow from the orthography, and only the morphological, the syntactic and the cohesion statistics need a trained model.
73
+ The library works both with raw strings and with `Doc` objects of [spaCy](https://github.com/explosion/spaCy): sentences, words and character N-grams are extracted by rules, syllables and stress follow from the orthography, and only the morphological, the syntactic, the cohesion and the lexical sophistication statistics, the profile of the function words and the comparison of corpora need a trained model.
70
74
 
71
75
  * **[Object extraction](https://sergeyshk.github.io/esTS/extractors/sentences/)** - configurable sentence, word and character N-gram tokenizers that know the inverted marks, the dialogue dash and the abbreviations of Spanish
72
76
  * **[Syllables and stress](https://sergeyshk.github.io/esTS/syllables/)** - rule-based syllabification and the stressed syllable derived from the spelling, with no dictionary
@@ -74,11 +78,15 @@ The library works both with raw strings and with `Doc` objects of [spaCy](https:
74
78
  * **[Readability metrics](https://sergeyshk.github.io/esTS/stats/readability_stats/)** - Fernández Huerta, Szigriszt-Pazos with the INFLESZ scale, Gutiérrez de Polini, Crawford, Legibilidad µ, SOL, LIX and RIX, with a consensus grade, the school stages of Spain and reading time
75
79
  * **[Lexical diversity metrics](https://sergeyshk.github.io/esTS/stats/diversity_stats/)** - TTR and its variations, MATTR, MSTTR, MTLD, HD-D, Simpson's and Yule's indices, entropy, Zipf's and Heaps' laws
76
80
  * **[Morphological statistics](https://sergeyshk.github.io/esTS/stats/morph_stats/)** - parts of speech and fifteen grammatical features of Universal Dependencies, with the markers of Spanish: the moods, the non-finite forms, `ser` against `estar`, the adverbs in `-mente`
81
+ * **[Corpus measures](https://sergeyshk.github.io/esTS/corpus/keyness/)** - keywords against a reference corpus, collocations, the dispersion of a word over the parts of a text, a KWIC concordance and the stylometry of authorship: Burrows's Delta with its variants, Zeta, the Mendenhall curve, the profile of the function words; the comparison of two corpora by 132 features of a text with effect sizes
82
+ * **[Visualizers](https://sergeyshk.github.io/esTS/visualizers/zipf/)** - Zipf's law, literature fingerprinting, a word tree, lexical dispersion and keywords, a network of collocations, a dendrogram, PCA and MDS by Delta, vocabulary growth, sentence lengths
83
+ * **[Datasets](https://sergeyshk.github.io/esTS/datasets/spanishliterature/)** - Spanish-language literature in the public domain: 150 works by 33 authors from Spain, Latin America and the Philippines in prose, poems, drama and publicism, with the genre, the years and the country; a frequency dictionary of 83,785 lemmas by Google Books Ngram with ipm, range and dispersion
77
84
  * **[spaCy components](https://sergeyshk.github.io/esTS/components/)** - every statistics class as a component of a pipeline, the statistics attached to the `Doc` in one pass
78
85
  * **[Cohesion statistics](https://sergeyshk.github.io/esTS/stats/cohesion_stats/)** - the overlap of nouns, arguments and content words between sentences, givenness and temporal cohesion in the manner of Coh-Metrix, with the density of 255 Spanish discourse markers
86
+ * **[Lexical sophistication statistics](https://sergeyshk.github.io/esTS/stats/lexical_stats/)** - how rare the words of a text are in the language: the frequency, range and dispersion of the lemmas by a dictionary of Google Books Ngram, the frequency bands top-1000 to 10000, surprisal, perplexity and lexical density
79
87
  * **[Syntactic statistics](https://sergeyshk.github.io/esTS/stats/syntax_stats/)** - the dependency tree by distances, depth, clauses and coordination, with the constructions of the administrative style: the passive with `ser` and with `se`, the participial and the gerund clauses, the chains of `de`, the split predicates
80
88
 
81
- Corpus measures and stylometry come in 0.3, style, phonostatistics, metre and rhyme in 0.4.
89
+ Style, phonostatistics, metre and rhyme come in 0.4.
82
90
 
83
91
  ## Installation
84
92
 
@@ -94,12 +102,14 @@ Or with [uv](https://docs.astral.sh/uv/):
94
102
  uv add pyests
95
103
  ```
96
104
 
97
- The distribution on PyPI is `pyests`, the package it installs is `ests`. The basic statistics, the readability and the lexical diversity metrics need no spaCy model; the morphological, the syntactic and the cohesion statistics do, and so does parsing a text yourself to pass the `Doc` instead of a string:
105
+ The distribution on PyPI is `pyests`, the package it installs is `ests`. The basic statistics, the readability and the lexical diversity metrics need no spaCy model; the morphological, the syntactic, the cohesion and the lexical sophistication statistics of a string do, and so do the profile of the function words, the features of a text and the comparison of corpora, and parsing a text yourself to pass the `Doc` instead of a string:
98
106
 
99
107
  ```bash
100
108
  python -m spacy download es_core_news_sm
101
109
  ```
102
110
 
111
+ The statistics by the frequency dictionary need it downloaded once with `FreqDict().download()`. The datasets go to the directory `ests_data` next to the installed package; another one is passed in `data_dir` or set in the environment variable `ESTS_DATA_DIR` before the package is imported.
112
+
103
113
  ## Quick start
104
114
 
105
115
  ```python
@@ -446,6 +456,27 @@ More in the [documentation](https://sergeyshk.github.io/esTS/stats/cohesion_stat
446
456
 
447
457
  </details>
448
458
 
459
+ <details>
460
+ <summary><b>Lexical sophistication statistics</b></summary>
461
+
462
+ <br>
463
+
464
+ How rare the words of a text are in the language, in the manner of TAALES: the mean frequency, range and dispersion of the lemmas by the frequency dictionary of Google Books Ngram, the shares of the words of the frequency bands top-1000, 2000, 5000 and 10000, the surprisal and the perplexity by the unigram model of the dictionary, the lexical density. The bands and the density work out of the box; the statistics by the dictionary need it downloaded once.
465
+
466
+ ```python
467
+ >>> from ests import LexicalStats
468
+ >>> from ests.datasets import FreqDict
469
+
470
+ >>> FreqDict().download()
471
+ >>> ls = LexicalStats("El felinólogo examinaba al minino con parsimonia")
472
+ >>> ls.coverage, ls.p_top1000, round(ls.surprisal, 2)
473
+ (0.8571428571428571, 0.42857142857142855, 13.71)
474
+ ```
475
+
476
+ More in the [documentation](https://sergeyshk.github.io/esTS/stats/lexical_stats/).
477
+
478
+ </details>
479
+
449
480
  <details>
450
481
  <summary><b>spaCy components</b></summary>
451
482
 
@@ -469,12 +500,140 @@ Every statistics class is also a component of a pipeline, so a text is annotated
469
500
  (12, ('DET', 'NOUN'), 2.0)
470
501
  ```
471
502
 
472
- The factories are `ests_basic`, `ests_readability`, `ests_diversity`, `ests_morph`, `ests_syntax` and `ests_cohesion`; the name of the pipe is free and is what the extension is called.
503
+ The factories are `ests_basic`, `ests_readability`, `ests_diversity`, `ests_morph`, `ests_syntax`, `ests_cohesion` and `ests_lexical`; the name of the pipe is free and is what the extension is called.
473
504
 
474
505
  More in the [documentation](https://sergeyshk.github.io/esTS/components/).
475
506
 
476
507
  </details>
477
508
 
509
+ <details>
510
+ <summary><b>Corpus measures</b></summary>
511
+
512
+ <br>
513
+
514
+ The library compares corpora and describes the use of a word, with the measures of corpus linguistics:
515
+
516
+ * keywords of a target corpus against a reference one or against the frequency dictionary of Google Books Ngram: the log-likelihood with its p-value, Log Ratio, chi-square, %DIFF, BIC, ELL and the odds ratio
517
+ * collocations by logDice, MI, MI³, t-score, Dice, log-likelihood, NPMI and minimum sensitivity, checked against NLTK
518
+ * the dispersion of a word over the parts of a text: DP of Gries, normalized DP, Juilland's D, Carroll's D2, Rosengren's S and the Kullback-Leibler divergence
519
+ * a KWIC concordance by word form or by lemma
520
+ * stylometry: the distances between texts by Burrows's Delta and its variants, with the attribution of a text to reference authors, the markers of preferred and avoided words by Zeta, Kilgarriff's chi-square, the Mendenhall curve and the profile of the function words
521
+ * the comparison of two corpora by 132 features of a text over windows of about the same size: Cliff's delta, Cohen's d and the AUC of every feature, the Mann-Whitney test with Holm's correction and a bootstrap interval of the difference of the medians that resamples whole texts
522
+
523
+ ```python
524
+ >>> from ests import WordsExtractor
525
+ >>> from ests.corpus import collocations, keyness, kwic, zeta
526
+
527
+ >>> we = WordsExtractor(use_lexemes=True, lowercase=True)
528
+ >>> target = we.extract("El gato estaba en la ventana y miraba a los pájaros. Los pájaros se fueron y el gato "
529
+ ... "se durmió en la ventana. Mañana el gato volverá a estar en la ventana y mirará a los pájaros.")
530
+ >>> reference = we.extract("El perro estaba en el suelo y dormía. Después el perro comió y volvió a dormir. "
531
+ ... "Mañana el perro saldrá a pasear.")
532
+
533
+ >>> [(k.word, round(k.g2, 2)) for k in keyness(target, reference, top_n=2)]
534
+ [('gato', 2.8), ('pájaro', 2.8)]
535
+
536
+ >>> [(c.left, c.right, round(c.score, 1)) for c in collocations(target, window=2, top_n=2)]
537
+ [('en', 'ventana', 13.0), ('estar', 'en', 12.7)]
538
+
539
+ >>> [line.keyword for line in kwic("Los gatos juegan y el gato duerme.", "gato", by_lemma=True)]
540
+ ['gatos', 'gato']
541
+
542
+ >>> scores = zeta(target, reference, segment_size=10)
543
+ >>> [z.word for z in scores[:2]], [z.word for z in scores[-2:]]
544
+ (['gato', 'ventana'], ['dormir', 'perro'])
545
+ ```
546
+
547
+ Words are compared as they are, so case, lemmas and stop words are chosen at the extraction.
548
+
549
+ More in the [documentation](https://sergeyshk.github.io/esTS/corpus/keyness/).
550
+
551
+ </details>
552
+
553
+ <details>
554
+ <summary><b>Datasets</b></summary>
555
+
556
+ <br>
557
+
558
+ * [spanish_literature](https://sergeyshk.github.io/esTS/datasets/spanishliterature/) - Spanish-language literature in the public domain: 150 works by 33 authors from Spain, Latin America and the Philippines, from Cervantes to the 1920s, in prose, poems, drama and publicism; 65 million characters
559
+ * [freq_dict](https://sergeyshk.github.io/esTS/datasets/freqdict/) - a frequency dictionary of 83,785 Spanish lemmas from the books of Google Books Ngram of 1980-2019 (63 billion words): ipm, range and Juilland's D over the years, the number of books and the part of speech (CC BY 3.0)
560
+
561
+ The texts are cut to the text of the author, without title pages, notes of the transcribers and the editors, tables of contents and footnotes, and come with the genre, the author, the title, the years of the first publication and the country; the records can be filtered by any of them and by the length of the text.
562
+
563
+ ```python
564
+ >>> from ests.datasets import SpanishLiterature
565
+
566
+ >>> sl = SpanishLiterature()
567
+ >>> sl.download()
568
+ >>> for record in sl.get_records(author="galdos", year_from=1880, year_to=1884):
569
+ ... print(record["title"], record["year_from"], len(record["text"]))
570
+ La desheredada 1881 820454
571
+ El amigo Manso 1882 521156
572
+ La de Bringas 1884 413730
573
+ Tormento 1884 477286
574
+ ```
575
+
576
+ The archive (19 MB) is downloaded once by `download()` into the data directory and verified against its SHA-256 checksum; before the download `get_texts()` and `get_records()` raise `DatasetNotFoundError` with a hint.
577
+
578
+ More in the [documentation](https://sergeyshk.github.io/esTS/datasets/spanishliterature/).
579
+
580
+ </details>
581
+
582
+ <details>
583
+ <summary><b>Visualizers</b></summary>
584
+
585
+ <br>
586
+
587
+ * [Zipf's law](https://sergeyshk.github.io/esTS/visualizers/zipf/) with the theoretical curve and the Zipf-Mandelbrot fit
588
+ * [Literature fingerprinting](https://sergeyshk.github.io/esTS/visualizers/fingerprinting/) (Literature Fingerprinting)
589
+ * [Word tree](https://sergeyshk.github.io/esTS/visualizers/word_tree/) (Word Tree)
590
+ * [Corpus plots](https://sergeyshk.github.io/esTS/visualizers/corpus/): lexical dispersion, a chart of keywords, a network of collocations
591
+ * [Stylometric plots](https://sergeyshk.github.io/esTS/visualizers/stylometry/): a dendrogram, the principal components and the multidimensional scaling by Delta, the Mendenhall curves
592
+ * [Vocabulary growth and frequency spectrum](https://sergeyshk.github.io/esTS/visualizers/vocabulary/), [sentence lengths](https://sergeyshk.github.io/esTS/visualizers/sentences/) with a moving average
593
+
594
+ The matplotlib plots take the axes `ax` and return `Axes`, so they can be laid out on one figure; the network of collocations and the word tree are graphs of graphviz, whose executables render them. Cosine Delta separates three novels by Galdós from three by Unamuno:
595
+
596
+ ```python
597
+ import matplotlib.pyplot as plt
598
+ from ests import WordsExtractor
599
+ from ests.corpus import delta
600
+ from ests.datasets import SpanishLiterature
601
+ from ests.visualizers import dendrogram_plot, pca_plot
602
+
603
+ # six novels by Galdós and Unamuno from the corpus of literature
604
+ titles = (
605
+ "Marianela",
606
+ "Misericordia",
607
+ "Torquemada en la hoguera",
608
+ "Niebla",
609
+ "Abel Sánchez",
610
+ "La tía Tula",
611
+ )
612
+ sl = SpanishLiterature()
613
+ sl.download()
614
+ novels = {
615
+ record["title"]: record["text"]
616
+ for author in ("galdos", "unamuno")
617
+ for record in sl.get_records(author=author)
618
+ if record["title"] in titles
619
+ }
620
+ we = WordsExtractor(lowercase=True)
621
+ corpus = {title: we.extract(novels[title]) for title in titles}
622
+ distances = delta(corpus, n_mfw=100, variant="cosine")
623
+
624
+ fig, (left, right) = plt.subplots(1, 2, figsize=(13, 5))
625
+ dendrogram_plot(distances, ax=left)
626
+ pca_plot(corpus, n_mfw=100, ax=right)
627
+ ```
628
+
629
+ <p align="center">
630
+ <img src="https://raw.githubusercontent.com/SergeyShk/esTS/master/docs/img/stylometry.png" alt="Stylometric plots" width="760">
631
+ </p>
632
+
633
+ More in the [documentation](https://sergeyshk.github.io/esTS/visualizers/zipf/).
634
+
635
+ </details>
636
+
478
637
  ## Development
479
638
 
480
639
  The project uses [uv](https://docs.astral.sh/uv/) for dependency management and [ruff](https://docs.astral.sh/ruff/) for linting and formatting.
@@ -492,7 +651,7 @@ Run `make help` for the full list of commands.
492
651
 
493
652
  The documentation is bilingual: English pages are `docs/*.md`, Spanish ones are `docs/*.es.md` next to them ([mkdocs-static-i18n](https://github.com/ultrabug/mkdocs-static-i18n)); when editing a page, update both versions.
494
653
 
495
- The installed version is `ests.__version__`. All exceptions inherit `ests.EstsError` and one of the built-in classes (`SourceError` and `ParameterError` - `ValueError`, `SourceTypeError` - `TypeError`, `UnknownStatError` - `KeyError`), so `except ValueError` keeps working. The library prints nothing on its own: its messages go to the `ests` logger (`logging.getLogger("ests")`) and are silent by default.
654
+ The installed version is `ests.__version__`. All exceptions inherit `ests.EstsError` and one of the built-in classes (`SourceError`, `ParameterError` and `DataFileError` - `ValueError`, `SourceTypeError` - `TypeError`, `UnknownStatError` - `KeyError`, `DatasetNotFoundError` - `OSError`, `DownloadError` - `RuntimeError`), so `except ValueError` keeps working. The library prints nothing on its own: its messages go to the `ests` logger (`logging.getLogger("ests")`) and are silent by default.
496
655
 
497
656
  Before submitting changes, install the hooks that run the linters on commit and the tests on push:
498
657
 
@@ -513,7 +672,10 @@ Bug reports, ideas and pull requests are welcome - [issues](https://github.com/S
513
672
  * **ests**:
514
673
  * basic_stats.py - basic text statistics
515
674
  * cohesion_stats.py - cohesion statistics
675
+ * lexical_stats.py - lexical sophistication statistics
516
676
  * components.py - components of a spaCy pipeline
677
+ * corpus - measures of corpus linguistics: keywords, collocations, dispersion, concordance, stylometry, comparison of corpora
678
+ * datasets - datasets: Spanish-language literature, the frequency dictionary
517
679
  * constants.py - constants of the Spanish language and of the metrics
518
680
  * diversity_stats.py - lexical diversity metrics
519
681
  * exceptions.py - library exceptions
@@ -523,6 +685,8 @@ Bug reports, ideas and pull requests are welcome - [issues](https://github.com/S
523
685
  * syntax_stats.py - syntactic statistics
524
686
  * syllables.py - syllabification and stress
525
687
  * utils.py - helper tools
688
+ * visualizers - plots: Zipf's law, fingerprinting, word tree, corpus and stylometric plots, vocabulary growth, sentence lengths
689
+ * **scripts** - scripts that build the archives of the datasets
526
690
  * **tests** - tests mirroring the package structure
527
691
 
528
692
  </details>
@@ -537,13 +701,14 @@ Bug reports, ideas and pull requests are welcome - [issues](https://github.com/S
537
701
 
538
702
  ## Citation
539
703
 
540
- Please use the following BibTeX entry for citing **esTS** if you use it in your research or software. Citations are helpful for the continued development and maintenance of this library. The same metadata is in [CITATION.cff](https://github.com/SergeyShk/esTS/blob/master/CITATION.cff) - GitHub shows it under the "Cite this repository" button.
704
+ Please use the following BibTeX entry for citing **esTS** if you use it in your research or software. Citations are helpful for the continued development and maintenance of this library. The same metadata is in [CITATION.cff](https://github.com/SergeyShk/esTS/blob/master/CITATION.cff) - GitHub shows it under the "Cite this repository" button. The Concept DOI [10.5281/zenodo.22924655](https://doi.org/10.5281/zenodo.22924655) on Zenodo points at every version of the library; the DOI of a single version is on the page of its release.
541
705
 
542
706
  ```bibtex
543
707
  @software{esTS,
544
708
  author = {Sergey Shkarin},
545
709
  title = {{esTS, a library for statistics extraction from texts in Spanish}},
546
710
  year = 2026,
711
+ doi = {10.5281/zenodo.22924655},
547
712
  url = {https://github.com/SergeyShk/esTS}
548
713
  }
549
714
  ```
@@ -24,13 +24,14 @@
24
24
  <a href="https://github.com/SergeyShk/esTS/actions/workflows/ci.yml"><img src="https://github.com/SergeyShk/esTS/actions/workflows/ci.yml/badge.svg" alt="Build"></a>
25
25
  <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
26
26
  <a href="https://github.com/SergeyShk/esTS/blob/master/LICENSE.txt"><img src="https://img.shields.io/github/license/sergeyshk/esTS.svg" alt="License"></a>
27
+ <a href="https://doi.org/10.5281/zenodo.22924655"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.22924655.svg" alt="DOI"></a>
27
28
  </p>
28
29
 
29
30
  ---
30
31
 
31
- **esTS** calcula para textos en español lo que normalmente exige juntar varias herramientas sueltas: estadísticas básicas, legibilidad, diversidad léxica, morfología, sintaxis y cohesión, con fórmulas publicadas y con los coeficientes y las escalas de sus autores, y con las categorías y los rasgos de Universal Dependencies.
32
+ **esTS** calcula para textos en español lo que normalmente exige juntar varias herramientas sueltas: estadísticas básicas, legibilidad, diversidad léxica, complejidad léxica, morfología, sintaxis y cohesión, con fórmulas publicadas y con los coeficientes y las escalas de sus autores, y con las categorías y los rasgos de Universal Dependencies.
32
33
 
33
- La biblioteca trabaja tanto con cadenas como con objetos `Doc` de [spaCy](https://github.com/explosion/spaCy): las oraciones, las palabras y los N-gramas de caracteres se extraen por reglas, las sílabas y el acento se deducen de la ortografía, y solo las estadísticas morfológicas, las sintácticas y las de cohesión necesitan un modelo entrenado.
34
+ La biblioteca trabaja tanto con cadenas como con objetos `Doc` de [spaCy](https://github.com/explosion/spaCy): las oraciones, las palabras y los N-gramas de caracteres se extraen por reglas, las sílabas y el acento se deducen de la ortografía, y solo las estadísticas morfológicas, las sintácticas, las de cohesión y las de complejidad léxica, el perfil de las palabras funcionales y la comparación de corpus necesitan un modelo entrenado.
34
35
 
35
36
  * **[Extracción de objetos](https://sergeyshk.github.io/esTS/es/extractors/sentences/)** - tokenizadores configurables de oraciones, palabras y N-gramas de caracteres que conocen los signos de apertura, la raya de diálogo y las abreviaturas del español
36
37
  * **[Sílabas y acento](https://sergeyshk.github.io/esTS/es/syllables/)** - silabificación por reglas y sílaba tónica deducida de la escritura, sin diccionario
@@ -38,11 +39,15 @@ La biblioteca trabaja tanto con cadenas como con objetos `Doc` de [spaCy](https:
38
39
  * **[Métricas de legibilidad](https://sergeyshk.github.io/esTS/es/stats/readability_stats/)** - Fernández Huerta, Szigriszt-Pazos con la escala INFLESZ, Gutiérrez de Polini, Crawford, Legibilidad µ, SOL, LIX y RIX, con grado de consenso, etapas escolares de España y tiempo de lectura
39
40
  * **[Métricas de diversidad léxica](https://sergeyshk.github.io/esTS/es/stats/diversity_stats/)** - TTR y sus variantes, MATTR, MSTTR, MTLD, HD-D, índices de Simpson y de Yule, entropía, leyes de Zipf y de Heaps
40
41
  * **[Estadísticas morfológicas](https://sergeyshk.github.io/esTS/es/stats/morph_stats/)** - categorías gramaticales y quince rasgos morfológicos de Universal Dependencies, con los marcadores del español: los modos, las formas no personales, `ser` frente a `estar`, los adverbios en `-mente`
42
+ * **[Medidas de corpus](https://sergeyshk.github.io/esTS/es/corpus/keyness/)** - palabras clave frente a un corpus de referencia, colocaciones, la dispersión de una palabra por las partes de un texto, una concordancia KWIC y la estilometría de autoría: la Delta de Burrows con sus variantes, Zeta, la curva de Mendenhall, el perfil de las palabras funcionales; la comparación de dos corpus por 132 rasgos de un texto con tamaños del efecto
43
+ * **[Visualizaciones](https://sergeyshk.github.io/esTS/es/visualizers/zipf/)** - ley de Zipf, huella literaria, árbol de palabras, dispersión léxica y palabras clave, red de colocaciones, dendrograma, PCA y MDS por la Delta, crecimiento del vocabulario, longitudes de las oraciones
44
+ * **[Conjuntos de datos](https://sergeyshk.github.io/esTS/es/datasets/spanishliterature/)** - literatura en español de dominio público: 150 obras de 33 autores de España, Hispanoamérica y Filipinas en prosa, poesía, teatro y ensayo, con el género, los años y el país; un diccionario de frecuencias de 83 785 lemas según Google Books Ngram con ipm, rango y dispersión
41
45
  * **[Componentes de spaCy](https://sergeyshk.github.io/esTS/es/components/)** - cada clase de estadísticas como componente de un pipeline, con las estadísticas puestas en el `Doc` en una sola pasada
42
46
  * **[Estadísticas de cohesión](https://sergeyshk.github.io/esTS/es/stats/cohesion_stats/)** - la repetición de sustantivos, argumentos y palabras con contenido entre oraciones, la información dada y la cohesión temporal a la manera de Coh-Metrix, con la densidad de 255 marcadores del discurso españoles
47
+ * **[Estadísticas de complejidad léxica](https://sergeyshk.github.io/esTS/es/stats/lexical_stats/)** - cuán raras son las palabras de un texto en la lengua: la frecuencia, el rango y la dispersión de los lemas según un diccionario de Google Books Ngram, las bandas de frecuencia del top-1000 al 10000, la sorpresa, la perplejidad y la densidad léxica
43
48
  * **[Estadísticas sintácticas](https://sergeyshk.github.io/esTS/es/stats/syntax_stats/)** - el árbol de dependencias por distancias, profundidad, cláusulas y coordinación, con las construcciones del estilo administrativo: la pasiva con `ser` y con `se`, las cláusulas de participio y de gerundio, las cadenas de `de`, los predicados escindidos
44
49
 
45
- Las medidas de corpus y la estilometría llegan en la 0.3, el estilo, la fonoestadística, la métrica y la rima en la 0.4.
50
+ El estilo, la fonoestadística, la métrica y la rima llegan en la 0.4.
46
51
 
47
52
  ## Instalación
48
53
 
@@ -58,12 +63,14 @@ O con [uv](https://docs.astral.sh/uv/):
58
63
  uv add pyests
59
64
  ```
60
65
 
61
- El distribuible en PyPI se llama `pyests` y el paquete que instala es `ests`. Las estadísticas básicas, la legibilidad y la diversidad léxica no necesitan ningún modelo de spaCy; las estadísticas morfológicas, las sintácticas y las de cohesión sí, igual que analizar un texto por su cuenta para pasar el `Doc` en lugar de una cadena:
66
+ El distribuible en PyPI se llama `pyests` y el paquete que instala es `ests`. Las estadísticas básicas, la legibilidad y la diversidad léxica no necesitan ningún modelo de spaCy; las estadísticas morfológicas, las sintácticas, las de cohesión y las de complejidad léxica de una cadena sí, igual que el perfil de las palabras funcionales, los rasgos de un texto y la comparación de corpus, y analizar un texto por su cuenta para pasar el `Doc` en lugar de una cadena:
62
67
 
63
68
  ```bash
64
69
  python -m spacy download es_core_news_sm
65
70
  ```
66
71
 
72
+ Las estadísticas según el diccionario de frecuencias lo necesitan descargado una vez con `FreqDict().download()`. Los conjuntos de datos van al directorio `ests_data` junto al paquete instalado; otro se pasa en `data_dir` o se define en la variable de entorno `ESTS_DATA_DIR` antes de importar el paquete.
73
+
67
74
  ## Primeros pasos
68
75
 
69
76
  ```python
@@ -410,6 +417,27 @@ Más en la [documentación](https://sergeyshk.github.io/esTS/es/stats/cohesion_s
410
417
 
411
418
  </details>
412
419
 
420
+ <details>
421
+ <summary><b>Estadísticas de complejidad léxica</b></summary>
422
+
423
+ <br>
424
+
425
+ Cuán raras son las palabras de un texto en la lengua, a la manera de TAALES: la frecuencia media, el rango y la dispersión de los lemas según el diccionario de frecuencias de Google Books Ngram, las proporciones de palabras de las bandas de frecuencia top-1000, 2000, 5000 y 10000, la sorpresa y la perplejidad según el modelo de unigramas del diccionario, la densidad léxica. Las bandas y la densidad funcionan sin más; las estadísticas según el diccionario lo necesitan descargado una vez.
426
+
427
+ ```python
428
+ >>> from ests import LexicalStats
429
+ >>> from ests.datasets import FreqDict
430
+
431
+ >>> FreqDict().download()
432
+ >>> ls = LexicalStats("El felinólogo examinaba al minino con parsimonia")
433
+ >>> ls.coverage, ls.p_top1000, round(ls.surprisal, 2)
434
+ (0.8571428571428571, 0.42857142857142855, 13.71)
435
+ ```
436
+
437
+ Más en la [documentación](https://sergeyshk.github.io/esTS/es/stats/lexical_stats/).
438
+
439
+ </details>
440
+
413
441
  <details>
414
442
  <summary><b>Componentes de spaCy</b></summary>
415
443
 
@@ -433,12 +461,140 @@ Cada clase de estadísticas es también un componente de un pipeline, de modo qu
433
461
  (12, ('DET', 'NOUN'), 2.0)
434
462
  ```
435
463
 
436
- Las fábricas son `ests_basic`, `ests_readability`, `ests_diversity`, `ests_morph`, `ests_syntax` y `ests_cohesion`; el nombre del paso del pipeline es libre y es como se llama la extensión.
464
+ Las fábricas son `ests_basic`, `ests_readability`, `ests_diversity`, `ests_morph`, `ests_syntax`, `ests_cohesion` y `ests_lexical`; el nombre del paso del pipeline es libre y es como se llama la extensión.
437
465
 
438
466
  Más en la [documentación](https://sergeyshk.github.io/esTS/es/components/).
439
467
 
440
468
  </details>
441
469
 
470
+ <details>
471
+ <summary><b>Medidas de corpus</b></summary>
472
+
473
+ <br>
474
+
475
+ La biblioteca compara corpus y describe el uso de una palabra con las medidas de la lingüística de corpus:
476
+
477
+ * palabras clave de un corpus objetivo frente a uno de referencia o frente al diccionario de frecuencias de Google Books Ngram: la razón de verosimilitud con su valor p, Log Ratio, ji cuadrado, %DIFF, BIC, ELL y la razón de momios
478
+ * colocaciones por logDice, MI, MI³, t-score, Dice, razón de verosimilitud, NPMI y sensibilidad mínima, contrastadas con NLTK
479
+ * la dispersión de una palabra por las partes de un texto: la DP de Gries, la DP normalizada, la D de Juilland, la D2 de Carroll, la S de Rosengren y la divergencia de Kullback-Leibler
480
+ * una concordancia KWIC por forma o por lema
481
+ * estilometría: las distancias entre textos por la Delta de Burrows y sus variantes, con la atribución de un texto a autores de referencia, los marcadores de palabras preferidas y evitadas por Zeta, el ji cuadrado de Kilgarriff, la curva de Mendenhall y el perfil de las palabras funcionales
482
+ * la comparación de dos corpus por 132 rasgos de un texto en ventanas de un tamaño parecido: la delta de Cliff, la d de Cohen y el AUC de cada rasgo, la prueba de Mann-Whitney con la corrección de Holm y un intervalo bootstrap de la diferencia de las medianas que remuestrea textos enteros
483
+
484
+ ```python
485
+ >>> from ests import WordsExtractor
486
+ >>> from ests.corpus import collocations, keyness, kwic, zeta
487
+
488
+ >>> we = WordsExtractor(use_lexemes=True, lowercase=True)
489
+ >>> target = we.extract("El gato estaba en la ventana y miraba a los pájaros. Los pájaros se fueron y el gato "
490
+ ... "se durmió en la ventana. Mañana el gato volverá a estar en la ventana y mirará a los pájaros.")
491
+ >>> reference = we.extract("El perro estaba en el suelo y dormía. Después el perro comió y volvió a dormir. "
492
+ ... "Mañana el perro saldrá a pasear.")
493
+
494
+ >>> [(k.word, round(k.g2, 2)) for k in keyness(target, reference, top_n=2)]
495
+ [('gato', 2.8), ('pájaro', 2.8)]
496
+
497
+ >>> [(c.left, c.right, round(c.score, 1)) for c in collocations(target, window=2, top_n=2)]
498
+ [('en', 'ventana', 13.0), ('estar', 'en', 12.7)]
499
+
500
+ >>> [line.keyword for line in kwic("Los gatos juegan y el gato duerme.", "gato", by_lemma=True)]
501
+ ['gatos', 'gato']
502
+
503
+ >>> scores = zeta(target, reference, segment_size=10)
504
+ >>> [z.word for z in scores[:2]], [z.word for z in scores[-2:]]
505
+ (['gato', 'ventana'], ['dormir', 'perro'])
506
+ ```
507
+
508
+ Las palabras se comparan tal cual, así que la caja, los lemas y las palabras vacías se eligen en la extracción.
509
+
510
+ Más en la [documentación](https://sergeyshk.github.io/esTS/es/corpus/keyness/).
511
+
512
+ </details>
513
+
514
+ <details>
515
+ <summary><b>Conjuntos de datos</b></summary>
516
+
517
+ <br>
518
+
519
+ * [spanish_literature](https://sergeyshk.github.io/esTS/es/datasets/spanishliterature/) - literatura en español de dominio público: 150 obras de 33 autores de España, Hispanoamérica y Filipinas, de Cervantes a los años veinte, en prosa, poesía, teatro y ensayo; 65 millones de caracteres
520
+ * [freq_dict](https://sergeyshk.github.io/esTS/es/datasets/freqdict/) - un diccionario de frecuencias de 83 785 lemas del español a partir de los libros de Google Books Ngram de 1980-2019 (63 000 millones de palabras): ipm, rango y D de Juilland por años, número de libros y categoría gramatical (CC BY 3.0)
521
+
522
+ Los textos se recortan al texto del autor, sin portadas, notas de los transcriptores y de los editores, índices ni notas al pie, y llevan el género, el autor, el título, los años de la primera publicación y el país; los registros se pueden filtrar por cualquiera de ellos y por la longitud del texto.
523
+
524
+ ```python
525
+ >>> from ests.datasets import SpanishLiterature
526
+
527
+ >>> sl = SpanishLiterature()
528
+ >>> sl.download()
529
+ >>> for record in sl.get_records(author="galdos", year_from=1880, year_to=1884):
530
+ ... print(record["title"], record["year_from"], len(record["text"]))
531
+ La desheredada 1881 820454
532
+ El amigo Manso 1882 521156
533
+ La de Bringas 1884 413730
534
+ Tormento 1884 477286
535
+ ```
536
+
537
+ El archivo (19 MB) se descarga una vez con `download()` en el directorio de datos y se verifica con su suma de comprobación SHA-256; antes de la descarga `get_texts()` y `get_records()` levantan `DatasetNotFoundError` con una indicación.
538
+
539
+ Más en la [documentación](https://sergeyshk.github.io/esTS/es/datasets/spanishliterature/).
540
+
541
+ </details>
542
+
543
+ <details>
544
+ <summary><b>Visualizaciones</b></summary>
545
+
546
+ <br>
547
+
548
+ * [Ley de Zipf](https://sergeyshk.github.io/esTS/es/visualizers/zipf/) con la curva teórica y el ajuste de Zipf-Mandelbrot
549
+ * [Huella literaria](https://sergeyshk.github.io/esTS/es/visualizers/fingerprinting/) (Literature Fingerprinting)
550
+ * [Árbol de palabras](https://sergeyshk.github.io/esTS/es/visualizers/word_tree/) (Word Tree)
551
+ * [Gráficos de corpus](https://sergeyshk.github.io/esTS/es/visualizers/corpus/): dispersión léxica, un diagrama de palabras clave, una red de colocaciones
552
+ * [Gráficos estilométricos](https://sergeyshk.github.io/esTS/es/visualizers/stylometry/): un dendrograma, las componentes principales y el escalamiento multidimensional por la Delta, las curvas de Mendenhall
553
+ * [Crecimiento del vocabulario y espectro de frecuencias](https://sergeyshk.github.io/esTS/es/visualizers/vocabulary/), [longitudes de las oraciones](https://sergeyshk.github.io/esTS/es/visualizers/sentences/) con una media móvil
554
+
555
+ Los gráficos de matplotlib reciben los ejes `ax` y devuelven `Axes`, así que se pueden disponer en una misma figura; la red de colocaciones y el árbol de palabras son grafos de graphviz, cuyos ejecutables los dibujan. La Delta coseno separa tres novelas de Galdós de tres de Unamuno:
556
+
557
+ ```python
558
+ import matplotlib.pyplot as plt
559
+ from ests import WordsExtractor
560
+ from ests.corpus import delta
561
+ from ests.datasets import SpanishLiterature
562
+ from ests.visualizers import dendrogram_plot, pca_plot
563
+
564
+ # seis novelas de Galdós y de Unamuno del corpus de literatura
565
+ titles = (
566
+ "Marianela",
567
+ "Misericordia",
568
+ "Torquemada en la hoguera",
569
+ "Niebla",
570
+ "Abel Sánchez",
571
+ "La tía Tula",
572
+ )
573
+ sl = SpanishLiterature()
574
+ sl.download()
575
+ novels = {
576
+ record["title"]: record["text"]
577
+ for author in ("galdos", "unamuno")
578
+ for record in sl.get_records(author=author)
579
+ if record["title"] in titles
580
+ }
581
+ we = WordsExtractor(lowercase=True)
582
+ corpus = {title: we.extract(novels[title]) for title in titles}
583
+ distances = delta(corpus, n_mfw=100, variant="cosine")
584
+
585
+ fig, (left, right) = plt.subplots(1, 2, figsize=(13, 5))
586
+ dendrogram_plot(distances, ax=left)
587
+ pca_plot(corpus, n_mfw=100, ax=right)
588
+ ```
589
+
590
+ <p align="center">
591
+ <img src="https://raw.githubusercontent.com/SergeyShk/esTS/master/docs/img/stylometry.png" alt="Gráficos estilométricos" width="760">
592
+ </p>
593
+
594
+ Más en la [documentación](https://sergeyshk.github.io/esTS/es/visualizers/zipf/).
595
+
596
+ </details>
597
+
442
598
  ## Desarrollo
443
599
 
444
600
  El proyecto usa [uv](https://docs.astral.sh/uv/) para gestionar las dependencias y [ruff](https://docs.astral.sh/ruff/) para el análisis y el formato del código.
@@ -456,7 +612,7 @@ Ejecute `make help` para ver la lista completa de comandos.
456
612
 
457
613
  La documentación es bilingüe: las páginas en inglés son `docs/*.md` y las españolas `docs/*.es.md` junto a ellas ([mkdocs-static-i18n](https://github.com/ultrabug/mkdocs-static-i18n)); al editar una página, actualice las dos versiones.
458
614
 
459
- La versión instalada está en `ests.__version__`. Todas las excepciones heredan de `ests.EstsError` y de una de las clases integradas (`SourceError` y `ParameterError` de `ValueError`, `SourceTypeError` de `TypeError`, `UnknownStatError` de `KeyError`), así que `except ValueError` sigue funcionando. La biblioteca no imprime nada por su cuenta: sus mensajes van al logger `ests` (`logging.getLogger("ests")`) y están en silencio por defecto.
615
+ La versión instalada está en `ests.__version__`. Todas las excepciones heredan de `ests.EstsError` y de una de las clases integradas (`SourceError`, `ParameterError` y `DataFileError` de `ValueError`, `SourceTypeError` de `TypeError`, `UnknownStatError` de `KeyError`, `DatasetNotFoundError` de `OSError`, `DownloadError` de `RuntimeError`), así que `except ValueError` sigue funcionando. La biblioteca no imprime nada por su cuenta: sus mensajes van al logger `ests` (`logging.getLogger("ests")`) y están en silencio por defecto.
460
616
 
461
617
  Antes de enviar cambios, instale los hooks que ejecutan los linters al hacer commit y las pruebas al hacer push:
462
618
 
@@ -477,7 +633,10 @@ Los informes de errores, las ideas y los pull requests son bienvenidos: las [iss
477
633
  * **ests**:
478
634
  * basic_stats.py - estadísticas básicas del texto
479
635
  * cohesion_stats.py - estadísticas de cohesión
636
+ * lexical_stats.py - estadísticas de complejidad léxica
480
637
  * components.py - componentes de un pipeline de spaCy
638
+ * corpus - medidas de la lingüística de corpus: palabras clave, colocaciones, dispersión, concordancia, estilometría, comparación de corpus
639
+ * datasets - conjuntos de datos: literatura en español, diccionario de frecuencias
481
640
  * constants.py - constantes de la lengua española y de las métricas
482
641
  * diversity_stats.py - métricas de diversidad léxica
483
642
  * exceptions.py - excepciones de la biblioteca
@@ -487,6 +646,8 @@ Los informes de errores, las ideas y los pull requests son bienvenidos: las [iss
487
646
  * syntax_stats.py - estadísticas sintácticas
488
647
  * syllables.py - silabificación y acento
489
648
  * utils.py - herramientas auxiliares
649
+ * visualizers - gráficos: ley de Zipf, huella literaria, árbol de palabras, gráficos de corpus y estilométricos, crecimiento del vocabulario, longitudes de las oraciones
650
+ * **scripts** - scripts que construyen los archivos de los conjuntos de datos
490
651
  * **tests** - pruebas que reproducen la estructura del paquete
491
652
 
492
653
  </details>
@@ -501,13 +662,14 @@ Los informes de errores, las ideas y los pull requests son bienvenidos: las [iss
501
662
 
502
663
  ## Cómo citar
503
664
 
504
- Si usa **esTS** en su investigación o en su software, cítelo con la siguiente entrada BibTeX. Las citas ayudan al desarrollo y al mantenimiento continuos de la biblioteca. Los mismos metadatos están en [CITATION.cff](https://github.com/SergeyShk/esTS/blob/master/CITATION.cff): GitHub los muestra bajo el botón «Cite this repository».
665
+ Si usa **esTS** en su investigación o en su software, cítelo con la siguiente entrada BibTeX. Las citas ayudan al desarrollo y al mantenimiento continuos de la biblioteca. Los mismos metadatos están en [CITATION.cff](https://github.com/SergeyShk/esTS/blob/master/CITATION.cff): GitHub los muestra bajo el botón «Cite this repository». El Concept DOI [10.5281/zenodo.22924655](https://doi.org/10.5281/zenodo.22924655) en Zenodo apunta a todas las versiones de la biblioteca; el DOI de una versión concreta está en la página de su publicación.
505
666
 
506
667
  ```bibtex
507
668
  @software{esTS,
508
669
  author = {Sergey Shkarin},
509
670
  title = {{esTS, a library for statistics extraction from texts in Spanish}},
510
671
  year = 2026,
672
+ doi = {10.5281/zenodo.22924655},
511
673
  url = {https://github.com/SergeyShk/esTS}
512
674
  }
513
675
  ```