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.
- uavocab-0.5.0/LICENSES.md +50 -0
- uavocab-0.5.0/PKG-INFO +133 -0
- uavocab-0.5.0/README.md +101 -0
- uavocab-0.5.0/pyproject.toml +48 -0
- uavocab-0.5.0/setup.cfg +4 -0
- uavocab-0.5.0/src/uavocab/__init__.py +225 -0
- uavocab-0.5.0/src/uavocab/_store.py +187 -0
- uavocab-0.5.0/src/uavocab/data/b1_aspect.parquet +0 -0
- uavocab-0.5.0/src/uavocab/data/b1_collocations.parquet +0 -0
- uavocab-0.5.0/src/uavocab/data/b1_government.parquet +0 -0
- uavocab-0.5.0/src/uavocab/data/borodin_a1.parquet +0 -0
- uavocab-0.5.0/src/uavocab/data/borodin_a2.parquet +0 -0
- uavocab-0.5.0/src/uavocab/data/crosswalk.parquet +0 -0
- uavocab-0.5.0/src/uavocab/data/frequency.parquet +0 -0
- uavocab-0.5.0/src/uavocab/data/manifest.json +278 -0
- uavocab-0.5.0/src/uavocab/data/stress.parquet +0 -0
- uavocab-0.5.0/src/uavocab/data/validated_cefr.parquet +0 -0
- uavocab-0.5.0/src/uavocab.egg-info/PKG-INFO +133 -0
- uavocab-0.5.0/src/uavocab.egg-info/SOURCES.txt +22 -0
- uavocab-0.5.0/src/uavocab.egg-info/dependency_links.txt +1 -0
- uavocab-0.5.0/src/uavocab.egg-info/requires.txt +4 -0
- uavocab-0.5.0/src/uavocab.egg-info/top_level.txt +1 -0
- uavocab-0.5.0/tests/test_api.py +214 -0
- uavocab-0.5.0/tests/test_counts.py +83 -0
|
@@ -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.
|
uavocab-0.5.0/README.md
ADDED
|
@@ -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"]
|
uavocab-0.5.0/setup.cfg
ADDED
|
@@ -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
|
+
}
|