polars-list-math 0.1.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 polars-list-math contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: polars-list-math
3
+ Version: 0.1.1
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Rust
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: Implementation :: CPython
13
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: polars>=1.39.3
17
+ License-File: LICENSE
18
+ Summary: List-oriented math and expression helpers for Polars.
19
+ Keywords: polars,list,dataframe,rust,pyo3
20
+ Home-Page: https://github.com/rufrozen/polars-list-math
21
+ License-Expression: MIT
22
+ Requires-Python: >=3.12
23
+ Description-Content-Type: text/markdown
24
+ Project-URL: Documentation, https://github.com/rufrozen/polars-list-math/tree/main/docs
25
+ Project-URL: Homepage, https://github.com/rufrozen/polars-list-math
26
+ Project-URL: Issues, https://github.com/rufrozen/polars-list-math/issues
27
+ Project-URL: Repository, https://github.com/rufrozen/polars-list-math
28
+
29
+ # polars-list-math
30
+
31
+ `polars-list-math` is a Python/Rust package with list-oriented expression
32
+ helpers for Polars.
33
+
34
+ Import the package once to register extra methods on `Expr.list`:
35
+
36
+ ```python
37
+ import polars as pl
38
+ import polars_list_math # noqa: F401
39
+ ```
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install polars-list-math
45
+ # or
46
+ uv add polars-list-math
47
+ ```
48
+
49
+ Requires Python 3.12+ and `polars>=1.39.3`.
50
+
51
+ ## Methods
52
+
53
+ | Method | Result | Docs |
54
+ | --- | --- | --- |
55
+ | `Expr.list.zip(...)` | Zip lists into `list[struct]` | [Polars proposal](https://github.com/pola-rs/polars/issues/22719) |
56
+ | `Expr.list.combinations(...)` | Pair each item with itself and later items in the same list | [Combinations](https://github.com/rufrozen/polars-list-math/blob/main/docs/combinations.md) |
57
+ | `Expr.list.combinations_to(...)` | Pair each item with each item from another list | [Combinations](https://github.com/rufrozen/polars-list-math/blob/main/docs/combinations.md) |
58
+ | `Expr.list.similarity(...)` | Weighted similarity between two lists | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
59
+ | `Expr.list.mean_similarity(...)` | Mean similarity inside a nested-list row | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
60
+ | `Expr.list.mean_similarity_to(...)` | Mean similarity to reference nested lists | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
61
+
62
+ The Python helper `py_list_similarity(...)` computes the same weighted
63
+ similarity for plain Python sequences.
64
+
65
+ If a future Polars release ships native methods with the same names, this
66
+ package leaves the native implementation untouched.
67
+
68
+ ## Quick Examples
69
+
70
+ ```python
71
+ df = pl.DataFrame(
72
+ {
73
+ "a": [[1, 2, 3]],
74
+ "b": [[2, 1, 3]],
75
+ "groups": [[[1, 2, 3], [1, 2, 3], [4, 5, 6]]],
76
+ }
77
+ )
78
+
79
+ df.with_columns(
80
+ pl.col("a").list.similarity("b").alias("similarity"),
81
+ pl.col("groups").list.mean_similarity().alias("mean_similarity"),
82
+ pl.col("a").list.combinations().alias("pairs"),
83
+ pl.col("a").list.zip("b", fields=["a", "b"]).alias("zipped"),
84
+ )
85
+ ```
86
+
87
+ ## Development
88
+
89
+ ```bash
90
+ make install
91
+ make develop
92
+ make test
93
+ ```
94
+
95
+ Build and check the package locally:
96
+
97
+ ```bash
98
+ make check-dist
99
+ ```
100
+
101
+ Release steps are in
102
+ [docs/publishing.md](https://github.com/rufrozen/polars-list-math/blob/main/docs/publishing.md).
103
+
@@ -0,0 +1,74 @@
1
+ # polars-list-math
2
+
3
+ `polars-list-math` is a Python/Rust package with list-oriented expression
4
+ helpers for Polars.
5
+
6
+ Import the package once to register extra methods on `Expr.list`:
7
+
8
+ ```python
9
+ import polars as pl
10
+ import polars_list_math # noqa: F401
11
+ ```
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install polars-list-math
17
+ # or
18
+ uv add polars-list-math
19
+ ```
20
+
21
+ Requires Python 3.12+ and `polars>=1.39.3`.
22
+
23
+ ## Methods
24
+
25
+ | Method | Result | Docs |
26
+ | --- | --- | --- |
27
+ | `Expr.list.zip(...)` | Zip lists into `list[struct]` | [Polars proposal](https://github.com/pola-rs/polars/issues/22719) |
28
+ | `Expr.list.combinations(...)` | Pair each item with itself and later items in the same list | [Combinations](https://github.com/rufrozen/polars-list-math/blob/main/docs/combinations.md) |
29
+ | `Expr.list.combinations_to(...)` | Pair each item with each item from another list | [Combinations](https://github.com/rufrozen/polars-list-math/blob/main/docs/combinations.md) |
30
+ | `Expr.list.similarity(...)` | Weighted similarity between two lists | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
31
+ | `Expr.list.mean_similarity(...)` | Mean similarity inside a nested-list row | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
32
+ | `Expr.list.mean_similarity_to(...)` | Mean similarity to reference nested lists | [Similarity](https://github.com/rufrozen/polars-list-math/blob/main/docs/similarity.md) |
33
+
34
+ The Python helper `py_list_similarity(...)` computes the same weighted
35
+ similarity for plain Python sequences.
36
+
37
+ If a future Polars release ships native methods with the same names, this
38
+ package leaves the native implementation untouched.
39
+
40
+ ## Quick Examples
41
+
42
+ ```python
43
+ df = pl.DataFrame(
44
+ {
45
+ "a": [[1, 2, 3]],
46
+ "b": [[2, 1, 3]],
47
+ "groups": [[[1, 2, 3], [1, 2, 3], [4, 5, 6]]],
48
+ }
49
+ )
50
+
51
+ df.with_columns(
52
+ pl.col("a").list.similarity("b").alias("similarity"),
53
+ pl.col("groups").list.mean_similarity().alias("mean_similarity"),
54
+ pl.col("a").list.combinations().alias("pairs"),
55
+ pl.col("a").list.zip("b", fields=["a", "b"]).alias("zipped"),
56
+ )
57
+ ```
58
+
59
+ ## Development
60
+
61
+ ```bash
62
+ make install
63
+ make develop
64
+ make test
65
+ ```
66
+
67
+ Build and check the package locally:
68
+
69
+ ```bash
70
+ make check-dist
71
+ ```
72
+
73
+ Release steps are in
74
+ [docs/publishing.md](https://github.com/rufrozen/polars-list-math/blob/main/docs/publishing.md).
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from ._list_combinations import install as _install_list_combinations
4
+ from ._list_mean_similarity import install as _install_list_mean_similarity
5
+ from ._list_similarity import (
6
+ install as _install_list_similarity,
7
+ )
8
+ from ._list_similarity import (
9
+ py_list_similarity,
10
+ )
11
+ from ._list_zip import install as _install_list_zip
12
+
13
+ __all__ = [
14
+ "install",
15
+ "py_list_similarity",
16
+ ]
17
+
18
+
19
+ def install(*, overwrite: bool = False) -> None:
20
+ _install_list_combinations(overwrite=overwrite)
21
+ _install_list_zip(overwrite=overwrite)
22
+ _install_list_similarity(overwrite=overwrite)
23
+ _install_list_mean_similarity(overwrite=overwrite)
24
+
25
+
26
+ install()
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import polars as pl
9
+ from polars._utils.wrap import wrap_expr
10
+ from polars.plugins import register_plugin_function
11
+
12
+ IntoExpr = Any
13
+
14
+ _INPUT_PREFIX = "__polars_list_math_list_combinations_input_"
15
+ _LIB = Path(__file__).parent
16
+ _NATIVE_EXTENSIONS = {".so", ".pyd", ".dll", ".dylib"}
17
+ _DEFAULT_LEFT_INDEX = "left_index"
18
+ _DEFAULT_RIGHT_INDEX = "right_index"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class _CombinationFields:
23
+ include_index: bool
24
+ left_index: str
25
+ right_index: str
26
+ left_value: str
27
+ right_value: str
28
+
29
+
30
+ def list_combinations(
31
+ expr: IntoExpr,
32
+ *,
33
+ left_value: str = "left_value",
34
+ right_value: str = "right_value",
35
+ with_index: bool = False,
36
+ left_index: str | None = None,
37
+ right_index: str | None = None,
38
+ skip_null: bool = False,
39
+ ) -> pl.Expr:
40
+ """Build a list expression with all ``i <= j`` pairs from each input list."""
41
+ fields = _normalize_fields(left_index, right_index, left_value, right_value, with_index)
42
+ _validate_skip_null(skip_null)
43
+ if _native_library_available():
44
+ return register_plugin_function(
45
+ plugin_path=_LIB,
46
+ function_name="list_combinations",
47
+ args=[expr],
48
+ kwargs=_kwargs(fields, with_index, skip_null),
49
+ is_elementwise=True,
50
+ use_abs_path=True,
51
+ )
52
+
53
+ parsed_expr = _parse_into_expr(expr)
54
+
55
+ return parsed_expr.map_elements(
56
+ lambda value: _combinations_row(value, fields, skip_null),
57
+ return_dtype=None,
58
+ skip_nulls=False,
59
+ )
60
+
61
+
62
+ def list_combinations_to(
63
+ expr: IntoExpr,
64
+ target: IntoExpr,
65
+ *,
66
+ left_value: str = "left_value",
67
+ right_value: str = "right_value",
68
+ with_index: bool = False,
69
+ left_index: str | None = None,
70
+ right_index: str | None = None,
71
+ skip_null: bool = False,
72
+ ) -> pl.Expr:
73
+ """Build a list expression with all pairs from the input list to a target list."""
74
+ fields = _normalize_fields(left_index, right_index, left_value, right_value, with_index)
75
+ _validate_skip_null(skip_null)
76
+ if _native_library_available():
77
+ return register_plugin_function(
78
+ plugin_path=_LIB,
79
+ function_name="list_combinations_to",
80
+ args=[expr, target],
81
+ kwargs=_kwargs(fields, with_index, skip_null),
82
+ is_elementwise=True,
83
+ use_abs_path=True,
84
+ )
85
+
86
+ parsed_exprs = [_parse_into_expr(expr), _parse_into_expr(target)]
87
+ aliases = [f"{_INPUT_PREFIX}{index}" for index in range(2)]
88
+ row_expr = pl.struct(
89
+ parsed.alias(alias) for parsed, alias in zip(parsed_exprs, aliases, strict=True)
90
+ )
91
+
92
+ def combinations_to_row(row: Any) -> list[dict[str, Any]] | None:
93
+ left, right = _row_values(row, aliases)
94
+ return _combinations_to_row(left, right, fields, skip_null)
95
+
96
+ return row_expr.map_elements(
97
+ combinations_to_row,
98
+ return_dtype=None,
99
+ skip_nulls=False,
100
+ )
101
+
102
+
103
+ def install(*, overwrite: bool = False) -> None:
104
+ """Register the Polars list namespace helpers."""
105
+ list_namespace = type(pl.col("__polars_list_math_list_combinations_probe__").list)
106
+
107
+ if overwrite or not hasattr(list_namespace, "combinations"):
108
+ list_namespace.combinations = _expr_list_combinations # type: ignore
109
+ if overwrite or not hasattr(list_namespace, "combinations_to"):
110
+ list_namespace.combinations_to = _expr_list_combinations_to # type: ignore
111
+
112
+
113
+ def _expr_list_combinations(
114
+ self: Any,
115
+ *,
116
+ left_value: str = "left_value",
117
+ right_value: str = "right_value",
118
+ with_index: bool = False,
119
+ left_index: str | None = None,
120
+ right_index: str | None = None,
121
+ skip_null: bool = False,
122
+ ) -> pl.Expr:
123
+ base_expr = wrap_expr(self._pyexpr)
124
+ return list_combinations(
125
+ base_expr,
126
+ left_value=left_value,
127
+ right_value=right_value,
128
+ with_index=with_index,
129
+ left_index=left_index,
130
+ right_index=right_index,
131
+ skip_null=skip_null,
132
+ )
133
+
134
+
135
+ def _expr_list_combinations_to(
136
+ self: Any,
137
+ target: IntoExpr,
138
+ *,
139
+ left_value: str = "left_value",
140
+ right_value: str = "right_value",
141
+ with_index: bool = False,
142
+ left_index: str | None = None,
143
+ right_index: str | None = None,
144
+ skip_null: bool = False,
145
+ ) -> pl.Expr:
146
+ base_expr = wrap_expr(self._pyexpr)
147
+ return list_combinations_to(
148
+ base_expr,
149
+ target,
150
+ left_value=left_value,
151
+ right_value=right_value,
152
+ with_index=with_index,
153
+ left_index=left_index,
154
+ right_index=right_index,
155
+ skip_null=skip_null,
156
+ )
157
+
158
+
159
+ def _combinations_row(
160
+ value: Any,
161
+ fields: _CombinationFields,
162
+ skip_null: bool,
163
+ ) -> list[dict[str, Any]] | None:
164
+ values = _list_or_none(value, "list.combinations")
165
+ if values is None:
166
+ return None
167
+
168
+ out: list[dict[str, Any]] = []
169
+ for left_index in range(len(values)):
170
+ for right_index in range(left_index, len(values)):
171
+ left_value = values[left_index]
172
+ right_value = values[right_index]
173
+ if skip_null and (left_value is None or right_value is None):
174
+ continue
175
+ out.append(_pair_record(fields, left_index, right_index, left_value, right_value))
176
+ return out
177
+
178
+
179
+ def _combinations_to_row(
180
+ left: Any,
181
+ right: Any,
182
+ fields: _CombinationFields,
183
+ skip_null: bool,
184
+ ) -> list[dict[str, Any]] | None:
185
+ left_values = _list_or_none(left, "list.combinations_to")
186
+ right_values = _list_or_none(right, "list.combinations_to")
187
+ if left_values is None or right_values is None:
188
+ return None
189
+
190
+ out: list[dict[str, Any]] = []
191
+ for left_index, left_value in enumerate(left_values):
192
+ for right_index, right_value in enumerate(right_values):
193
+ if skip_null and (left_value is None or right_value is None):
194
+ continue
195
+ out.append(_pair_record(fields, left_index, right_index, left_value, right_value))
196
+ return out
197
+
198
+
199
+ def _pair_record(
200
+ fields: _CombinationFields,
201
+ left_index: int,
202
+ right_index: int,
203
+ left_value: Any,
204
+ right_value: Any,
205
+ ) -> dict[str, Any]:
206
+ out: dict[str, Any] = {}
207
+ if fields.include_index:
208
+ out[fields.left_index] = left_index
209
+ out[fields.right_index] = right_index
210
+ out[fields.left_value] = left_value
211
+ out[fields.right_value] = right_value
212
+ return out
213
+
214
+
215
+ def _normalize_fields(
216
+ left_index: str | None,
217
+ right_index: str | None,
218
+ left_value: str,
219
+ right_value: str,
220
+ with_index: bool,
221
+ ) -> _CombinationFields:
222
+ _validate_with_index(with_index)
223
+ include_index = with_index or left_index is not None or right_index is not None
224
+ resolved_left_index = _DEFAULT_LEFT_INDEX if left_index is None else left_index
225
+ resolved_right_index = _DEFAULT_RIGHT_INDEX if right_index is None else right_index
226
+
227
+ index_names = (resolved_left_index, resolved_right_index) if include_index else ()
228
+ field_names = (*index_names, left_value, right_value)
229
+ if not all(isinstance(field, str) for field in field_names):
230
+ msg = "combination field names must be strings"
231
+ raise TypeError(msg)
232
+ if not (left_index is None or isinstance(left_index, str)) or not (
233
+ right_index is None or isinstance(right_index, str)
234
+ ):
235
+ msg = "combination index field names must be strings or None"
236
+ raise TypeError(msg)
237
+ if len(set(field_names)) != len(field_names):
238
+ msg = "combination field names must be unique"
239
+ raise ValueError(msg)
240
+ return _CombinationFields(
241
+ include_index=include_index,
242
+ left_index=resolved_left_index,
243
+ right_index=resolved_right_index,
244
+ left_value=left_value,
245
+ right_value=right_value,
246
+ )
247
+
248
+
249
+ def _validate_with_index(with_index: bool) -> None:
250
+ if not isinstance(with_index, bool):
251
+ msg = "with_index must be a bool"
252
+ raise TypeError(msg)
253
+
254
+
255
+ def _validate_skip_null(skip_null: bool) -> None:
256
+ if not isinstance(skip_null, bool):
257
+ msg = "skip_null must be a bool"
258
+ raise TypeError(msg)
259
+
260
+
261
+ def _kwargs(fields: _CombinationFields, with_index: bool, skip_null: bool) -> dict[str, Any]:
262
+ return {
263
+ "left_index": fields.left_index if fields.include_index else None,
264
+ "right_index": fields.right_index if fields.include_index else None,
265
+ "left_value": fields.left_value,
266
+ "right_value": fields.right_value,
267
+ "with_index": with_index,
268
+ "skip_null": skip_null,
269
+ }
270
+
271
+
272
+ def _parse_into_expr(expr: IntoExpr) -> pl.Expr:
273
+ if isinstance(expr, pl.Expr):
274
+ return expr
275
+ if isinstance(expr, str):
276
+ return pl.col(expr)
277
+ return pl.lit(expr)
278
+
279
+
280
+ def _list_or_none(value: Any, name: str) -> list[Any] | None:
281
+ if value is None:
282
+ return None
283
+ if isinstance(value, pl.Series):
284
+ return value.to_list()
285
+ if not isinstance(value, list):
286
+ msg = f"{name} expects a List expression"
287
+ raise TypeError(msg)
288
+ return value
289
+
290
+
291
+ def _row_values(row: Any, aliases: list[str]) -> list[Any]:
292
+ if row is None:
293
+ return [None] * len(aliases)
294
+ if isinstance(row, Mapping):
295
+ return [row[alias] for alias in aliases]
296
+ return list(row)
297
+
298
+
299
+ def _native_library_available() -> bool:
300
+ return any(path.is_file() and path.suffix in _NATIVE_EXTENSIONS for path in _LIB.iterdir())