wordlevel 0.0.1a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. wordlevel/__init__.py +11 -0
  2. wordlevel/_version.py +24 -0
  3. wordlevel/api/__init__.py +0 -0
  4. wordlevel/api/catalog/__init__.py +0 -0
  5. wordlevel/api/catalog/catalog.py +265 -0
  6. wordlevel/api/catalog/constructors.py +287 -0
  7. wordlevel/api/catalog/plot.py +516 -0
  8. wordlevel/api/comparison/__init__.py +0 -0
  9. wordlevel/api/comparison/comparison.py +386 -0
  10. wordlevel/api/comparison/normalization.py +53 -0
  11. wordlevel/api/comparison/score.py +1136 -0
  12. wordlevel/api/lexicons/__init__.py +0 -0
  13. wordlevel/api/lexicons/lexicon.py +91 -0
  14. wordlevel/api/lexicons/types/__init__.py +0 -0
  15. wordlevel/api/lexicons/types/labmt.py +15 -0
  16. wordlevel/api/lexicons/types/socialsent.py +21 -0
  17. wordlevel/configs.py +374 -0
  18. wordlevel/constants.py +26 -0
  19. wordlevel/core/__init__.py +0 -0
  20. wordlevel/core/catalog/__init__.py +0 -0
  21. wordlevel/core/catalog/plot/__init__.py +0 -0
  22. wordlevel/core/catalog/plot/shift/__init__.py +0 -0
  23. wordlevel/core/catalog/plot/shift/render/__init__.py +0 -0
  24. wordlevel/core/catalog/plot/shift/render/bars.py +434 -0
  25. wordlevel/core/catalog/plot/shift/render/border.py +167 -0
  26. wordlevel/core/catalog/plot/shift/render/labels.py +244 -0
  27. wordlevel/core/catalog/plot/shift/render/scales.py +147 -0
  28. wordlevel/core/catalog/plot/shift/render/shift.py +259 -0
  29. wordlevel/core/catalog/plot/shift/render/totals.py +364 -0
  30. wordlevel/core/catalog/plot/shift/render/zero_line.py +43 -0
  31. wordlevel/core/catalog/plot/shift/transform.py +103 -0
  32. wordlevel/core/catalog/plot/utils.py +11 -0
  33. wordlevel/core/comparison/__init__.py +0 -0
  34. wordlevel/core/comparison/normalization.py +59 -0
  35. wordlevel/core/comparison/score/__init__.py +0 -0
  36. wordlevel/core/comparison/score/attribution.py +134 -0
  37. wordlevel/core/comparison/score/components.py +285 -0
  38. wordlevel/core/comparison/score/lexicon.py +126 -0
  39. wordlevel/core/comparison/score/metrics/__init__.py +0 -0
  40. wordlevel/core/comparison/score/metrics/entropy.py +84 -0
  41. wordlevel/core/comparison/score/metrics/frequency.py +6 -0
  42. wordlevel/core/comparison/score/metrics/jensen_shannon_divergence.py +75 -0
  43. wordlevel/core/comparison/score/metrics/kullback_leibler_divergence.py +101 -0
  44. wordlevel/core/comparison/score/metrics/rank_divergence.py +92 -0
  45. wordlevel/core/comparison/score/plan.py +268 -0
  46. wordlevel/core/lexicons/__init__.py +0 -0
  47. wordlevel/core/lexicons/load.py +43 -0
  48. wordlevel/core/utils.py +61 -0
  49. wordlevel/datasets/__init__.py +0 -0
  50. wordlevel/datasets/data/presidental_speeches.parquet +0 -0
  51. wordlevel/datasets/dataset.py +71 -0
  52. wordlevel-0.0.1a2.dist-info/METADATA +17 -0
  53. wordlevel-0.0.1a2.dist-info/RECORD +55 -0
  54. wordlevel-0.0.1a2.dist-info/WHEEL +4 -0
  55. wordlevel-0.0.1a2.dist-info/licenses/LICENSE +7 -0
