uavocab 0.5.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.
@@ -0,0 +1,50 @@
1
+ # Licenses
2
+
3
+ ## The combined work: CC-BY-NC-SA 4.0
4
+
5
+ This package as a whole is distributed under the Creative Commons
6
+ Attribution-NonCommercial-ShareAlike 4.0 International license
7
+ (https://creativecommons.org/licenses/by-nc-sa/4.0/).
8
+
9
+ Why NonCommercial and share-alike: the per-lemma CEFR levels come from the
10
+ Ukrainian Vocabulary Profile (PULS), which its authors license CC BY-NC-SA 4.0
11
+ (decided 2026-09-24) and which is redistributed here with their permission.
12
+ Those levels are in the validated overlay, in every row of the crosswalk, in
13
+ the `puls_level` column of the two Borodin tables, and in the `related_level`
14
+ column of the aspect table. NonCommercial and share-alike propagate to a
15
+ combined work, so the package carries both. They are the one NonCommercial
16
+ component; nothing else NonCommercial is shipped. The frequency list and the
17
+ two Borodin minimums are CC-BY 4.0 on their own and remain so, and the
18
+ government and collocation tables and the stress column are CC-BY-SA 4.0; each
19
+ dataset's own license is recorded in `uavocab.sources()` and below, so a downstream user who drops
20
+ the PULS level columns owes attribution (and, for the corpus tables,
21
+ share-alike) and nothing more.
22
+
23
+ Cite the profile as its authors ask: Профіль української лексики (ПУЛЬС) / О. Синчак, В. Старко, М. Бурак, М. Свистун та ін. Львів: Школа української мови та культури УКУ, 2026.
24
+
25
+ ## Per dataset
26
+
27
+ | Dataset | License | Attribution |
28
+ |---|---|---|
29
+ | `frequency` (top-12,000 lemma list) | CC-BY 4.0 | Derived aggregate counts from the UberText 2.0 lemma frequency dictionary, lang-uk. Chaplynskyi, D. (2023). Introducing UberText 2.0: A Corpus of Modern Ukrainian at Scale. Proc. 2nd UNLP, ACL. https://lang.org.ua/en/ubertext/ |
30
+ | `validated_cefr` (PULS CEFR overlay) | CC-BY-NC-SA 4.0 | Ukrainian Vocabulary Profile (PULS): Synchak, O., Starko, V., Burak, M., Svystun, M. https://puls.peremova.org/ (eLex 2025). Per-lemma levels redistributed with the authors' permission (V. Starko, July 2026); CC BY-NC-SA 4.0 at the authors' decision (O. Synchak, 2026-09-24), attribution required. Snapshot 2026-07-23; the profile is not final. |
31
+ | `borodin_a1` (A1 lexical minimum) | CC-BY 4.0 | Borodin, K. and Turkevych, O. 1000 i 1 slovo. Leksychnyi minimum z ukrainskoi movy yak inozemnoi. Riven A1. Slavic Language Education 3, LANGUAGE-LAB, Humboldt-Universitat zu Berlin. https://doi.org/10.18452/28236. Machine-readable list provided by the authors, 2026-07-27. The `puls_level` column is CC-BY-NC-SA (PULS). |
32
+ | `borodin_a2` (A2 lexical minimum) | CC-BY 4.0 | Borodin, K. and Lazarenko, O. (2026). 1000 i 1 slovo. Riven A2. Europa-Universitat Viadrina Frankfurt (Oder). https://doi.org/10.11584/opus4-1448. The `puls_level` column is CC-BY-NC-SA (PULS). |
33
+ | `crosswalk` (minimums against the profile) | CC-BY-NC-SA 4.0 | Lawrence, J. (2026), over the two Borodin volumes (CC-BY 4.0), the PULS profile (CC BY-NC-SA 4.0, by permission) and UberText 2.0 ranks (CC-BY 4.0). |
34
+ | `b1_aspect` (aspect relations) | CC-BY-NC-SA 4.0 | Levels: PULS, by permission, CC BY-NC-SA 4.0. Aspect partners: Wiktionary via kaikki.org, CC-BY-SA 3.0. Counts: UberText 2.0, CC-BY 4.0. |
35
+ | `b1_government` (case government) | CC-BY-SA 4.0 | Counts over the UberText 2.0 Wikipedia subcorpus (lang-uk, CC-BY 4.0); derived statistics CC-BY-SA 4.0. |
36
+ | `b1_collocations` (logDice collocations) | CC-BY-SA 4.0 | Same corpus and terms as `b1_government`. |
37
+ | `stress` (word stress per lemma) | CC-BY-SA 4.0 | ukrainian-word-stress 2.1.0 (lang-uk: Oleksiy Syvokon, maintained by Dmytro Chaplynskyi), MIT, https://github.com/lang-uk/ukrainian-word-stress; en.wiktionary via kaikki.org and uk.wiktionary, CC-BY-SA 3.0; uk.wikipedia lead sentences for a few names, CC-BY-SA 4.0; goroh.pp.ua consulted for 19 tiebreaks (attested stress only). Share-alike from the Wiktionary rows; its own table, joined on rank, so the list stays CC-BY. |
38
+
39
+ ## Not in this package
40
+
41
+ Predicted CEFR levels (the site's gap-fill model; predictions are never merged
42
+ into the validated overlay), any VESUM-derived lexicon or wordform data
43
+ (CC BY-NC-SA), the UniMorph derivational families (CC BY-NC 4.0), the
44
+ Wikipedia example sentences, and the Wiktionary paradigm tables. If you need
45
+ any of them, they are on the site with their own terms.
46
+
47
+ ## The code
48
+
49
+ The Python code in this package (`src/uavocab/*.py`) is released under the
50
+ same CC-BY-NC-SA 4.0 terms as the data, to keep one license on the wheel.
uavocab-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: uavocab
3
+ Version: 0.5.0
4
+ Summary: The open Ukrainian vocabulary data behind uavocab.org, offline: the top-12,000 lemma frequency list, the validated PULS CEFR overlay, the Borodin A1 and A2 lexical minimums, their crosswalk, the B1 enrichment tables, and a per-lemma word-stress column.
5
+ Author: Jake Lawrence
6
+ License-Expression: CC-BY-NC-SA-4.0
7
+ Project-URL: Homepage, https://uavocab.org/research
8
+ Project-URL: Documentation, https://uavocab.org/research#release
9
+ Project-URL: Data, https://uavocab.org/data
10
+ Project-URL: Source, https://github.com/jake0lawrence/UA-Vocab
11
+ Project-URL: Issues, https://github.com/jake0lawrence/UA-Vocab/issues
12
+ Project-URL: Changelog, https://github.com/jake0lawrence/UA-Vocab/blob/main/python/uavocab/CHANGELOG.md
13
+ Project-URL: Release files, https://github.com/jake0lawrence/UA-Vocab/tree/main/downloads/ukrainian-frequency
14
+ Project-URL: Zenodo: frequency list, https://doi.org/10.5281/zenodo.22823134
15
+ Project-URL: Zenodo: word stress, https://doi.org/10.5281/zenodo.23092388
16
+ Keywords: ukrainian,uavocab,vocabulary,frequency list,CEFR,lexical minimum,language learning,corpus
17
+ Classifier: Development Status :: 3 - Alpha
18
+ Classifier: Intended Audience :: Education
19
+ Classifier: Intended Audience :: Science/Research
20
+ Classifier: Natural Language :: Ukrainian
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Topic :: Education
24
+ Classifier: Topic :: Text Processing :: Linguistic
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSES.md
28
+ Requires-Dist: pyarrow>=14
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=7; extra == "test"
31
+ Dynamic: license-file
32
+
33
+ # uavocab
34
+
35
+ The open Ukrainian vocabulary data behind [uavocab.org](https://uavocab.org),
36
+ installable and offline: the top-12,000 lemma frequency list over UberText 2.0,
37
+ the validated PULS CEFR overlay (A1 to C1), the Borodin A1 and A2 lexical
38
+ minimums, the crosswalk between the minimums and the profile, the B1
39
+ enrichment tables (aspect relations, case government, collocations), and a
40
+ per-lemma word-stress column. The study the data comes from, with its methods,
41
+ findings and tools, is [uavocab.org/research](https://uavocab.org/research).
42
+
43
+ ```
44
+ pip install uavocab
45
+ ```
46
+
47
+ ```python
48
+ import uavocab
49
+ uavocab.lookup("п'ять")["level"] # 'A1', with rank, source and enrichment beside it
50
+ uavocab.lookup("орган")["stress"] # {'stress': 'о́рган', 'variants': ['о́рган', 'орга́н'], 'status': 'homograph', ...}
51
+ uavocab.coverage(["в", "школа", "йти"], "A2")["coverage"] # share of tokens a validated-A2 learner knows
52
+ ```
53
+
54
+ This package was published as `ukvocab` up to 0.4.1. It was renamed to match
55
+ the site it belongs to; `pip install ukvocab` still works and now installs
56
+ this package, and `import ukvocab` keeps working with a warning that names
57
+ the new import.
58
+
59
+ ## Where everything is
60
+
61
+ | | |
62
+ |---|---|
63
+ | The study (methods, findings, tools) | https://uavocab.org/research |
64
+ | Every dataset, its license and how to cite it | https://uavocab.org/data |
65
+ | The release files (CSV, JSON, Anki, with SHA-256 sums) | https://uavocab.org/research#release |
66
+ | Every release file, the datasheets, and this package's source (fork it) | https://github.com/jake0lawrence/UA-Vocab |
67
+ | Archived copies with DOIs (Zenodo) | frequency list [10.5281/zenodo.22823134](https://doi.org/10.5281/zenodo.22823134), word stress [10.5281/zenodo.23092388](https://doi.org/10.5281/zenodo.23092388) |
68
+ | The lemmatizer matched to this list | https://uavocab.org/research/lemmatizer |
69
+ | PyPI | https://pypi.org/project/uavocab/ |
70
+
71
+ ## What you get
72
+
73
+ | function | returns |
74
+ |---|---|
75
+ | `lookup(lemma, pos=None)` | one lemma: `level`, `level_source`, `rank`, the frequency figures, the `stress` (primary stressed form, accepted variants, resolution status and note), every other reading of the spelling, minimum membership, the crosswalk class, and the B1 `enrichment` (aspect partners, government, collocations); `None` when no table knows the string |
76
+ | `levels(lemmas)` | batch: input string to `{level, level_source, rank, pos, source}` |
77
+ | `coverage(lemmas, known_set)` | token coverage of a lemmatized text against a known set: a level (`"A2"` means every validated lemma at or below A2), `"borodin-a1"` / `"borodin-a2"`, an int N (top N of the list), or your own set of lemmas; reports the unknowns by frequency |
78
+ | `sources()` | version, snapshot date, license, attribution, citation and SHA-256 per dataset, the combined license and why, and what is deliberately not shipped |
79
+
80
+ Every return value carries the dataset, version and snapshot it came from.
81
+ Data is Parquet, loaded lazily on first use, never on import. The only
82
+ dependency is `pyarrow`.
83
+
84
+ Two things the package does not do, on purpose. It does not lemmatize: pass
85
+ lemmas in the UberText / VESUM convention (infinitive for verbs, nominative
86
+ singular for nouns); the site's `POST /api/lemmatize` is the hosted lemmatizer
87
+ matched to this list ([its contract](https://uavocab.org/research/lemmatizer)).
88
+ And it ships no predicted CEFR levels: only the profile's
89
+ validated levels are here, and a lemma the profile does not cover gets
90
+ `level: None` rather than a guess.
91
+
92
+ ## Provenance and honesty
93
+
94
+ The data tier is generated from the site's published release files by
95
+ `scripts/build-package.py` in [the open repository](https://github.com/jake0lawrence/UA-Vocab),
96
+ which holds every release file too, and is never edited by hand; the tests assert that every table's row count and hash
97
+ match the published file exactly. `sources()` is the manifest that build
98
+ wrote. The PULS overlay is a dated snapshot (2026-07-23) of a profile that is
99
+ still being extended, redistributed with its authors' permission. The join
100
+ onto the frequency list is part-of-speech aware; ten declared cross-tag rules
101
+ recover the words the two tag conventions merely label differently, and each
102
+ row's `join` field says how it matched, so the strict join is one filter away.
103
+
104
+ ## License
105
+
106
+ The combined package is **CC-BY-NC-SA 4.0**, because the PULS levels are
107
+ NonCommercial and share-alike at the authors' decision (2026-09-24) and they run
108
+ through most tables. The frequency list and the two Borodin minimums are CC-BY
109
+ 4.0 on their own, and the government, collocation and stress tables CC-BY-SA
110
+ 4.0; drop the PULS level columns and NonCommercial does not apply. `LICENSES.md` has the
111
+ per-dataset terms and every attribution.
112
+
113
+ ## Citation
114
+
115
+ ```
116
+ Lawrence, J. (2026). uavocab: the Ukrainian Frequency datasets (top-12,000 lemma
117
+ list, validated PULS CEFR overlay, Borodin A1/A2 minimums, crosswalk, B1
118
+ enrichment tables, word stress), version 0.5.0. https://uavocab.org/research
119
+
120
+ Upstream: Chaplynskyi, D. (2023). Introducing UberText 2.0: A Corpus of Modern
121
+ Ukrainian at Scale. Proc. 2nd UNLP, ACL. | Synchak, O., Starko, V., Burak, M.,
122
+ Svystun, M. (2025). Corpus-Based Vocabulary Profiling for Ukrainian: From
123
+ Lexical Analysis to CEFR. eLex 2025; PULS, puls.peremova.org. | Borodin, K. and
124
+ Turkevych, O. 1000 i 1 slovo, A1. https://doi.org/10.18452/28236 | Borodin, K.
125
+ and Lazarenko, O. (2026). 1000 i 1 slovo, A2. https://doi.org/10.11584/opus4-1448
126
+ | Syvokon, O. and Chaplynskyi, D. (lang-uk). ukrainian-word-stress, MIT.
127
+ https://github.com/lang-uk/ukrainian-word-stress
128
+ ```
129
+
130
+ Archived, DOI-minted copies of each dataset are listed in `sources()` under
131
+ `conceptDoi` once they exist (today the frequency list and the stress column;
132
+ the PULS-derived datasets wait on the profile's published license notice).
133
+ Cite the concept DOI for the dataset you used.
@@ -0,0 +1,101 @@
1
+ # uavocab
2
+
3
+ The open Ukrainian vocabulary data behind [uavocab.org](https://uavocab.org),
4
+ installable and offline: the top-12,000 lemma frequency list over UberText 2.0,
5
+ the validated PULS CEFR overlay (A1 to C1), the Borodin A1 and A2 lexical
6
+ minimums, the crosswalk between the minimums and the profile, the B1
7
+ enrichment tables (aspect relations, case government, collocations), and a
8
+ per-lemma word-stress column. The study the data comes from, with its methods,
9
+ findings and tools, is [uavocab.org/research](https://uavocab.org/research).
10
+
11
+ ```
12
+ pip install uavocab
13
+ ```
14
+
15
+ ```python
16
+ import uavocab
17
+ uavocab.lookup("п'ять")["level"] # 'A1', with rank, source and enrichment beside it
18
+ uavocab.lookup("орган")["stress"] # {'stress': 'о́рган', 'variants': ['о́рган', 'орга́н'], 'status': 'homograph', ...}
19
+ uavocab.coverage(["в", "школа", "йти"], "A2")["coverage"] # share of tokens a validated-A2 learner knows
20
+ ```
21
+
22
+ This package was published as `ukvocab` up to 0.4.1. It was renamed to match
23
+ the site it belongs to; `pip install ukvocab` still works and now installs
24
+ this package, and `import ukvocab` keeps working with a warning that names
25
+ the new import.
26
+
27
+ ## Where everything is
28
+
29
+ | | |
30
+ |---|---|
31
+ | The study (methods, findings, tools) | https://uavocab.org/research |
32
+ | Every dataset, its license and how to cite it | https://uavocab.org/data |
33
+ | The release files (CSV, JSON, Anki, with SHA-256 sums) | https://uavocab.org/research#release |
34
+ | Every release file, the datasheets, and this package's source (fork it) | https://github.com/jake0lawrence/UA-Vocab |
35
+ | Archived copies with DOIs (Zenodo) | frequency list [10.5281/zenodo.22823134](https://doi.org/10.5281/zenodo.22823134), word stress [10.5281/zenodo.23092388](https://doi.org/10.5281/zenodo.23092388) |
36
+ | The lemmatizer matched to this list | https://uavocab.org/research/lemmatizer |
37
+ | PyPI | https://pypi.org/project/uavocab/ |
38
+
39
+ ## What you get
40
+
41
+ | function | returns |
42
+ |---|---|
43
+ | `lookup(lemma, pos=None)` | one lemma: `level`, `level_source`, `rank`, the frequency figures, the `stress` (primary stressed form, accepted variants, resolution status and note), every other reading of the spelling, minimum membership, the crosswalk class, and the B1 `enrichment` (aspect partners, government, collocations); `None` when no table knows the string |
44
+ | `levels(lemmas)` | batch: input string to `{level, level_source, rank, pos, source}` |
45
+ | `coverage(lemmas, known_set)` | token coverage of a lemmatized text against a known set: a level (`"A2"` means every validated lemma at or below A2), `"borodin-a1"` / `"borodin-a2"`, an int N (top N of the list), or your own set of lemmas; reports the unknowns by frequency |
46
+ | `sources()` | version, snapshot date, license, attribution, citation and SHA-256 per dataset, the combined license and why, and what is deliberately not shipped |
47
+
48
+ Every return value carries the dataset, version and snapshot it came from.
49
+ Data is Parquet, loaded lazily on first use, never on import. The only
50
+ dependency is `pyarrow`.
51
+
52
+ Two things the package does not do, on purpose. It does not lemmatize: pass
53
+ lemmas in the UberText / VESUM convention (infinitive for verbs, nominative
54
+ singular for nouns); the site's `POST /api/lemmatize` is the hosted lemmatizer
55
+ matched to this list ([its contract](https://uavocab.org/research/lemmatizer)).
56
+ And it ships no predicted CEFR levels: only the profile's
57
+ validated levels are here, and a lemma the profile does not cover gets
58
+ `level: None` rather than a guess.
59
+
60
+ ## Provenance and honesty
61
+
62
+ The data tier is generated from the site's published release files by
63
+ `scripts/build-package.py` in [the open repository](https://github.com/jake0lawrence/UA-Vocab),
64
+ which holds every release file too, and is never edited by hand; the tests assert that every table's row count and hash
65
+ match the published file exactly. `sources()` is the manifest that build
66
+ wrote. The PULS overlay is a dated snapshot (2026-07-23) of a profile that is
67
+ still being extended, redistributed with its authors' permission. The join
68
+ onto the frequency list is part-of-speech aware; ten declared cross-tag rules
69
+ recover the words the two tag conventions merely label differently, and each
70
+ row's `join` field says how it matched, so the strict join is one filter away.
71
+
72
+ ## License
73
+
74
+ The combined package is **CC-BY-NC-SA 4.0**, because the PULS levels are
75
+ NonCommercial and share-alike at the authors' decision (2026-09-24) and they run
76
+ through most tables. The frequency list and the two Borodin minimums are CC-BY
77
+ 4.0 on their own, and the government, collocation and stress tables CC-BY-SA
78
+ 4.0; drop the PULS level columns and NonCommercial does not apply. `LICENSES.md` has the
79
+ per-dataset terms and every attribution.
80
+
81
+ ## Citation
82
+
83
+ ```
84
+ Lawrence, J. (2026). uavocab: the Ukrainian Frequency datasets (top-12,000 lemma
85
+ list, validated PULS CEFR overlay, Borodin A1/A2 minimums, crosswalk, B1
86
+ enrichment tables, word stress), version 0.5.0. https://uavocab.org/research
87
+
88
+ Upstream: Chaplynskyi, D. (2023). Introducing UberText 2.0: A Corpus of Modern
89
+ Ukrainian at Scale. Proc. 2nd UNLP, ACL. | Synchak, O., Starko, V., Burak, M.,
90
+ Svystun, M. (2025). Corpus-Based Vocabulary Profiling for Ukrainian: From
91
+ Lexical Analysis to CEFR. eLex 2025; PULS, puls.peremova.org. | Borodin, K. and
92
+ Turkevych, O. 1000 i 1 slovo, A1. https://doi.org/10.18452/28236 | Borodin, K.
93
+ and Lazarenko, O. (2026). 1000 i 1 slovo, A2. https://doi.org/10.11584/opus4-1448
94
+ | Syvokon, O. and Chaplynskyi, D. (lang-uk). ukrainian-word-stress, MIT.
95
+ https://github.com/lang-uk/ukrainian-word-stress
96
+ ```
97
+
98
+ Archived, DOI-minted copies of each dataset are listed in `sources()` under
99
+ `conceptDoi` once they exist (today the frequency list and the stress column;
100
+ the PULS-derived datasets wait on the profile's published license notice).
101
+ Cite the concept DOI for the dataset you used.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "uavocab"
7
+ version = "0.5.0"
8
+ description = "The open Ukrainian vocabulary data behind uavocab.org, offline: the top-12,000 lemma frequency list, the validated PULS CEFR overlay, the Borodin A1 and A2 lexical minimums, their crosswalk, the B1 enrichment tables, and a per-lemma word-stress column."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "CC-BY-NC-SA-4.0"
12
+ license-files = ["LICENSES.md"]
13
+ authors = [{ name = "Jake Lawrence" }]
14
+ keywords = ["ukrainian", "uavocab", "vocabulary", "frequency list", "CEFR", "lexical minimum", "language learning", "corpus"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Education",
18
+ "Intended Audience :: Science/Research",
19
+ "Natural Language :: Ukrainian",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Topic :: Education",
23
+ "Topic :: Text Processing :: Linguistic",
24
+ ]
25
+ dependencies = ["pyarrow>=14"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://uavocab.org/research"
29
+ Documentation = "https://uavocab.org/research#release"
30
+ Data = "https://uavocab.org/data"
31
+ Source = "https://github.com/jake0lawrence/UA-Vocab"
32
+ Issues = "https://github.com/jake0lawrence/UA-Vocab/issues"
33
+ Changelog = "https://github.com/jake0lawrence/UA-Vocab/blob/main/python/uavocab/CHANGELOG.md"
34
+ "Release files" = "https://github.com/jake0lawrence/UA-Vocab/tree/main/downloads/ukrainian-frequency"
35
+ "Zenodo: frequency list" = "https://doi.org/10.5281/zenodo.22823134"
36
+ "Zenodo: word stress" = "https://doi.org/10.5281/zenodo.23092388"
37
+
38
+ [project.optional-dependencies]
39
+ test = ["pytest>=7"]
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.package-data]
45
+ uavocab = ["data/*.parquet", "data/manifest.json"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,225 @@
1
+ """uavocab: the open Ukrainian vocabulary data behind uavocab.org, offline.
2
+
3
+ The data is the Ukrainian Frequency study at https://uavocab.org/research.
4
+ This package was published as `ukvocab` up to 0.4.1; that name now installs
5
+ this one.
6
+
7
+ Four functions, kept small on purpose:
8
+
9
+ lookup(lemma, pos=None) one lemma: level, its source, rank, enrichment
10
+ levels(lemmas) batch: lemma -> level, with the source
11
+ coverage(lemmas, known_set) the running-text coverage computation
12
+ sources() version, snapshot and license per dataset
13
+
14
+ Every return value carries the dataset, version and snapshot it came from.
15
+ The package does not lemmatize: pass lemmas in the VESUM / UberText
16
+ convention (the infinitive for verbs, the nominative singular for nouns).
17
+ Predicted CEFR levels are deliberately not in this package.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections import Counter
23
+ from typing import Dict, Iterable, List, Optional, Union
24
+
25
+ from . import _store
26
+ from ._store import LEVELS, norm
27
+
28
+ __all__ = ["lookup", "levels", "coverage", "sources", "LEVELS", "norm", "__version__"]
29
+
30
+
31
+ def __getattr__(name):
32
+ if name == "__version__":
33
+ return _store.manifest()["packageVersion"]
34
+ raise AttributeError(name)
35
+
36
+
37
+ LEVEL_SOURCE_VALIDATED = "PULS validated level, POS-aware join onto the frequency list (validated_cefr)"
38
+ LEVEL_SOURCE_CROSSWALK = "PULS validated level via the crosswalk: lemma-string join, no part of speech (crosswalk)"
39
+
40
+
41
+ def _reading(row: dict) -> dict:
42
+ lv = _store.level_by_rank().get(row["rank"])
43
+ return {
44
+ "rank": row["rank"],
45
+ "pos": row["pos"],
46
+ "count": row["count"],
47
+ "permille": row["permille"],
48
+ "cumulative_coverage": row["cumulativeCoverage"],
49
+ "freq_band": row["freqBand"],
50
+ "level": lv["level"] if lv else None,
51
+ "join": lv["join"] if lv else None,
52
+ "stress": _store.stress_by_rank().get(row["rank"]),
53
+ }
54
+
55
+
56
+ def _enrichment(key: str) -> dict:
57
+ return {
58
+ "aspect": [dict(r) for r in _store.aspect_by_verb().get(key, [])],
59
+ "government": [dict(r) for r in _store.government_by_verb().get(key, [])],
60
+ "collocations": [dict(r) for r in _store.collocations_by_lemma().get(key, [])],
61
+ }
62
+
63
+
64
+ def lookup(lemma: str, pos: Optional[str] = None) -> Optional[dict]:
65
+ """One lemma. Returns None when no table knows the string at all.
66
+
67
+ With `pos` (a UD tag such as NOUN, VERB, ADJ, DET) the reading with that
68
+ part of speech is returned; without it, the most frequent reading, and
69
+ every other reading is listed under `readings` so a homograph is never
70
+ silently collapsed. The level comes from the validated overlay when the
71
+ lemma is on the frequency list (POS-aware); otherwise from the crosswalk,
72
+ which joins by lemma string only, and `level_source` says which.
73
+ """
74
+ key = norm(lemma)
75
+ readings = _store.frequency_by_key().get(key, [])
76
+ if pos is not None:
77
+ readings = [r for r in readings if r["pos"] == pos]
78
+ primary = _reading(readings[0]) if readings else None
79
+ xw = _store.crosswalk_by_key().get(key)
80
+ if primary is None and xw is None:
81
+ return None
82
+
83
+ level = primary["level"] if primary else None
84
+ level_source = LEVEL_SOURCE_VALIDATED if level else None
85
+ if level is None and xw and xw.get("validatedLevel"):
86
+ level = xw["validatedLevel"]
87
+ level_source = LEVEL_SOURCE_CROSSWALK
88
+
89
+ used = ["frequency", "validated_cefr", "crosswalk", "borodin_a1", "borodin_a2"]
90
+ if primary is not None:
91
+ used.append("stress")
92
+ enrichment = _enrichment(key)
93
+ if any(enrichment.values()):
94
+ used += ["b1_aspect", "b1_government", "b1_collocations"]
95
+
96
+ return {
97
+ "query": lemma,
98
+ "lemma": readings[0]["lemma"] if readings else (xw["lemma"] if xw else key),
99
+ "on_list": primary is not None,
100
+ "rank": primary["rank"] if primary else None,
101
+ "pos": primary["pos"] if primary else (xw.get("pos") if xw else None),
102
+ "count": primary["count"] if primary else None,
103
+ "permille": primary["permille"] if primary else None,
104
+ "freq_band": primary["freq_band"] if primary else None,
105
+ "level": level,
106
+ "level_source": level_source,
107
+ "join": primary["join"] if primary else None,
108
+ "stress": primary["stress"] if primary else None,
109
+ "readings": [_reading(r) for r in _store.frequency_by_key().get(key, [])],
110
+ "minimums": {
111
+ "a1": key in _store.crosswalk_volume_keys("A1"),
112
+ "a2": key in _store.crosswalk_volume_keys("A2"),
113
+ "a1_entries": [r["entry"] for r in _store.borodin_keys("a1").get(key, [])],
114
+ "a2_entries": [r["entry"] for r in _store.borodin_keys("a2").get(key, [])],
115
+ },
116
+ "crosswalk": {
117
+ "class": xw["class"],
118
+ "volume": xw.get("volume"),
119
+ "validated_level": xw.get("validatedLevel"),
120
+ "relative": xw.get("relative"),
121
+ "relative_via": xw.get("relativeVia"),
122
+ } if xw else None,
123
+ "enrichment": enrichment,
124
+ "sources": {name: _store.source_of(name) for name in used},
125
+ }
126
+
127
+
128
+ def levels(lemmas: Iterable[str]) -> Dict[str, dict]:
129
+ """Batch lookup: input string -> {level, level_source, rank, pos, source}.
130
+ A lemma no table knows gets level None and an empty source."""
131
+ out: Dict[str, dict] = {}
132
+ for lemma in lemmas:
133
+ hit = lookup(lemma)
134
+ if hit is None:
135
+ out[lemma] = {"level": None, "level_source": None, "rank": None, "pos": None, "source": None}
136
+ continue
137
+ src_name = "validated_cefr" if hit["level_source"] == LEVEL_SOURCE_VALIDATED else ("crosswalk" if hit["level"] else None)
138
+ out[lemma] = {
139
+ "level": hit["level"],
140
+ "level_source": hit["level_source"],
141
+ "rank": hit["rank"],
142
+ "pos": hit["pos"],
143
+ "source": _store.source_of(src_name) if src_name else None,
144
+ }
145
+ return out
146
+
147
+
148
+ KnownSet = Union[str, int, Iterable[str]]
149
+
150
+
151
+ def _resolve_known(known_set: KnownSet):
152
+ """A known set is an iterable of lemma strings, a level name (every
153
+ validated lemma at or below it), 'borodin-a1' / 'borodin-a2' (the
154
+ minimum, on and off the list, A2 including A1), or an int N (the top N of
155
+ the frequency list)."""
156
+ if isinstance(known_set, int) and not isinstance(known_set, bool):
157
+ return _store.top_n_set(known_set), f"top {known_set} of the frequency list", ["frequency"]
158
+ if isinstance(known_set, str):
159
+ tag = known_set.strip()
160
+ if tag.upper() in LEVELS:
161
+ return _store.level_set(tag.upper()), f"every validated lemma at or below {tag.upper()}", ["validated_cefr", "frequency"]
162
+ if tag.lower() in ("borodin-a1", "a1-minimum"):
163
+ return _store.crosswalk_volume_keys("A1"), "the Borodin A1 minimum (every headword, on or off the list)", ["crosswalk", "borodin_a1"]
164
+ if tag.lower() in ("borodin-a2", "a2-minimum"):
165
+ return _store.crosswalk_volume_keys("A2"), "the Borodin A1 and A2 minimums (every headword, on or off the list)", ["crosswalk", "borodin_a1", "borodin_a2"]
166
+ raise ValueError(f"unknown known_set name {known_set!r}: a level (A1..C1), 'borodin-a1', 'borodin-a2', an int, or an iterable of lemmas")
167
+ return frozenset(norm(s) for s in known_set), "a caller-supplied set of lemmas", ["frequency"]
168
+
169
+
170
+ def coverage(lemmas: Iterable[str], known_set: KnownSet, top_unknown: int = 20) -> dict:
171
+ """Running-text coverage of a lemmatized text against a known set.
172
+
173
+ `lemmas` is the text as a sequence of lemmas, one per token, repeats
174
+ included (coverage is token coverage, not type coverage). Tokens are
175
+ counted as known when their normalized lemma is in the known set; the
176
+ package does not decide what a word is, so pass only the tokens you want
177
+ counted (a caller usually drops punctuation, numbers and proper names).
178
+ Also reports how many tokens are on the frequency list at all, and the
179
+ most frequent unknown lemmas, so a gap is a list rather than a number.
180
+ """
181
+ known, description, used = _resolve_known(known_set)
182
+ counts = Counter(norm(t) for t in lemmas)
183
+ total = sum(counts.values())
184
+ known_tokens = sum(n for k, n in counts.items() if k in known)
185
+ on_list = sum(n for k, n in counts.items() if k in _store.frequency_by_key())
186
+ unknown = Counter({k: n for k, n in counts.items() if k not in known})
187
+ by_level: Dict[str, int] = {lvl: 0 for lvl in LEVELS}
188
+ unleveled = 0
189
+ for k, n in counts.items():
190
+ readings = _store.frequency_by_key().get(k)
191
+ lv = _store.level_by_rank().get(readings[0]["rank"]) if readings else None
192
+ if lv:
193
+ by_level[lv["level"]] += n
194
+ else:
195
+ unleveled += n
196
+ return {
197
+ "tokens": total,
198
+ "types": len(counts),
199
+ "known": known_tokens,
200
+ "unknown": total - known_tokens,
201
+ "coverage": (known_tokens / total) if total else 0.0,
202
+ "on_list": on_list,
203
+ "on_list_share": (on_list / total) if total else 0.0,
204
+ "by_validated_level": by_level,
205
+ "unleveled": unleveled,
206
+ "known_set": description,
207
+ "known_set_size": len(known),
208
+ "top_unknown": unknown.most_common(top_unknown),
209
+ "sources": {name: _store.source_of(name) for name in used},
210
+ }
211
+
212
+
213
+ def sources() -> dict:
214
+ """Version, snapshot, license, attribution, citation and checksum per
215
+ dataset, plus the combined license and why, straight from the manifest."""
216
+ m = _store.manifest()
217
+ return {
218
+ "package": m["package"],
219
+ "package_version": m["packageVersion"],
220
+ "site": m["site"],
221
+ "combined_license": m["combinedLicense"],
222
+ "combined_license_why": m["combinedLicenseWhy"],
223
+ "not_shipped": list(m["notShipped"]),
224
+ "datasets": {name: dict(d) for name, d in m["datasets"].items()},
225
+ }