wordlevel/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from wordlevel.api.catalog.catalog import Catalog
2
+ from wordlevel.api.comparison.comparison import comp
3
+ from wordlevel.api.lexicons.lexicon import lex
4
+ from wordlevel.datasets.dataset import Dataset
5
+
6
+ try:
7
+ from wordlevel._version import __version__
8
+ except ImportError:
9
+ __version__ = "0.0.0"
10
+
11
+ __all__ = ["__version__", "Catalog", "comp", "Dataset", "lex"]
wordlevel/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.0.1a2'
22
+ __version_tuple__ = version_tuple = (0, 0, 1, 'a2')
23
+
24
+ __commit_id__ = commit_id = None
File without changes
File without changes
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Collection, Mapping, Sequence
4
+ from functools import cached_property
5
+ from types import MappingProxyType
6
+
7
+ import polars as pl
8
+
9
+ from wordlevel.api.catalog.constructors import (
10
+ from_catalog,
11
+ from_polars,
12
+ from_dict_of_freqs,
13
+ from_list_of_freqs,
14
+ )
15
+ from wordlevel.api.catalog.plot import Plotter
16
+ from wordlevel.api.catalog.constructors import CatalogData, DictOfFreqs, ListOfFreqs
17
+ from wordlevel.api.comparison.comparison import Comparison, ComparisonResult
18
+
19
+
20
+ class Catalog:
21
+ """
22
+ The workspace for corpora and making comparisons between them.
23
+
24
+ For specific construction functions, see the [construction](construct/index.md) documentation.
25
+
26
+ Args:
27
+ data:
28
+ The corpora token-frequency data. Accepts a Polars DataFrame or LazyFrame,
29
+ [`ListOfFreqs`][wordlevel.api.catalog.constructors.ListOfFreqs],
30
+ [`DictOfFreqs`][wordlevel.api.catalog.constructors.DictOfFreqs], or an existing
31
+ `Catalog`. The class will attempt to infer the structure of the data and use the
32
+ appropriate constructor.
33
+ token_col:
34
+ If the `data` is a Polars dataframe, the column that contains the tokens (words,
35
+ subwords, n-grams, etc.). Ignored otherwise
36
+ corpora:
37
+ If the `data` is a [`ListOfFreqs`][wordlevel.api.catalog.constructors.ListOfFreqs], the
38
+ names of the corpora, listed in the same order as `data`. If not provided, will default
39
+ to `corpus1`, `corpus2`, etc. If the `data` is a Polars Dataframe or Lazyframe, the
40
+ names of the columns containing token frequencies, which must be provided. Ignored
41
+ otherwise
42
+
43
+ Raises:
44
+ TypeError: If `data` is not one of the supported types.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ data: pl.DataFrame | pl.LazyFrame | Catalog | CatalogData | DictOfFreqs | ListOfFreqs,
50
+ token_col: str | None = None,
51
+ corpora: Sequence[str] | None = None,
52
+ ) -> None:
53
+ if isinstance(data, CatalogData):
54
+ init_data = data
55
+ elif isinstance(data, Catalog):
56
+ init_data = from_catalog(data)
57
+ elif isinstance(data, (pl.DataFrame, pl.LazyFrame)):
58
+ if token_col is None:
59
+ raise ValueError("`token_col` must be specified if providing a DataFrame")
60
+ if corpora is None:
61
+ raise ValueError("`corpora` must be specified if providing a DataFrame")
62
+ init_data = from_polars(
63
+ df=data,
64
+ token_col=token_col,
65
+ freq_cols=corpora,
66
+ )
67
+ elif isinstance(data, Mapping):
68
+ init_data = from_dict_of_freqs(freqs=data)
69
+ elif isinstance(data, Sequence):
70
+ init_data = from_list_of_freqs(freqs=data, corpora=corpora)
71
+ else:
72
+ raise TypeError(f"Input type not supported: {type(data)}")
73
+
74
+ self._init_from_catalog_data(init_data)
75
+
76
+ def _init_from_catalog_data(self, data: CatalogData) -> None:
77
+ """
78
+ Initializes the catalog given the core `CatalogData`.
79
+ """
80
+ self._df = data.df
81
+ self._token_col = data.token_col
82
+ self._freq_cols = set(data.freq_cols)
83
+ self._normalization_suffix = data.normalization_suffix
84
+ self._comparisons = data.comparisons
85
+
86
+ @classmethod
87
+ def from_catalog(cls, catalog: Catalog) -> Catalog:
88
+ """
89
+ Creates a new catalog from an existing one.
90
+
91
+ The new catalog copies the data of the original one without mutating it.
92
+
93
+ Example:
94
+ ```python
95
+ --8<-- "snippets/catalog/catalog.py:from_catalog"
96
+ ```
97
+
98
+ Args:
99
+ catalog: An existing `Catalog`
100
+
101
+ Returns:
102
+ A new `Catalog` with the same internal data as the provided catalog
103
+ """
104
+ init_data = from_catalog(catalog)
105
+
106
+ return cls(data=init_data)
107
+
108
+ @classmethod
109
+ def from_polars(
110
+ cls,
111
+ df: pl.DataFrame | pl.LazyFrame,
112
+ token_col: str,
113
+ corpora: Collection[str],
114
+ normalize: bool = True,
115
+ ) -> Catalog:
116
+ """
117
+ Creates a `Catalog` of corpora from a Polars dataframe.
118
+
119
+ Example:
120
+ ```python
121
+ --8<-- "snippets/catalog/catalog.py:from_polars"
122
+ ```
123
+
124
+ Args:
125
+ df:
126
+ A Polars DataFrame or LazyFrame containing corpus information. Rows are tokens and
127
+ columns are their frequencies in different corpora
128
+ token_col:
129
+ The column that contains the tokens (words, subwords, n-grams, etc.)
130
+ corpora:
131
+ The columns that include the token frequencies for each corpus
132
+ normalize:
133
+ Whether to normalize the corpus frequencies for each corpus so that they are in the
134
+ range $[0, 1]$ and sum to 1. The original corpus frequencies will be preserved
135
+
136
+ Returns:
137
+ A `Catalog` of corpora representing the provided tokens and frequencies
138
+
139
+ Raises:
140
+ KeyError: If `token_col` is not a column in `df`.
141
+ KeyError: If any column in `corpora` is not a column in `df`.
142
+ ValueError: If fewer than two columns are provided in `corpora`.
143
+ """
144
+ init_data = from_polars(
145
+ df=df,
146
+ token_col=token_col,
147
+ freq_cols=corpora,
148
+ normalize=normalize,
149
+ )
150
+
151
+ return cls(data=init_data)
152
+
153
+ @classmethod
154
+ def from_list_of_freqs(
155
+ cls,
156
+ freqs: ListOfFreqs,
157
+ corpora: Sequence[str] | None = None,
158
+ ) -> Catalog:
159
+ """
160
+ Creates a `Catalog` of corpora from a sequence of token-to-frequency mappings, each
161
+ representing the frequencies of tokens in a particular corpus.
162
+
163
+ Example:
164
+ ```python
165
+ --8<-- "snippets/catalog/catalog.py:from_list_of_freqs"
166
+ ```
167
+
168
+ Args:
169
+ freqs:
170
+ A sequence of token-to-frequency mappings, one per corpus. Each mapping is
171
+ `{token: frequency}` for a single corpus
172
+ corpora:
173
+ The names of the corpora, listed in the same order as `freqs`. If not provided, will
174
+ default to `corpus1`, `corpus2`, etc.
175
+
176
+ Returns:
177
+ A `Catalog` of corpora representing the provided tokens and frequencies
178
+
179
+ Raises:
180
+ ValueError: If `freqs` and `corpora` have different lengths.
181
+ ValueError: If fewer than two corpora are provided in `freqs`.
182
+ ValueError: If any corpus in `freqs` has no token frequencies.
183
+ """
184
+ init_data = from_list_of_freqs(freqs=freqs, corpora=corpora)
185
+
186
+ return cls(data=init_data)
187
+
188
+ @classmethod
189
+ def from_dict_of_freqs(cls, freqs: DictOfFreqs) -> Catalog:
190
+ """
191
+ Creates a `Catalog` of corpora from named token-to-frequency mappings.
192
+
193
+ Example:
194
+ ```python
195
+ --8<-- "snippets/catalog/catalog.py:from_dict_of_freqs"
196
+ ```
197
+
198
+ Args:
199
+ freqs:
200
+ A mapping of corpus names to token-to-frequency mappings, one per corpus. Each
201
+ mapping is `{corpus: {token: frequency}}`, with one mapping per corpus
202
+
203
+ Returns:
204
+ A `Catalog` of corpora representing the provided tokens and frequencies
205
+
206
+ Raises:
207
+ ValueError: If fewer than two corpora are provided in `freqs`.
208
+ ValueError: If any corpus in `freqs` has no token frequencies.
209
+ """
210
+ init_data = from_dict_of_freqs(freqs=freqs)
211
+
212
+ return cls(data=init_data)
213
+
214
+ @property
215
+ def comparisons(self) -> Mapping[str, ComparisonResult]:
216
+ """
217
+ Comparisons made between corpora in the catalog, keyed by each comparison's alias.
218
+ """
219
+ return MappingProxyType(self._comparisons)
220
+
221
+ @property
222
+ def df(self) -> pl.DataFrame:
223
+ """
224
+ Dataframe representing the frequencies of tokens in the corpora in the catalog.
225
+ """
226
+ return self._df.collect()
227
+
228
+ @cached_property
229
+ def plot(self) -> Plotter:
230
+ """
231
+ Namespace for plotting methods. See [available plots](plot/index.md).
232
+ """
233
+ return Plotter(self)
234
+
235
+ def with_comparisons(self, *comparisons: Comparison) -> Catalog:
236
+ """
237
+ Returns a new catalog with comparisons added to it.
238
+
239
+ New comparisons will overwrite existing comparisons with the same name.
240
+
241
+ Example:
242
+ ```python
243
+ --8<-- "snippets/catalog/catalog.py:setup"
244
+
245
+ --8<-- "snippets/catalog/catalog.py:with_comparisons"
246
+ ```
247
+
248
+ Args:
249
+ comparisons: The comparisons to add to the catalog
250
+
251
+ Returns:
252
+ The catalog with the comparisons added to it
253
+ """
254
+ catalog = self.__class__(self)
255
+
256
+ for comparison in comparisons:
257
+ result = ComparisonResult.from_comparison(
258
+ comparison,
259
+ self._df,
260
+ token_col=self._token_col,
261
+ normalization_suffix=self._normalization_suffix,
262
+ )
263
+ catalog._comparisons[result.alias] = result
264
+
265
+ return catalog
@@ -0,0 +1,287 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Collection
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Iterable, Mapping, Sequence, TypeAlias
6
+
7
+ import polars as pl
8
+ from polars import col
9
+
10
+ from wordlevel.constants import NORMALIZATION_SUFFIX, TOKEN
11
+ from wordlevel.core.utils import normalize_freqs
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from wordlevel.api.catalog.catalog import Catalog
16
+ from wordlevel.api.comparison.comparison import ComparisonResult
17
+
18
+
19
+ @dataclass
20
+ class CatalogData:
21
+ """
22
+ The core data that is required to initialize and copy a `Catalog`
23
+
24
+ Args:
25
+ df: Polars
26
+ DataFrame representing the frequencies of tokens in the corpora in the catalog.
27
+ token_col:
28
+ The column in `df` that contains the tokens (words, subwords, n-grams, etc.)
29
+ freq_cols:
30
+ The columns in `df` that include the token frequencies for each corpus
31
+ normalization_suffix:
32
+ The suffix used on the `freq_cols` to indicate their normalized forms where all
33
+ frequencies are in the range $[0, 1]$ and sum to 1
34
+ comparisons:
35
+ Comparisons made between corpora in the catalog, keyed by each comparison's alias
36
+ """
37
+
38
+ df: pl.LazyFrame
39
+ token_col: str
40
+ freq_cols: Collection[str]
41
+ normalization_suffix: str
42
+ comparisons: dict[str, ComparisonResult]
43
+
44
+
45
+ ListOfFreqs: TypeAlias = Sequence[Mapping[str, float]]
46
+ """
47
+ A sequence of token-to-frequency mappings, one per corpus. Each mapping is `{word: frequency}` for
48
+ a single corpus
49
+
50
+ Example:
51
+ ```python
52
+ list_of_freqs = [
53
+ {"hi": 1, "hey": 2, "hello": 3},
54
+ {"hi": 2, "hey": 4, "bye": 6}
55
+ ]
56
+ ```
57
+ """
58
+
59
+
60
+ DictOfFreqs: TypeAlias = Mapping[str, Mapping[str, float]]
61
+ """
62
+ A mapping of corpus names to word-to-frequency mappings. The mapping is
63
+ `{corpus: {word: frequency}}`
64
+
65
+ Example:
66
+ ```python
67
+ dict_of_freqs = {
68
+ "corpus1": {"hi": 1, "hey": 2, "hello": 3},
69
+ "corpus2": {"hi": 2, "hey": 4, "bye": 6}
70
+ }
71
+ ```
72
+ """
73
+
74
+
75
+ def from_catalog(catalog: Catalog) -> CatalogData:
76
+ """
77
+ Creates the data for a new catalog from an existing one.
78
+
79
+ Copies the data of the original catalog without mutating it.
80
+
81
+ Args:
82
+ catalog: An existing `Catalog`
83
+
84
+ Returns:
85
+ The `CatalogData` needed to initialize a new catalog
86
+ """
87
+ return CatalogData(
88
+ df=catalog._df.clone().lazy(),
89
+ token_col=catalog._token_col,
90
+ freq_cols=catalog._freq_cols,
91
+ normalization_suffix=catalog._normalization_suffix,
92
+ comparisons=catalog._comparisons.copy(),
93
+ )
94
+
95
+
96
+ def from_polars(
97
+ df: pl.DataFrame | pl.LazyFrame,
98
+ token_col: str,
99
+ freq_cols: Collection[str],
100
+ normalize: bool = True,
101
+ ) -> CatalogData:
102
+ """
103
+ Creates the data for a new catalog from a Polars dataframe.
104
+
105
+ Args:
106
+ df:
107
+ A Polars DataFrame or LazyFrame containing corpus information. Rows are tokens and
108
+ columns are their frequencies in different corpora
109
+ token_col:
110
+ The column that contains the tokens (words, subwords, n-grams, etc.)
111
+ freq_cols:
112
+ The columns that include the token frequencies for each corpus
113
+ normalize:
114
+ Whether to normalize the corpus frequencies for each corpus so that they are in the
115
+ range $[0, 1]$ and sum to 1. The original corpus frequencies will be preserved
116
+
117
+ Returns:
118
+ The `CatalogData` needed to initialize a new catalog
119
+
120
+ Raises:
121
+ KeyError: If `token_col` is not a column in `df`.
122
+ KeyError: If any column in `freq_cols` is not a column in `df`.
123
+ ValueError: If fewer than two columns are provided in `freq_cols`.
124
+ """
125
+ # Check all columns exist
126
+ deduped_freq_cols = set(freq_cols)
127
+
128
+ if isinstance(df, pl.DataFrame):
129
+ cols = set(df.columns)
130
+ else:
131
+ cols = set(df.collect_schema().names())
132
+
133
+ if token_col not in cols:
134
+ raise KeyError(f"Token column not in provided dataframe: {token_col}")
135
+
136
+ if len(deduped_freq_cols) < 2:
137
+ raise ValueError("At least two frequency columns must be provided via `freq_cols`")
138
+
139
+ for freq_col in deduped_freq_cols:
140
+ if freq_col not in cols:
141
+ raise KeyError(f"Frequency column not in provided dataframe: {freq_col}")
142
+
143
+ # Do necessary transformations
144
+ lf = df.clone().lazy()
145
+ lf = lf.select(token_col, *[col(freq_col).fill_null(0) for freq_col in deduped_freq_cols])
146
+
147
+ if normalize:
148
+ lf = _normalize_frequency_columns(
149
+ lf,
150
+ freq_cols=deduped_freq_cols,
151
+ normalization_suffix=NORMALIZATION_SUFFIX,
152
+ )
153
+ normalization_suffix = NORMALIZATION_SUFFIX
154
+ else:
155
+ normalization_suffix = ""
156
+
157
+ return CatalogData(
158
+ df=lf,
159
+ token_col=token_col,
160
+ freq_cols=deduped_freq_cols,
161
+ normalization_suffix=normalization_suffix,
162
+ comparisons=dict(),
163
+ )
164
+
165
+
166
+ def from_list_of_freqs(
167
+ freqs: ListOfFreqs,
168
+ corpora: Sequence[str] | None = None,
169
+ ) -> CatalogData:
170
+ """
171
+ Creates the data for a new catalog from a sequence of token-to-frequency mappings, each
172
+ representing the frequencies of tokens in a particular corpus.
173
+
174
+ Args:
175
+ freqs:
176
+ A sequence of token-to-frequency mappings, one per corpus. Each mapping is
177
+ `{token: frequency}` for a single corpus
178
+ corpora:
179
+ The names of the corpora, listed in the same order as `freqs`. If not provided, will
180
+ default to `corpus1`, `corpus2`, etc.
181
+
182
+ Returns:
183
+ The `CatalogData` needed to initialize a new catalog
184
+
185
+ Raises:
186
+ ValueError: If `freqs` and `corpora` have different lengths.
187
+ ValueError: If fewer than two corpora are provided in `freqs`.
188
+ ValueError: If any corpus in `freqs` has no token frequencies.
189
+ """
190
+ if corpora is None:
191
+ corpora = [f"corpus{n}" for n in range(1, len(freqs) + 1)]
192
+ else:
193
+ n_freqs = len(freqs)
194
+ n_corpora = len(corpora)
195
+ if n_freqs != n_corpora:
196
+ raise ValueError(
197
+ f"`freqs` and `corpora` must be the same length ({n_freqs} vs. {n_corpora})"
198
+ )
199
+
200
+ if len(freqs) < 2:
201
+ raise ValueError("At least two frequency sets (corpora) need to be provided")
202
+
203
+ return _from_freqs(
204
+ freqs=zip(corpora, freqs),
205
+ token=TOKEN,
206
+ )
207
+
208
+
209
+ def from_dict_of_freqs(
210
+ freqs: DictOfFreqs,
211
+ ) -> CatalogData:
212
+ """
213
+ Creates the data for a new catalog from named token-to-frequency mappings.
214
+
215
+ Args:
216
+ freqs:
217
+ A mapping of corpus names to token-to-frequency mappings, one per corpus. Each
218
+ mapping is `{corpus: {token: frequency}}`, with one mapping per corpus
219
+
220
+ Returns:
221
+ The `CatalogData` needed to initialize a new catalog
222
+
223
+ Raises:
224
+ ValueError: If fewer than two corpora are provided in `freqs`.
225
+ ValueError: If any corpus in `freqs` has no token frequencies.
226
+ """
227
+ if len(freqs) < 2:
228
+ raise ValueError("At least two frequency sets (corpora) need to be provided")
229
+
230
+ return _from_freqs(
231
+ freqs=freqs.items(),
232
+ token=TOKEN,
233
+ )
234
+
235
+
236
+ def _from_freqs(
237
+ freqs: Iterable[tuple[str, Mapping[str, float]]],
238
+ token: str = TOKEN,
239
+ ) -> CatalogData:
240
+ """
241
+ Creates the data for a new catalog from token-frequency data structures
242
+
243
+ Args:
244
+ freqs:
245
+ A sequence of tuples, one per corpus. Each tuple is `(corpus, {token: frequency})`
246
+ token:
247
+ The name the use for the column representing tokens in the produced catalog dataframe
248
+
249
+ Returns:
250
+ The `CatalogData` needed to initialize a new catalog
251
+ """
252
+ freq_cols: list[str] = []
253
+ lf = pl.LazyFrame(schema={token: pl.String})
254
+
255
+ for corpus, token2freq in freqs:
256
+ if not token2freq:
257
+ raise ValueError(f"There are no token frequencies for `{corpus}`")
258
+
259
+ freq_cols.append(corpus)
260
+
261
+ tokens, token_freqs = zip(*token2freq.items())
262
+ lf_corpus = pl.LazyFrame([tokens, token_freqs], schema=[token, corpus])
263
+ lf = lf.join(lf_corpus, on=token, how="full", coalesce=True)
264
+
265
+ return from_polars(
266
+ lf,
267
+ token_col=token,
268
+ freq_cols=freq_cols,
269
+ )
270
+
271
+
272
+ def _normalize_frequency_columns(
273
+ df: pl.LazyFrame,
274
+ freq_cols: Collection[str],
275
+ normalization_suffix: str = NORMALIZATION_SUFFIX,
276
+ ) -> pl.LazyFrame:
277
+ """
278
+ Normalizes columns in a dataframe so all values are in the range $[0, 1]$ and sum to 1. The
279
+ normalized columns are named using a `normalization_suffix`, appended to the original column
280
+ names.
281
+ """
282
+ normed_cols = [
283
+ normalize_freqs(col(freq_col)).alias(f"{freq_col}{normalization_suffix}")
284
+ for freq_col in freq_cols
285
+ ]
286
+
287
+ return df.with_columns(*normed_cols)