PySpark-Column-Selectors 0.1.3__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.
- PySparkSelectors/__init__.py +240 -0
- PySparkSelectors/__main__.py +4 -0
- PySparkSelectors/cli.py +18 -0
- PySparkSelectors/models.py +1564 -0
- PySparkSelectors/py.typed +1 -0
- PySparkSelectors/spark_overrides.py +677 -0
- PySparkSelectors/user_functions.py +745 -0
- PySparkSelectors/utils.py +298 -0
- pyspark_column_selectors-0.1.3.dist-info/METADATA +674 -0
- pyspark_column_selectors-0.1.3.dist-info/RECORD +13 -0
- pyspark_column_selectors-0.1.3.dist-info/WHEEL +4 -0
- pyspark_column_selectors-0.1.3.dist-info/entry_points.txt +2 -0
- pyspark_column_selectors-0.1.3.dist-info/licenses/LICENSE +279 -0
|
@@ -0,0 +1,1564 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import operator
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
from typing import Any, Union
|
|
6
|
+
|
|
7
|
+
import pyspark.sql.types as T
|
|
8
|
+
from pyspark.errors import PySparkNotImplementedError, PySparkTypeError, PySparkValueError
|
|
9
|
+
from pyspark.sql import DataFrame
|
|
10
|
+
from pyspark.sql import functions as F
|
|
11
|
+
from pyspark.sql.column import Column
|
|
12
|
+
|
|
13
|
+
from .utils import (
|
|
14
|
+
_get_column_method,
|
|
15
|
+
_quoted_col,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
""" enable support for many different types of pyspark environments"""
|
|
19
|
+
# if your environment isn't supported then you can add it to this section.
|
|
20
|
+
# Spark supports multiple DataFrame implementations. Keep every available runtime
|
|
21
|
+
# class in the patch set: public/classic DataFrame and Spark Connect DataFrame.
|
|
22
|
+
# using try except as to allow passthrough if environment doesn't support these.
|
|
23
|
+
try:
|
|
24
|
+
from pyspark.sql.classic.dataframe import DataFrame as _ClassicDataFrame
|
|
25
|
+
except ImportError:
|
|
26
|
+
_ClassicDataFrame = None
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from pyspark.sql.connect.dataframe import DataFrame as _ConnectDataFrame
|
|
30
|
+
except ImportError:
|
|
31
|
+
_ConnectDataFrame = None
|
|
32
|
+
|
|
33
|
+
_DATAFRAME_CLASSES = tuple(
|
|
34
|
+
dict.fromkeys(cls for cls in (DataFrame, _ClassicDataFrame, _ConnectDataFrame) if cls is not None)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
from pyspark.sql.connect.column import Column as _ConnectColumn
|
|
39
|
+
except ImportError:
|
|
40
|
+
_ConnectColumn = None
|
|
41
|
+
|
|
42
|
+
_COLUMN_CLASSES = tuple(dict.fromkeys(cls for cls in (Column, _ConnectColumn) if cls is not None))
|
|
43
|
+
|
|
44
|
+
# --------------------------------------------------------------------------------
|
|
45
|
+
# Make column selectors allow column level transformations
|
|
46
|
+
# including support for arithmetic, comparison, and spark functions
|
|
47
|
+
# --------------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SelectorcolumnOperations:
|
|
51
|
+
"""Mixin providing per-column transform chaining for selector objects.
|
|
52
|
+
|
|
53
|
+
Every selector class (`DTypeSelector`, `IndexSelector`, `RegexSelector`, and
|
|
54
|
+
any future selector) inherits from this class (via `BaseSelector`) so that
|
|
55
|
+
arithmetic operators (``+``, ``-``, ``*``, ...), comparison operators
|
|
56
|
+
(``==``, ``>``, ...), and any function available on
|
|
57
|
+
`pyspark.sql.functions` (``.upper()``, ``.sum()``, ``.cast(...)``, ...) can
|
|
58
|
+
be chained directly onto a selector, e.g. ``by_dtype([T.IntegerType]) + 1`` or
|
|
59
|
+
``by_dtype([T.StringType]).upper()``. Each chained call does not mutate the
|
|
60
|
+
selector in place -- it returns a new, immutable copy with the extra
|
|
61
|
+
transform appended, so the original selector is always safe to reuse.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
transforms : list of callable, optional
|
|
66
|
+
A list of one-argument functions, each accepting and returning a
|
|
67
|
+
`pyspark.sql.Column`, representing the transform pipeline already
|
|
68
|
+
chained onto this selector. Defaults to an empty list when not given.
|
|
69
|
+
|
|
70
|
+
Attributes
|
|
71
|
+
----------
|
|
72
|
+
transforms : list of callable
|
|
73
|
+
The transform pipeline described above. Applied, in order, to every
|
|
74
|
+
matched column's `pyspark.sql.Column` expression by `resolve_columns`.
|
|
75
|
+
|
|
76
|
+
Examples
|
|
77
|
+
--------
|
|
78
|
+
>>> by_dtype([T.IntegerType]) + 1
|
|
79
|
+
<DTypeSelector ...>
|
|
80
|
+
>>> by_dtype([T.StringType]).upper()
|
|
81
|
+
<DTypeSelector ...>
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, transforms: list[Callable[[Column], Column]] | None = None) -> None:
|
|
85
|
+
"""Initialize the transform pipeline.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
transforms : list of callable, optional
|
|
90
|
+
Existing transform pipeline to start from. Defaults to an empty
|
|
91
|
+
list when not given.
|
|
92
|
+
"""
|
|
93
|
+
self.transforms = transforms or []
|
|
94
|
+
|
|
95
|
+
# tracks a pending rename (`.prefix()`/`.suffix()`/`.map_alias()`) as
|
|
96
|
+
# pure name metadata -- a `str -> str` function applied to the
|
|
97
|
+
# originally-matched column name -- kept completely separate from
|
|
98
|
+
# `self.transforms` (the *value* pipeline). This is what
|
|
99
|
+
# `_resolve_selector_exprs` uses to compute each matched column's
|
|
100
|
+
# final output name; it deliberately never touches the `Column`
|
|
101
|
+
# expression itself mid-chain, so it can't be corrupted by whatever
|
|
102
|
+
# value transform text (`.cast(...)`, arithmetic, ...) precedes it,
|
|
103
|
+
# and it never appears in `self.transforms`, so `~`/`&`/`|`'s
|
|
104
|
+
# "does this selector already have a transform chained onto it?"
|
|
105
|
+
# filter-condition check isn't tripped by a rename alone -- see
|
|
106
|
+
# "prefix / suffix and map name fx's" section.
|
|
107
|
+
self._name_transform: Callable[[str], str] | None = None
|
|
108
|
+
|
|
109
|
+
# --------------------------------------------------
|
|
110
|
+
# utility used by every operator/function
|
|
111
|
+
# --------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _copy(self, transform: Callable[[Column], Column]) -> "SelectorcolumnOperations":
|
|
114
|
+
"""Return an immutable copy of this selector with one more transform appended.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
transform : callable
|
|
119
|
+
A one-argument function accepting and returning a
|
|
120
|
+
`pyspark.sql.Column`, appended to the end of the copy's transform
|
|
121
|
+
pipeline.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
SelectorcolumnOperations
|
|
126
|
+
A new instance of the same concrete class, with all of this
|
|
127
|
+
instance's attributes copied over and `transform` appended to
|
|
128
|
+
`transforms`. `self` itself is left unmodified.
|
|
129
|
+
"""
|
|
130
|
+
# allows to immutably create new versions of the class depending on the transformation that occurs.
|
|
131
|
+
new = self.__class__.__new__(self.__class__)
|
|
132
|
+
|
|
133
|
+
new.__dict__.update(self.__dict__)
|
|
134
|
+
|
|
135
|
+
new.transforms = self.transforms + [transform]
|
|
136
|
+
|
|
137
|
+
return new
|
|
138
|
+
|
|
139
|
+
def _with_name_transform(self, name_transform: Callable[[str], str]) -> "SelectorcolumnOperations":
|
|
140
|
+
"""Return an immutable copy of this selector with a rename queued.
|
|
141
|
+
|
|
142
|
+
Backs `.prefix()`/`.suffix()`/`.map_alias()`. Unlike `_copy`, this
|
|
143
|
+
never touches `self.transforms` (the value pipeline) or the
|
|
144
|
+
selector's `Column` expressions at all -- it only records how the
|
|
145
|
+
*matched column name* should be transformed once real names are
|
|
146
|
+
known (in `_resolve_selector_exprs`), composing with any
|
|
147
|
+
rename already queued so multiple renames chain in order
|
|
148
|
+
(e.g. ``.prefix('a_').suffix('_b')``).
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
name_transform : callable
|
|
153
|
+
A one-argument function accepting a column's current name (`str`)
|
|
154
|
+
and returning its new name (`str`).
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
SelectorcolumnOperations
|
|
159
|
+
A new instance of the same concrete class, with all of this
|
|
160
|
+
instance's attributes copied over and `_name_transform` set to
|
|
161
|
+
`name_transform` composed after any already queued. `self` itself
|
|
162
|
+
is left unmodified.
|
|
163
|
+
"""
|
|
164
|
+
new = self.__class__.__new__(self.__class__)
|
|
165
|
+
|
|
166
|
+
new.__dict__.update(self.__dict__)
|
|
167
|
+
|
|
168
|
+
previous = self._name_transform
|
|
169
|
+
|
|
170
|
+
if previous is None:
|
|
171
|
+
new._name_transform = name_transform
|
|
172
|
+
else:
|
|
173
|
+
new._name_transform = lambda name: name_transform(previous(name))
|
|
174
|
+
|
|
175
|
+
return new
|
|
176
|
+
|
|
177
|
+
def prefix(self, prefix: str) -> "SelectorcolumnOperations":
|
|
178
|
+
"""Queue a rename prepending `prefix` to every matched column's name.
|
|
179
|
+
|
|
180
|
+
Mirrors polars' `Expr.prefix`, but implemented at the selector level
|
|
181
|
+
(via `_with_name_transform`) rather than by dispatching to
|
|
182
|
+
`pyspark.sql.Column.prefix` -- this keeps the rename as pure name
|
|
183
|
+
metadata instead of an early `.alias(...)` baked into the value
|
|
184
|
+
expression, so it composes correctly with any value transform
|
|
185
|
+
(`.cast(...)`, arithmetic, ...) chained before or after it, and does
|
|
186
|
+
not affect `~`/`&`/`|`'s filter-condition detection.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
prefix : str
|
|
191
|
+
The string to prepend to each matched column's current name.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
SelectorcolumnOperations
|
|
196
|
+
A new selector with the rename queued.
|
|
197
|
+
|
|
198
|
+
Examples
|
|
199
|
+
--------
|
|
200
|
+
>>> df.select(by_dtype([T.IntegerType]).prefix('int_'))
|
|
201
|
+
DataFrame[...]
|
|
202
|
+
>>> df.select((by_dtype([T.IntegerType]) + 1).prefix('int_'))
|
|
203
|
+
DataFrame[...]
|
|
204
|
+
"""
|
|
205
|
+
return self._with_name_transform(lambda name: f"{prefix}{name}")
|
|
206
|
+
|
|
207
|
+
def suffix(self, suffix: str) -> "SelectorcolumnOperations":
|
|
208
|
+
"""Queue a rename appending `suffix` to every matched column's name.
|
|
209
|
+
|
|
210
|
+
Mirrors polars' `Expr.suffix`. See `prefix` for why this is
|
|
211
|
+
implemented at the selector level instead of dispatching to
|
|
212
|
+
`pyspark.sql.Column.suffix`.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
suffix : str
|
|
217
|
+
The string to append to each matched column's current name.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
SelectorcolumnOperations
|
|
222
|
+
A new selector with the rename queued.
|
|
223
|
+
|
|
224
|
+
Examples
|
|
225
|
+
--------
|
|
226
|
+
>>> df.select(by_dtype([T.IntegerType]).suffix('_int'))
|
|
227
|
+
DataFrame[...]
|
|
228
|
+
"""
|
|
229
|
+
return self._with_name_transform(lambda name: f"{name}{suffix}")
|
|
230
|
+
|
|
231
|
+
def map_alias(self, func: Callable[[str], str]) -> "SelectorcolumnOperations":
|
|
232
|
+
"""Queue a rename computed by applying `func` to each matched column's name.
|
|
233
|
+
|
|
234
|
+
Mirrors polars' `Expr.map_alias`. `.prefix()`/`.suffix()` are both
|
|
235
|
+
just `.map_alias()` with a prepend/append built in. See `prefix` for
|
|
236
|
+
why this is implemented at the selector level instead of dispatching
|
|
237
|
+
to `pyspark.sql.Column.map_alias`.
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
func : callable
|
|
242
|
+
A one-argument function accepting a column's current name (`str`)
|
|
243
|
+
and returning its new name (`str`).
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
SelectorcolumnOperations
|
|
248
|
+
A new selector with the rename queued.
|
|
249
|
+
|
|
250
|
+
Examples
|
|
251
|
+
--------
|
|
252
|
+
>>> df.select(by_dtype([T.StringType]).map_alias(lambda n: n.upper()))
|
|
253
|
+
DataFrame[...]
|
|
254
|
+
"""
|
|
255
|
+
return self._with_name_transform(func)
|
|
256
|
+
|
|
257
|
+
# --------------------------------------------------
|
|
258
|
+
# operator decorator
|
|
259
|
+
# --------------------------------------------------
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def operator_wrapper(op: Callable[..., Any]) -> Callable[..., "SelectorcolumnOperations"]:
|
|
263
|
+
"""Build a dunder-method implementation that chains a Python operator.
|
|
264
|
+
|
|
265
|
+
Used to define every arithmetic/comparison dunder on this class
|
|
266
|
+
(``__add__``, ``__gt__``, ...) in one line each, e.g.
|
|
267
|
+
``__add__ = operator_wrapper(operator.add)``.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
op : callable
|
|
272
|
+
A function from the standard library `operator` module (or
|
|
273
|
+
anything with the same shape) applied to each matched column's
|
|
274
|
+
`pyspark.sql.Column` expression once the selector is resolved
|
|
275
|
+
against a real dataframe. Binary operators receive
|
|
276
|
+
``(column, other)``; unary operators such as
|
|
277
|
+
``operator.invert`` receive only ``column``.
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
-------
|
|
281
|
+
callable
|
|
282
|
+
A dunder-method-shaped function that appends the corresponding
|
|
283
|
+
operator call to the selector's transform pipeline via `_copy`
|
|
284
|
+
and returns the resulting new selector. Binary dunders accept
|
|
285
|
+
``(self, other)``; unary dunders accept only ``self``.
|
|
286
|
+
|
|
287
|
+
Examples
|
|
288
|
+
--------
|
|
289
|
+
>>> __add__ = operator_wrapper(operator.add)
|
|
290
|
+
>>> by_dtype([T.IntegerType]) + 1
|
|
291
|
+
<DTypeSelector ...>
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def overload(self, other):
|
|
295
|
+
|
|
296
|
+
return self._copy(lambda c: op(c, other))
|
|
297
|
+
|
|
298
|
+
return overload
|
|
299
|
+
|
|
300
|
+
# --------------------------------------------------
|
|
301
|
+
# spark function decorator
|
|
302
|
+
# --------------------------------------------------
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def spark_wrapper(spark_func: Callable[..., Column]) -> Callable[..., "SelectorcolumnOperations"]:
|
|
306
|
+
"""Build a chainable method that wraps one `pyspark.sql.functions` function.
|
|
307
|
+
|
|
308
|
+
Used both explicitly (e.g. `cast`) and automatically, via the
|
|
309
|
+
``for fx in dir(F): ...`` loop below, to make every callable in
|
|
310
|
+
`pyspark.sql.functions` available as a chained method on any selector,
|
|
311
|
+
e.g. ``by_dtype([T.StringType]).upper()`` (wrapping `pyspark.sql.functions.upper`).
|
|
312
|
+
|
|
313
|
+
Parameters
|
|
314
|
+
----------
|
|
315
|
+
spark_func : callable
|
|
316
|
+
A function from `pyspark.sql.functions` (or any function with the
|
|
317
|
+
same shape, e.g. ``lambda c, dtype: c.cast(dtype)``) whose first
|
|
318
|
+
argument is the `pyspark.sql.Column` to operate on.
|
|
319
|
+
|
|
320
|
+
Returns
|
|
321
|
+
-------
|
|
322
|
+
callable
|
|
323
|
+
A chainable method ``wrapper(self, *args, **kwargs)`` that appends
|
|
324
|
+
``lambda c: spark_func(c, *args, **kwargs)`` to the selector's
|
|
325
|
+
transform pipeline via `_copy`, and returns the resulting new
|
|
326
|
+
selector. Carries `spark_func`'s own name/docstring (via
|
|
327
|
+
`functools.wraps`) so ``help(some_selector.upper)`` shows the real
|
|
328
|
+
`pyspark.sql.functions.upper` documentation.
|
|
329
|
+
|
|
330
|
+
Examples
|
|
331
|
+
--------
|
|
332
|
+
>>> upper = spark_wrapper(F.upper)
|
|
333
|
+
>>> by_dtype([T.StringType]).upper()
|
|
334
|
+
<DTypeSelector ...>
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
@functools.wraps(spark_func)
|
|
338
|
+
def wrapper(self, *args, **kwargs):
|
|
339
|
+
|
|
340
|
+
return self._copy(lambda c: spark_func(c, *args, **kwargs))
|
|
341
|
+
|
|
342
|
+
return wrapper
|
|
343
|
+
|
|
344
|
+
def _make_dispatching_column_wrapper(method_name: str) -> Callable[..., Column]:
|
|
345
|
+
"""Build a wrapper that dispatches to `Column` or `pyspark.sql.functions` by argument type.
|
|
346
|
+
|
|
347
|
+
Resolves the `F`/`Column` name-collision. this is where there are functions
|
|
348
|
+
in `pyspark.sql.functions.<method_name>` and `pyspark.sql.Column.<method_name>`
|
|
349
|
+
share a name but treat a plain (non-`Column`) argument differently. This
|
|
350
|
+
wrapper picks whichever one matches the caller's intent based on the type of
|
|
351
|
+
the argument actually passed, so both usages keep working.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
method_name : str
|
|
356
|
+
Name shared by a `pyspark.sql.functions` function and a `pyspark.sql.Column`
|
|
357
|
+
instance method (e.g. `"contains"`), used to look up both versions.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
callable
|
|
362
|
+
A function `wrapper(c, *args, **kwargs)` that, when the first
|
|
363
|
+
argument in `args` (if any) is a `pyspark.sql.Column`, calls
|
|
364
|
+
`pyspark.sql.functions.<method_name>(c, *args, **kwargs)` (column-to-column
|
|
365
|
+
semantics); otherwise calls `getattr(c, method_name)(*args, **kwargs)`
|
|
366
|
+
(literal semantics via `Column`'s own bound method). Carries `Column`'s
|
|
367
|
+
docstring for the method so `help()` on the resulting chained selector
|
|
368
|
+
method shows accurate documentation.
|
|
369
|
+
"""
|
|
370
|
+
column_method = _get_column_method(method_name)
|
|
371
|
+
spark_func = getattr(F, method_name)
|
|
372
|
+
|
|
373
|
+
def wrapper(c: Column, *args: Any, **kwargs: Any) -> Column:
|
|
374
|
+
# `_COLUMN_CLASSES` (classic + Spark Connect `Column`, when Connect is
|
|
375
|
+
# available) instead of the bare `Column` name -- otherwise a Spark
|
|
376
|
+
# Connect `Column` argument would be misdetected as a plain literal
|
|
377
|
+
# and routed to the wrong (literal-semantics) branch below.
|
|
378
|
+
if args and isinstance(args[0], _COLUMN_CLASSES):
|
|
379
|
+
return spark_func(c, *args, **kwargs)
|
|
380
|
+
return getattr(c, method_name)(*args, **kwargs)
|
|
381
|
+
|
|
382
|
+
wrapper.__name__ = method_name
|
|
383
|
+
wrapper.__doc__ = column_method.__doc__
|
|
384
|
+
# same rename-op marker carry-through as `_make_column_only_wrapper`
|
|
385
|
+
# (defensive -- no current rename op collides with an `F` name).
|
|
386
|
+
# wrapper._is_rename_op = getattr(column_method, "_is_rename_op", False)
|
|
387
|
+
return wrapper
|
|
388
|
+
|
|
389
|
+
def resolve_columns(self: "BaseSelector", df: DataFrame) -> list[Column]:
|
|
390
|
+
"""Resolve this selector's matched columns into transformed `Column` expressions.
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
df : pyspark.sql.DataFrame
|
|
395
|
+
The dataframe to resolve the selector's matched column names
|
|
396
|
+
against, and to build each `pyspark.sql.Column` expression from.
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
list of pyspark.sql.Column
|
|
401
|
+
One `pyspark.sql.Column` expression per matched column, in
|
|
402
|
+
`resolve`'s order, with every transform in `self.transforms`
|
|
403
|
+
applied in sequence (e.g. ``by_dtype([T.IntegerType]) + 1`` returns
|
|
404
|
+
``F.col(name) + 1`` for every matched int column).
|
|
405
|
+
|
|
406
|
+
Examples
|
|
407
|
+
--------
|
|
408
|
+
>>> by_dtype([T.IntegerType]).resolve_columns(df)
|
|
409
|
+
[Column<'(integer_col + 0)'>, ...]
|
|
410
|
+
"""
|
|
411
|
+
cols = [_quoted_col(c) for c in self.resolve(df)]
|
|
412
|
+
|
|
413
|
+
for transform in self.transforms:
|
|
414
|
+
cols = [transform(c) for c in cols]
|
|
415
|
+
|
|
416
|
+
return cols
|
|
417
|
+
|
|
418
|
+
# --------------------------------------------------
|
|
419
|
+
# apply transforms
|
|
420
|
+
# --------------------------------------------------
|
|
421
|
+
|
|
422
|
+
__add__ = operator_wrapper(operator.add)
|
|
423
|
+
__sub__ = operator_wrapper(operator.sub)
|
|
424
|
+
__mul__ = operator_wrapper(operator.mul)
|
|
425
|
+
__truediv__ = operator_wrapper(operator.truediv)
|
|
426
|
+
__mod__ = operator_wrapper(operator.mod)
|
|
427
|
+
__pow__ = operator_wrapper(operator.pow)
|
|
428
|
+
# __len__ = operator_wrapper(operator.len)
|
|
429
|
+
__floordiv__ = operator_wrapper(operator.floordiv)
|
|
430
|
+
|
|
431
|
+
__eq__ = operator_wrapper(operator.eq)
|
|
432
|
+
__gt__ = operator_wrapper(operator.gt)
|
|
433
|
+
__ge__ = operator_wrapper(operator.ge)
|
|
434
|
+
__lt__ = operator_wrapper(operator.lt)
|
|
435
|
+
__le__ = operator_wrapper(operator.le)
|
|
436
|
+
__ne__ = operator_wrapper(operator.ne)
|
|
437
|
+
|
|
438
|
+
# __contains__ = operator_wrapper(operator.contains)
|
|
439
|
+
# __and__ = operator_wrapper(operator.and)
|
|
440
|
+
# __or__ = operator_wrapper(operator.or)
|
|
441
|
+
|
|
442
|
+
__getitem__ = operator_wrapper(operator.getitem)
|
|
443
|
+
# __setitem__ = operator_wrapper(operator.setitem)
|
|
444
|
+
# __delitem__ = operator_wrapper(operator.delitem)
|
|
445
|
+
__invert__ = operator_wrapper(operator.invert)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# --------------------------------------------------------------------------------
|
|
449
|
+
# Allow support for column selector level operations
|
|
450
|
+
# --------------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class SelectorSelectionOperations:
|
|
454
|
+
"""Mixin providing set-algebra and filter-condition operators for selectors.
|
|
455
|
+
|
|
456
|
+
Every selector class (`DTypeSelector`, `IndexSelector`, `RegexSelector`,
|
|
457
|
+
and any future selector) inherits from this class (via `BaseSelector`) so
|
|
458
|
+
that ``~ - & ^ |`` compose selectors together, e.g.
|
|
459
|
+
``by_dtype([T.IntegerType]) | by_dtype([T.StringType])`` (union) or
|
|
460
|
+
``~by_dtype([T.StringType])`` (every column that is NOT a string column).
|
|
461
|
+
|
|
462
|
+
These same operators (``&``, ``|``, ``~``) switch to "expression mode"
|
|
463
|
+
once a selector already has a comparison/arithmetic transform chained
|
|
464
|
+
onto it (e.g. ``by_dtype([T.IntegerType]) > 0``), or the other operand isn't a
|
|
465
|
+
bare selector at all (a plain `pyspark.sql.Column`). In that mode, they
|
|
466
|
+
combine row-conditions for `filter`/`where`
|
|
467
|
+
(e.g. ``df.filter(by_dtype([T.IntegerType]) > 0)``) instead of intersecting/
|
|
468
|
+
unioning which columns are matched -- see `SelectorFilterCondition`.
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
resolver : callable, optional
|
|
473
|
+
A one-argument function ``resolver(df) -> list[str]`` built by
|
|
474
|
+
`_selector_copy` when a set operator (``~ - & ^ |``) combines two
|
|
475
|
+
selectors. When present, `resolve` (defined on `BaseSelector`
|
|
476
|
+
subclasses) must call this instead of its own default matching
|
|
477
|
+
logic. Defaults to `None` when not given.
|
|
478
|
+
|
|
479
|
+
Attributes
|
|
480
|
+
----------
|
|
481
|
+
_resolver : callable or None
|
|
482
|
+
The resolver function described above, or `None` when this selector
|
|
483
|
+
has not been produced by a set operator.
|
|
484
|
+
|
|
485
|
+
Examples
|
|
486
|
+
--------
|
|
487
|
+
>>> by_dtype([T.IntegerType]) | by_dtype([T.StringType])
|
|
488
|
+
<DTypeSelector ...>
|
|
489
|
+
>>> ~by_dtype([T.StringType])
|
|
490
|
+
<DTypeSelector ...>
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
def __init__(self, resolver: Callable[[DataFrame], list[str]] | None = None) -> None:
|
|
494
|
+
"""Initialize the combinator resolver.
|
|
495
|
+
|
|
496
|
+
Parameters
|
|
497
|
+
----------
|
|
498
|
+
resolver : callable, optional
|
|
499
|
+
Existing resolver function to start from. Defaults to `None` when
|
|
500
|
+
not given.
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
self._resolver = resolver
|
|
504
|
+
|
|
505
|
+
def _selector_copy(self, resolver: Callable[[DataFrame], list[str]]) -> "SelectorSelectionOperations":
|
|
506
|
+
"""Return an immutable copy of this selector with a combined resolver.
|
|
507
|
+
|
|
508
|
+
Parameters
|
|
509
|
+
----------
|
|
510
|
+
resolver : callable
|
|
511
|
+
A one-argument function ``resolver(df) -> list[str]`` implementing
|
|
512
|
+
the combined set-algebra result (union, intersection, ...).
|
|
513
|
+
|
|
514
|
+
Returns
|
|
515
|
+
-------
|
|
516
|
+
SelectorSelectionOperations
|
|
517
|
+
A new instance of the same concrete class, with all of this
|
|
518
|
+
instance's attributes copied over and `_resolver` set to
|
|
519
|
+
`resolver`. `self` itself is left unmodified.
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
new = self.__class__.__new__(self.__class__)
|
|
523
|
+
|
|
524
|
+
new.__dict__.update(self.__dict__)
|
|
525
|
+
|
|
526
|
+
new._resolver = resolver
|
|
527
|
+
|
|
528
|
+
return new
|
|
529
|
+
|
|
530
|
+
def __and__(
|
|
531
|
+
self: "BaseSelector", other: Union["BaseSelector", Column, "SelectorFilterCondition"]
|
|
532
|
+
) -> Union["SelectorSelectionOperations", "SelectorFilterCondition"]:
|
|
533
|
+
"""Intersect two selectors' matched columns, or AND two row conditions.
|
|
534
|
+
|
|
535
|
+
Parameters
|
|
536
|
+
----------
|
|
537
|
+
other : BaseSelector, pyspark.sql.Column, or SelectorFilterCondition
|
|
538
|
+
The right-hand operand of ``&``.
|
|
539
|
+
|
|
540
|
+
Returns
|
|
541
|
+
-------
|
|
542
|
+
SelectorSelectionOperations or SelectorFilterCondition
|
|
543
|
+
A new selector matching only columns present in both `self` and
|
|
544
|
+
`other` (set-algebra mode), or a `SelectorFilterCondition` ANDing
|
|
545
|
+
the two row conditions together (expression mode -- triggered
|
|
546
|
+
when `self` already has a chained transform, or `other` isn't a
|
|
547
|
+
bare selector).
|
|
548
|
+
|
|
549
|
+
Examples
|
|
550
|
+
--------
|
|
551
|
+
>>> by_dtype([T.DoubleType, T.IntegerType]) & by_dtype([T.DoubleType, T.StringType])
|
|
552
|
+
<DTypeSelector ...>
|
|
553
|
+
>>> df.filter((by_dtype([T.IntegerType]) > 0) & (by_dtype([T.IntegerType]) < 9))
|
|
554
|
+
DataFrame[...]
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
# expression mode: `self` already has a comparison/arithmetic transform
|
|
558
|
+
# chained onto it (e.g. `by_dtype([T.IntegerType]) == 0`), or `other` isn't a raw
|
|
559
|
+
# selector at all (a plain pyspark Column like `F.col('x') == None`, or
|
|
560
|
+
# another transformed selector). in that case `&` means "AND these row
|
|
561
|
+
# conditions together", not "intersect these two selectors' matched
|
|
562
|
+
# columns" -- so it's deferred into a SelectorFilterCondition instead of
|
|
563
|
+
# the name-set intersection below.
|
|
564
|
+
if self.transforms or not isinstance(other, BaseSelector) or getattr(other, "transforms", None):
|
|
565
|
+
return SelectorFilterCondition(self, "and", other)
|
|
566
|
+
|
|
567
|
+
left = self
|
|
568
|
+
right = other
|
|
569
|
+
|
|
570
|
+
def _resolve(df):
|
|
571
|
+
|
|
572
|
+
right_cols = set(right.resolve(df))
|
|
573
|
+
|
|
574
|
+
return [dim for dim in left.resolve(df) if dim in right_cols]
|
|
575
|
+
|
|
576
|
+
return self._selector_copy(_resolve)
|
|
577
|
+
|
|
578
|
+
def __or__(
|
|
579
|
+
self: "BaseSelector", other: Union["BaseSelector", Column, "SelectorFilterCondition"]
|
|
580
|
+
) -> Union["SelectorSelectionOperations", "SelectorFilterCondition"]:
|
|
581
|
+
"""Union two selectors' matched columns, or OR two row conditions.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
other : BaseSelector, pyspark.sql.Column, or SelectorFilterCondition
|
|
586
|
+
The right-hand operand of ``|``.
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
SelectorSelectionOperations or SelectorFilterCondition
|
|
591
|
+
A new selector matching every column present in `self` or
|
|
592
|
+
`other` (set-algebra mode), or a `SelectorFilterCondition` ORing
|
|
593
|
+
the two row conditions together (expression mode -- same trigger
|
|
594
|
+
as `__and__`).
|
|
595
|
+
|
|
596
|
+
Examples
|
|
597
|
+
--------
|
|
598
|
+
>>> by_dtype([T.IntegerType]) | by_dtype([T.StringType])
|
|
599
|
+
<DTypeSelector ...>
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
# same expression-mode guard as __and__ -- see the comment there.
|
|
603
|
+
if self.transforms or not isinstance(other, BaseSelector) or getattr(other, "transforms", None):
|
|
604
|
+
return SelectorFilterCondition(self, "or", other)
|
|
605
|
+
|
|
606
|
+
left = self
|
|
607
|
+
right = other
|
|
608
|
+
|
|
609
|
+
def _resolve(df):
|
|
610
|
+
|
|
611
|
+
left_cols = set(left.resolve(df))
|
|
612
|
+
right_cols = set(right.resolve(df))
|
|
613
|
+
|
|
614
|
+
return [c for c in df.columns if c in left_cols or c in right_cols]
|
|
615
|
+
|
|
616
|
+
return self._selector_copy(_resolve)
|
|
617
|
+
|
|
618
|
+
def __sub__(
|
|
619
|
+
self: "BaseSelector", other: "BaseSelector"
|
|
620
|
+
) -> Union["SelectorSelectionOperations", "SelectorcolumnOperations"]:
|
|
621
|
+
"""Return columns matched by `self` but not by `other` (set difference).
|
|
622
|
+
|
|
623
|
+
Parameters
|
|
624
|
+
----------
|
|
625
|
+
other : BaseSelector
|
|
626
|
+
The selector whose matched columns are excluded from `self`'s.
|
|
627
|
+
|
|
628
|
+
Returns
|
|
629
|
+
-------
|
|
630
|
+
SelectorSelectionOperations or SelectorcolumnOperations
|
|
631
|
+
A new selector matching every column in `self` that is not also
|
|
632
|
+
in `other`. The column-operation result is used when the shared
|
|
633
|
+
`BaseSelector.__sub__` dispatcher receives a non-selector operand.
|
|
634
|
+
|
|
635
|
+
Examples
|
|
636
|
+
--------
|
|
637
|
+
>>> by_dtype([T.StringType, T.IntegerType, T.DoubleType]) - by_dtype([T.IntegerType, T.DoubleType])
|
|
638
|
+
<DTypeSelector ...>
|
|
639
|
+
"""
|
|
640
|
+
|
|
641
|
+
left = self
|
|
642
|
+
right = other
|
|
643
|
+
|
|
644
|
+
def _resolve(df):
|
|
645
|
+
|
|
646
|
+
right_cols = set(right.resolve(df))
|
|
647
|
+
|
|
648
|
+
return [dim for dim in left.resolve(df) if dim not in right_cols]
|
|
649
|
+
|
|
650
|
+
return self._selector_copy(_resolve)
|
|
651
|
+
|
|
652
|
+
def __xor__(self: "BaseSelector", other: "BaseSelector") -> "SelectorSelectionOperations":
|
|
653
|
+
"""Return columns matched by exactly one of `self`/`other` (symmetric difference).
|
|
654
|
+
|
|
655
|
+
Parameters
|
|
656
|
+
----------
|
|
657
|
+
other : BaseSelector
|
|
658
|
+
The selector to compare against `self`.
|
|
659
|
+
|
|
660
|
+
Returns
|
|
661
|
+
-------
|
|
662
|
+
SelectorSelectionOperations
|
|
663
|
+
A new selector matching every column present in exactly one of
|
|
664
|
+
`self` or `other`, but not both.
|
|
665
|
+
|
|
666
|
+
Examples
|
|
667
|
+
--------
|
|
668
|
+
>>> by_dtype([T.DoubleType, T.IntegerType]) ^ by_dtype([T.IntegerType, T.StringType])
|
|
669
|
+
<DTypeSelector ...>
|
|
670
|
+
"""
|
|
671
|
+
|
|
672
|
+
left = self
|
|
673
|
+
right = other
|
|
674
|
+
|
|
675
|
+
def _resolve(df):
|
|
676
|
+
|
|
677
|
+
left_cols = set(left.resolve(df))
|
|
678
|
+
right_cols = set(right.resolve(df))
|
|
679
|
+
|
|
680
|
+
return [c for c in df.columns if (c in left_cols) != (c in right_cols)]
|
|
681
|
+
|
|
682
|
+
return self._selector_copy(_resolve)
|
|
683
|
+
|
|
684
|
+
def __invert__(self: "BaseSelector") -> Union["SelectorSelectionOperations", "SelectorFilterCondition"]:
|
|
685
|
+
"""Complement this selector's matched columns, or negate a row condition.
|
|
686
|
+
|
|
687
|
+
Returns
|
|
688
|
+
-------
|
|
689
|
+
SelectorSelectionOperations or SelectorFilterCondition
|
|
690
|
+
A new selector matching every column NOT matched by `self`
|
|
691
|
+
(set-algebra mode), or a `SelectorFilterCondition` negating the
|
|
692
|
+
row condition (expression mode -- triggered when `self` already
|
|
693
|
+
has a chained comparison/arithmetic transform, e.g.
|
|
694
|
+
``~(by_dtype([T.IntegerType]) > 0)``).
|
|
695
|
+
|
|
696
|
+
Examples
|
|
697
|
+
--------
|
|
698
|
+
>>> ~by_dtype([T.StringType])
|
|
699
|
+
<DTypeSelector ...>
|
|
700
|
+
>>> df.filter(~(by_dtype([T.IntegerType]) > 0))
|
|
701
|
+
DataFrame[...]
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
# same expression-mode guard as __and__/__or__ -- if a comparison/arithmetic
|
|
705
|
+
# transform is already chained onto this selector (e.g. `~(by_dtype([T.IntegerType]) > 0)`),
|
|
706
|
+
# `~` has to negate that row condition, not complement the matched
|
|
707
|
+
# *columns*. without this check `~` would silently throw away the
|
|
708
|
+
# `> 0` transform and return "every column NOT of dtype int" instead -- a
|
|
709
|
+
# completely different (and wrong) result.
|
|
710
|
+
if self.transforms:
|
|
711
|
+
return SelectorFilterCondition(self, "invert")
|
|
712
|
+
|
|
713
|
+
original = self
|
|
714
|
+
|
|
715
|
+
return self._selector_copy(lambda df: [c for c in df.columns if c not in original.resolve(df)])
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
# --------------------------------------------------------------------------------
|
|
719
|
+
# Filter condition for row-level filtering
|
|
720
|
+
# --------------------------------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
class SelectorFilterCondition:
|
|
724
|
+
"""Lazy boolean row-condition combinator used by `filter`/`where`.
|
|
725
|
+
|
|
726
|
+
Companion to `SelectorSelectionOperations`: once ``&``/``|``/``~`` are
|
|
727
|
+
used on a selector that already has a comparison/arithmetic transform
|
|
728
|
+
chained onto it (e.g. ``by_dtype([T.IntegerType]) == 0``), or against a plain
|
|
729
|
+
`pyspark.sql.Column` (e.g. ``F.col('data_column_4') == None``), those
|
|
730
|
+
operators no longer mean "combine which columns are selected" -- they
|
|
731
|
+
mean "AND/OR these row conditions together".
|
|
732
|
+
`SelectorSelectionOperations.__and__`/`__or__`/`__invert__` detect that
|
|
733
|
+
case and return one of these instead of a new selector.
|
|
734
|
+
|
|
735
|
+
Resolving into a real boolean `pyspark.sql.Column` is deferred (via
|
|
736
|
+
`to_column`) since it requires a `DataFrame` to resolve each selector's
|
|
737
|
+
matched columns against -- that dataframe isn't known yet at
|
|
738
|
+
``&``/``|``/``~`` time, only once `filter`/`where` actually runs. A
|
|
739
|
+
selector operand that matches more than one column (e.g.
|
|
740
|
+
``by_dtype([T.IntegerType]) > 0`` matching several int columns) is folded down
|
|
741
|
+
with ``&`` across its own matches first, so combining conditions composes
|
|
742
|
+
cleanly no matter how many real columns are on either side.
|
|
743
|
+
|
|
744
|
+
Parameters
|
|
745
|
+
----------
|
|
746
|
+
left : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column
|
|
747
|
+
The left-hand operand of this condition.
|
|
748
|
+
op : {"and", "or", "invert", None}, optional
|
|
749
|
+
The boolean operator combining `left` and `right`. `None` means
|
|
750
|
+
`left` is passed through unchanged (used when a single selector is
|
|
751
|
+
given directly to `filter`).
|
|
752
|
+
right : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column, optional
|
|
753
|
+
The right-hand operand, required when `op` is ``"and"``/``"or"``,
|
|
754
|
+
unused when `op` is ``"invert"``/`None`.
|
|
755
|
+
|
|
756
|
+
Examples
|
|
757
|
+
--------
|
|
758
|
+
>>> df.filter((by_dtype([T.IntegerType]) > 0) & (by_dtype([T.IntegerType]) < 9))
|
|
759
|
+
DataFrame[...]
|
|
760
|
+
>>> df.filter(~(by_dtype([T.IntegerType]) > 0))
|
|
761
|
+
DataFrame[...]
|
|
762
|
+
"""
|
|
763
|
+
|
|
764
|
+
def __init__(
|
|
765
|
+
self,
|
|
766
|
+
left: Union["BaseSelector", "SelectorFilterCondition", Column],
|
|
767
|
+
op: str | None = None,
|
|
768
|
+
right: Union["BaseSelector", "SelectorFilterCondition", Column] | None = None,
|
|
769
|
+
) -> None:
|
|
770
|
+
"""Initialize the condition tree node.
|
|
771
|
+
|
|
772
|
+
Parameters
|
|
773
|
+
----------
|
|
774
|
+
left : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column
|
|
775
|
+
The left-hand operand of this condition.
|
|
776
|
+
op : {"and", "or", "invert", None}, optional
|
|
777
|
+
The boolean operator combining `left` and `right`.
|
|
778
|
+
right : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column, optional
|
|
779
|
+
The right-hand operand, when `op` requires one.
|
|
780
|
+
"""
|
|
781
|
+
|
|
782
|
+
self._left = left
|
|
783
|
+
self._op = op
|
|
784
|
+
self._right = right
|
|
785
|
+
|
|
786
|
+
@staticmethod
|
|
787
|
+
def _to_expr(operand: Union["BaseSelector", "SelectorFilterCondition", Column, Any], df: DataFrame) -> Column:
|
|
788
|
+
"""Resolve one operand (selector, condition, or plain value) into one `Column`.
|
|
789
|
+
|
|
790
|
+
Parameters
|
|
791
|
+
----------
|
|
792
|
+
operand : BaseSelector, SelectorFilterCondition, or Any
|
|
793
|
+
The operand to resolve. A `SelectorFilterCondition` recurses via
|
|
794
|
+
`to_column`; a `BaseSelector` resolves to all of its matched
|
|
795
|
+
columns' expressions, ANDed together; anything else (a plain
|
|
796
|
+
`pyspark.sql.Column` or literal) passes through unchanged.
|
|
797
|
+
df : pyspark.sql.DataFrame
|
|
798
|
+
The dataframe to resolve selector-matched columns against.
|
|
799
|
+
|
|
800
|
+
Returns
|
|
801
|
+
-------
|
|
802
|
+
pyspark.sql.Column
|
|
803
|
+
The resolved boolean (or other) expression.
|
|
804
|
+
|
|
805
|
+
Raises
|
|
806
|
+
------
|
|
807
|
+
pyspark.errors.PySparkValueError
|
|
808
|
+
If `operand` is a `BaseSelector` that matches zero columns
|
|
809
|
+
against `df`.
|
|
810
|
+
"""
|
|
811
|
+
|
|
812
|
+
if isinstance(operand, SelectorFilterCondition):
|
|
813
|
+
return operand.to_column(df)
|
|
814
|
+
|
|
815
|
+
if isinstance(operand, BaseSelector):
|
|
816
|
+
exprs = operand.resolve_columns(df)
|
|
817
|
+
|
|
818
|
+
if not exprs:
|
|
819
|
+
raise PySparkValueError(
|
|
820
|
+
errorClass="CANNOT_BE_EMPTY",
|
|
821
|
+
messageParameters={"item": "columns matched by the selector"},
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
combined = exprs[0]
|
|
825
|
+
|
|
826
|
+
for expr in exprs[1:]:
|
|
827
|
+
combined = combined & expr
|
|
828
|
+
|
|
829
|
+
return combined
|
|
830
|
+
|
|
831
|
+
return operand # plain pyspark Column / literal condition, passed through as-is
|
|
832
|
+
|
|
833
|
+
def to_column(self, df: DataFrame) -> Column:
|
|
834
|
+
"""Resolve this condition tree into one final boolean `pyspark.sql.Column`.
|
|
835
|
+
|
|
836
|
+
Parameters
|
|
837
|
+
----------
|
|
838
|
+
df : pyspark.sql.DataFrame
|
|
839
|
+
The dataframe to resolve every selector-matched column against.
|
|
840
|
+
|
|
841
|
+
Returns
|
|
842
|
+
-------
|
|
843
|
+
pyspark.sql.Column
|
|
844
|
+
The fully resolved boolean expression, ready to be passed to
|
|
845
|
+
`pyspark.sql.DataFrame.filter`.
|
|
846
|
+
|
|
847
|
+
Raises
|
|
848
|
+
------
|
|
849
|
+
pyspark.errors.PySparkValueError
|
|
850
|
+
If `self._op` is set to something other than
|
|
851
|
+
``"and"``/``"or"``/``"invert"``/`None`.
|
|
852
|
+
|
|
853
|
+
Examples
|
|
854
|
+
--------
|
|
855
|
+
>>> SelectorFilterCondition(by_dtype([T.IntegerType]) > 0).to_column(df)
|
|
856
|
+
Column<'(integer_col > 0)'>
|
|
857
|
+
"""
|
|
858
|
+
|
|
859
|
+
left_expr = self._to_expr(self._left, df)
|
|
860
|
+
|
|
861
|
+
if self._op is None:
|
|
862
|
+
return left_expr
|
|
863
|
+
|
|
864
|
+
if self._op == "invert":
|
|
865
|
+
return ~left_expr
|
|
866
|
+
|
|
867
|
+
right_expr = self._to_expr(self._right, df)
|
|
868
|
+
|
|
869
|
+
if self._op == "and":
|
|
870
|
+
return left_expr & right_expr
|
|
871
|
+
|
|
872
|
+
if self._op == "or":
|
|
873
|
+
return left_expr | right_expr
|
|
874
|
+
|
|
875
|
+
raise PySparkValueError(
|
|
876
|
+
message=f"unsupported filter condition operator: {self._op!r}",
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
def __and__(self, other: Union["BaseSelector", "SelectorFilterCondition", Column]) -> "SelectorFilterCondition":
|
|
880
|
+
"""Return a new condition ANDing `self` with `other`.
|
|
881
|
+
|
|
882
|
+
Parameters
|
|
883
|
+
----------
|
|
884
|
+
other : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column
|
|
885
|
+
The right-hand operand of ``&``.
|
|
886
|
+
|
|
887
|
+
Returns
|
|
888
|
+
-------
|
|
889
|
+
SelectorFilterCondition
|
|
890
|
+
A new condition node combining `self` and `other` with ``"and"``.
|
|
891
|
+
"""
|
|
892
|
+
return SelectorFilterCondition(self, "and", other)
|
|
893
|
+
|
|
894
|
+
def __rand__(self, other: Union["BaseSelector", Column]) -> "SelectorFilterCondition":
|
|
895
|
+
"""Return a new condition ANDing `other` with `self` (reflected `&`).
|
|
896
|
+
|
|
897
|
+
Parameters
|
|
898
|
+
----------
|
|
899
|
+
other : BaseSelector or pyspark.sql.Column
|
|
900
|
+
The left-hand operand, when `other & self` is evaluated because
|
|
901
|
+
`other` doesn't implement `__and__` for `self`'s type.
|
|
902
|
+
|
|
903
|
+
Returns
|
|
904
|
+
-------
|
|
905
|
+
SelectorFilterCondition
|
|
906
|
+
A new condition node combining `other` and `self` with ``"and"``.
|
|
907
|
+
"""
|
|
908
|
+
return SelectorFilterCondition(other, "and", self)
|
|
909
|
+
|
|
910
|
+
def __or__(self, other: Union["BaseSelector", "SelectorFilterCondition", Column]) -> "SelectorFilterCondition":
|
|
911
|
+
"""Return a new condition ORing `self` with `other`.
|
|
912
|
+
|
|
913
|
+
Parameters
|
|
914
|
+
----------
|
|
915
|
+
other : BaseSelector, SelectorFilterCondition, or pyspark.sql.Column
|
|
916
|
+
The right-hand operand of ``|``.
|
|
917
|
+
|
|
918
|
+
Returns
|
|
919
|
+
-------
|
|
920
|
+
SelectorFilterCondition
|
|
921
|
+
A new condition node combining `self` and `other` with ``"or"``.
|
|
922
|
+
"""
|
|
923
|
+
return SelectorFilterCondition(self, "or", other)
|
|
924
|
+
|
|
925
|
+
def __ror__(self, other: Union["BaseSelector", Column]) -> "SelectorFilterCondition":
|
|
926
|
+
"""Return a new condition ORing `other` with `self` (reflected `|`).
|
|
927
|
+
|
|
928
|
+
Parameters
|
|
929
|
+
----------
|
|
930
|
+
other : BaseSelector or pyspark.sql.Column
|
|
931
|
+
The left-hand operand, when `other | self` is evaluated because
|
|
932
|
+
`other` doesn't implement `__or__` for `self`'s type.
|
|
933
|
+
|
|
934
|
+
Returns
|
|
935
|
+
-------
|
|
936
|
+
SelectorFilterCondition
|
|
937
|
+
A new condition node combining `other` and `self` with ``"or"``.
|
|
938
|
+
"""
|
|
939
|
+
return SelectorFilterCondition(other, "or", self)
|
|
940
|
+
|
|
941
|
+
def __invert__(self) -> "SelectorFilterCondition":
|
|
942
|
+
"""Return a new condition negating `self`.
|
|
943
|
+
|
|
944
|
+
Returns
|
|
945
|
+
-------
|
|
946
|
+
SelectorFilterCondition
|
|
947
|
+
A new condition node wrapping `self` with ``"invert"``.
|
|
948
|
+
"""
|
|
949
|
+
return SelectorFilterCondition(self, "invert")
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
# ==========================================================
|
|
953
|
+
# define exactly what a column selector is and is capable of doing
|
|
954
|
+
# ==========================================================
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
class BaseSelector(SelectorSelectionOperations, SelectorcolumnOperations):
|
|
958
|
+
"""Abstract base class for every column selector (`by_dtype`, `by_index`, `matches`).
|
|
959
|
+
|
|
960
|
+
Combines `SelectorSelectionOperations` (set algebra: ``~ - & ^ |``) and
|
|
961
|
+
`SelectorcolumnOperations` (chained column transforms: arithmetic,
|
|
962
|
+
comparisons, every `pyspark.sql.functions` function) into one mixin base
|
|
963
|
+
that every concrete selector class inherits from. A concrete subclass
|
|
964
|
+
only needs to implement `resolve` -- everything else (transform chaining,
|
|
965
|
+
set-algebra, filter-condition composition) is inherited for free.
|
|
966
|
+
|
|
967
|
+
Parameters
|
|
968
|
+
----------
|
|
969
|
+
transforms : list of callable, optional
|
|
970
|
+
Initial transform pipeline, forwarded to
|
|
971
|
+
`SelectorcolumnOperations.__init__`.
|
|
972
|
+
resolver : callable, optional
|
|
973
|
+
Initial combinator resolver, forwarded to
|
|
974
|
+
`SelectorSelectionOperations.__init__`.
|
|
975
|
+
require_col_match : bool, default True
|
|
976
|
+
Whether `resolve` raises `pyspark.errors.PySparkValueError` when the
|
|
977
|
+
selector matches zero columns.
|
|
978
|
+
|
|
979
|
+
Notes
|
|
980
|
+
-----
|
|
981
|
+
MRO is ``BaseSelector -> SelectorSelectionOperations -> SelectorcolumnOperations``,
|
|
982
|
+
so `SelectorSelectionOperations`'s set-algebra `__invert__` (correct unary
|
|
983
|
+
signature) wins by default -- no override needed on this class for that
|
|
984
|
+
one. `__sub__` still needs disambiguation here since both mixins define
|
|
985
|
+
it, and unguarded MRO would make set-difference the silent default,
|
|
986
|
+
breaking arithmetic subtraction (e.g. ``some_selector - 5``, which has no
|
|
987
|
+
``.resolve()``).
|
|
988
|
+
"""
|
|
989
|
+
|
|
990
|
+
def __init__(
|
|
991
|
+
self,
|
|
992
|
+
transforms: list[Callable[[Column], Column]] | None = None,
|
|
993
|
+
resolver: Callable[[DataFrame], list[str]] | None = None,
|
|
994
|
+
require_col_match: bool = True,
|
|
995
|
+
) -> None:
|
|
996
|
+
"""Initialize both parent mixins' state.
|
|
997
|
+
|
|
998
|
+
Parameters
|
|
999
|
+
----------
|
|
1000
|
+
transforms : list of callable, optional
|
|
1001
|
+
Initial transform pipeline.
|
|
1002
|
+
resolver : callable, optional
|
|
1003
|
+
Initial combinator resolver.
|
|
1004
|
+
require_col_match : bool, default True
|
|
1005
|
+
Whether `resolve` raises `pyspark.errors.PySparkValueError` when
|
|
1006
|
+
this selector matches zero columns. Set to `False` to opt out
|
|
1007
|
+
and allow a zero-column match to pass through silently.
|
|
1008
|
+
"""
|
|
1009
|
+
SelectorcolumnOperations.__init__(self, transforms)
|
|
1010
|
+
SelectorSelectionOperations.__init__(self, resolver)
|
|
1011
|
+
|
|
1012
|
+
self.require_col_match = require_col_match
|
|
1013
|
+
|
|
1014
|
+
# --------------------------------------------------
|
|
1015
|
+
# each selector implements this
|
|
1016
|
+
# --------------------------------------------------
|
|
1017
|
+
|
|
1018
|
+
def resolve(self, df: DataFrame) -> list[str]:
|
|
1019
|
+
"""Resolve this selector's matched column names against a dataframe.
|
|
1020
|
+
|
|
1021
|
+
Must be implemented by every concrete subclass (`DTypeSelector`,
|
|
1022
|
+
`IndexSelector`, `RegexSelector`, ...). Implementations should check
|
|
1023
|
+
`self._resolver` first (set by `_selector_copy` when a set operator
|
|
1024
|
+
``~ - & ^ |`` combines two selectors) before falling back to their
|
|
1025
|
+
own type-specific matching logic, and should pass their final
|
|
1026
|
+
matched-name list through `_check_matched` before returning it.
|
|
1027
|
+
|
|
1028
|
+
Parameters
|
|
1029
|
+
----------
|
|
1030
|
+
df : pyspark.sql.DataFrame
|
|
1031
|
+
The dataframe to resolve matched column names against.
|
|
1032
|
+
|
|
1033
|
+
Returns
|
|
1034
|
+
-------
|
|
1035
|
+
list of str
|
|
1036
|
+
The matched column names, in dataframe-column order.
|
|
1037
|
+
|
|
1038
|
+
Raises
|
|
1039
|
+
------
|
|
1040
|
+
pyspark.errors.PySparkNotImplementedError
|
|
1041
|
+
Always, on `BaseSelector` itself -- subclasses must override this.
|
|
1042
|
+
"""
|
|
1043
|
+
|
|
1044
|
+
raise PySparkNotImplementedError(
|
|
1045
|
+
errorClass="NOT_IMPLEMENTED",
|
|
1046
|
+
messageParameters={"feature": "BaseSelector.resolve"},
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
def _check_matched(self, matched: list[str]) -> list[str]:
|
|
1050
|
+
"""Raise if `matched` is empty, unless `self.require_col_match` is `False`.
|
|
1051
|
+
|
|
1052
|
+
Single shared empty-match guard used by every concrete selector's
|
|
1053
|
+
`resolve` (`DTypeSelector`, `IndexSelector`, `RegexSelector`), on
|
|
1054
|
+
both its own type-specific matching path and its combined-resolver
|
|
1055
|
+
path (`self._resolver`, built by `~ - & ^ |`) -- so a set-algebra
|
|
1056
|
+
combination that ends up matching nothing (e.g. `all() - integer()`
|
|
1057
|
+
on a dataframe with only one integer column) is caught exactly the
|
|
1058
|
+
same way as a plain selector matching nothing (e.g. `numeric()` on a
|
|
1059
|
+
dataframe with no numeric columns at all).
|
|
1060
|
+
|
|
1061
|
+
Parameters
|
|
1062
|
+
----------
|
|
1063
|
+
matched : list of str
|
|
1064
|
+
The column names this selector resolved to.
|
|
1065
|
+
|
|
1066
|
+
Returns
|
|
1067
|
+
-------
|
|
1068
|
+
list of str
|
|
1069
|
+
`matched`, unchanged, when non-empty or when `self.require_col_match`
|
|
1070
|
+
is `False`.
|
|
1071
|
+
|
|
1072
|
+
Raises
|
|
1073
|
+
------
|
|
1074
|
+
pyspark.errors.PySparkValueError
|
|
1075
|
+
If `matched` is empty and `self.require_col_match` is `True`.
|
|
1076
|
+
|
|
1077
|
+
Examples
|
|
1078
|
+
--------
|
|
1079
|
+
>>> by_dtype([T.StringType]).resolve(df) # df has no string columns
|
|
1080
|
+
Traceback (most recent call last):
|
|
1081
|
+
...
|
|
1082
|
+
pyspark.errors.exceptions.base.PySparkValueError: ...
|
|
1083
|
+
>>> by_dtype([T.StringType], require_col_match=False).resolve(df)
|
|
1084
|
+
[]
|
|
1085
|
+
"""
|
|
1086
|
+
|
|
1087
|
+
if not matched and self.require_col_match:
|
|
1088
|
+
raise PySparkValueError(
|
|
1089
|
+
errorClass="CANNOT_BE_EMPTY",
|
|
1090
|
+
messageParameters={"item": f"columns matched by {self.__class__.__name__}"},
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
return matched
|
|
1094
|
+
|
|
1095
|
+
# --------------------------------------------------
|
|
1096
|
+
# MRO is now BaseSelector -> SelectorSelectionOperations -> SelectorcolumnOperations,
|
|
1097
|
+
# so SelectorSelectionOperations' set-algebra __invert__ (correct unary signature)
|
|
1098
|
+
# wins by default -- no override needed here anymore.
|
|
1099
|
+
#
|
|
1100
|
+
# __sub__ still needs disambiguation: both mixins define it, and unguarded
|
|
1101
|
+
# MRO would now make set-difference the silent default, breaking arithmetic
|
|
1102
|
+
# subtraction (e.g. some_selector - 5) since a plain value has no .resolve().
|
|
1103
|
+
# --------------------------------------------------
|
|
1104
|
+
|
|
1105
|
+
def __sub__(self, other: Any) -> SelectorSelectionOperations | SelectorcolumnOperations:
|
|
1106
|
+
"""Dispatch `-` to set-difference (selector operand) or arithmetic (plain value).
|
|
1107
|
+
|
|
1108
|
+
Parameters
|
|
1109
|
+
----------
|
|
1110
|
+
other : BaseSelector or Any
|
|
1111
|
+
A selector operand triggers set-difference (`SelectorSelectionOperations.__sub__`);
|
|
1112
|
+
anything else (e.g. an int/float) triggers arithmetic subtraction
|
|
1113
|
+
(`SelectorcolumnOperations.__sub__`).
|
|
1114
|
+
|
|
1115
|
+
Returns
|
|
1116
|
+
-------
|
|
1117
|
+
BaseSelector
|
|
1118
|
+
A new selector, either matching the set-difference of columns
|
|
1119
|
+
(selector operand) or with ``- other`` chained as a transform
|
|
1120
|
+
(plain value operand).
|
|
1121
|
+
|
|
1122
|
+
Examples
|
|
1123
|
+
--------
|
|
1124
|
+
>>> by_dtype([T.StringType, T.IntegerType]) - by_dtype([T.IntegerType])
|
|
1125
|
+
<DTypeSelector ...>
|
|
1126
|
+
>>> by_dtype([T.IntegerType]) - 5
|
|
1127
|
+
<DTypeSelector ...>
|
|
1128
|
+
"""
|
|
1129
|
+
|
|
1130
|
+
if isinstance(other, SelectorSelectionOperations):
|
|
1131
|
+
return SelectorSelectionOperations.__sub__(self, other)
|
|
1132
|
+
|
|
1133
|
+
return SelectorcolumnOperations.__sub__(self, other)
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
# --------------------------------------------------------------------------------
|
|
1137
|
+
# Create column selector for selecting columns by their real Spark SQL type
|
|
1138
|
+
# --------------------------------------------------------------------------------
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
class DTypeSelector(BaseSelector):
|
|
1142
|
+
"""Select columns whose real Spark SQL type is (or is a subclass of) any given type.
|
|
1143
|
+
|
|
1144
|
+
Matches against each column's real `pyspark.sql.types.DataType` instance
|
|
1145
|
+
(`df.schema[name].dataType`) via `isinstance`, not against dtype strings.
|
|
1146
|
+
Passing a category base class (e.g. `pyspark.sql.types.NumericType`)
|
|
1147
|
+
matches every concrete subclass of it (`ByteType`, `ShortType`,
|
|
1148
|
+
`IntegerType`, `LongType`, `FloatType`, `DoubleType`, `DecimalType`, ...),
|
|
1149
|
+
the same way `isinstance` already works for any Python class hierarchy --
|
|
1150
|
+
no separate "category" concept needed on top of it.
|
|
1151
|
+
|
|
1152
|
+
Parameters
|
|
1153
|
+
----------
|
|
1154
|
+
dtypes : type or sequence of type
|
|
1155
|
+
One or more `pyspark.sql.types.DataType` subclasses to match against
|
|
1156
|
+
each column's real datatype (e.g. `[T.IntegerType, T.StringType]`, or
|
|
1157
|
+
a single category base class like `T.NumericType`).
|
|
1158
|
+
transforms : list of callable, optional
|
|
1159
|
+
Initial transform pipeline, forwarded to `BaseSelector.__init__`.
|
|
1160
|
+
require_col_match : bool, default True
|
|
1161
|
+
Whether `resolve` raises `pyspark.errors.PySparkValueError` when zero
|
|
1162
|
+
columns match the requested data types.
|
|
1163
|
+
|
|
1164
|
+
Attributes
|
|
1165
|
+
----------
|
|
1166
|
+
dtypes : tuple of type
|
|
1167
|
+
The requested `DataType` subclasses, deduplicated, ready to pass
|
|
1168
|
+
straight to `isinstance`.
|
|
1169
|
+
|
|
1170
|
+
Examples
|
|
1171
|
+
--------
|
|
1172
|
+
>>> df.select(by_dtype([T.IntegerType, T.DoubleType]))
|
|
1173
|
+
DataFrame[...]
|
|
1174
|
+
>>> df.select(by_dtype([T.DoubleType]).cast('string'))
|
|
1175
|
+
DataFrame[...]
|
|
1176
|
+
>>> df.select(by_dtype([T.NumericType]))
|
|
1177
|
+
DataFrame[...]
|
|
1178
|
+
"""
|
|
1179
|
+
|
|
1180
|
+
def __init__(
|
|
1181
|
+
self,
|
|
1182
|
+
dtypes: type | Sequence[type],
|
|
1183
|
+
transforms: list[Callable[[Column], Column]] | None = None,
|
|
1184
|
+
require_col_match: bool = True,
|
|
1185
|
+
) -> None:
|
|
1186
|
+
"""Initialize the requested `DataType` subclass set.
|
|
1187
|
+
|
|
1188
|
+
Parameters
|
|
1189
|
+
----------
|
|
1190
|
+
dtypes : type or sequence of type
|
|
1191
|
+
One or more `pyspark.sql.types.DataType` subclasses to match.
|
|
1192
|
+
transforms : list of callable, optional
|
|
1193
|
+
Initial transform pipeline.
|
|
1194
|
+
require_col_match : bool, default True
|
|
1195
|
+
Whether `resolve` raises `pyspark.errors.PySparkValueError` when
|
|
1196
|
+
zero columns match `dtypes`. Forwarded to `BaseSelector.__init__`.
|
|
1197
|
+
|
|
1198
|
+
Raises
|
|
1199
|
+
------
|
|
1200
|
+
pyspark.errors.PySparkTypeError
|
|
1201
|
+
If `dtypes` (or any item in it) is not a class, or not a subclass
|
|
1202
|
+
of `pyspark.sql.types.DataType`.
|
|
1203
|
+
"""
|
|
1204
|
+
|
|
1205
|
+
super().__init__(transforms, require_col_match=require_col_match)
|
|
1206
|
+
|
|
1207
|
+
# a bare single class (e.g. by_dtype(T.IntegerType)) is convenient
|
|
1208
|
+
# shorthand for a one-item sequence -- normalize it the same way.
|
|
1209
|
+
if isinstance(dtypes, type):
|
|
1210
|
+
dtypes = (dtypes,)
|
|
1211
|
+
|
|
1212
|
+
dtypes = tuple(dict.fromkeys(dtypes)) # dedupe, preserve order
|
|
1213
|
+
|
|
1214
|
+
self._validate_dtype_classes(dtypes)
|
|
1215
|
+
|
|
1216
|
+
self.dtypes = dtypes
|
|
1217
|
+
|
|
1218
|
+
def _validate_dtype_classes(self, dtypes: Sequence[type]) -> None:
|
|
1219
|
+
"""Raise if any requested item isn't a real `pyspark.sql.types.DataType` subclass.
|
|
1220
|
+
|
|
1221
|
+
Parameters
|
|
1222
|
+
----------
|
|
1223
|
+
dtypes : sequence of type
|
|
1224
|
+
The candidate classes to validate.
|
|
1225
|
+
|
|
1226
|
+
Raises
|
|
1227
|
+
------
|
|
1228
|
+
pyspark.errors.PySparkTypeError
|
|
1229
|
+
If any item is not a class, or is a class but not a subclass of
|
|
1230
|
+
`pyspark.sql.types.DataType`.
|
|
1231
|
+
"""
|
|
1232
|
+
|
|
1233
|
+
bad = [d for d in dtypes if not (isinstance(d, type) and issubclass(d, T.DataType))]
|
|
1234
|
+
|
|
1235
|
+
if bad:
|
|
1236
|
+
raise PySparkTypeError(
|
|
1237
|
+
message=(
|
|
1238
|
+
f"by_dtype() received non-DataType item(s): {bad!r} -- every item must "
|
|
1239
|
+
"be a class (not an instance or a string) that subclasses "
|
|
1240
|
+
"pyspark.sql.types.DataType, e.g. T.IntegerType or T.NumericType."
|
|
1241
|
+
),
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
def resolve(self, df: DataFrame) -> list[str]:
|
|
1245
|
+
"""Resolve to every column name on `df` whose real datatype is in `self.dtypes`.
|
|
1246
|
+
|
|
1247
|
+
Parameters
|
|
1248
|
+
----------
|
|
1249
|
+
df : pyspark.sql.DataFrame
|
|
1250
|
+
The dataframe to resolve matched column names against.
|
|
1251
|
+
|
|
1252
|
+
Returns
|
|
1253
|
+
-------
|
|
1254
|
+
list of str
|
|
1255
|
+
Matched column names, in dataframe-column order.
|
|
1256
|
+
|
|
1257
|
+
Raises
|
|
1258
|
+
------
|
|
1259
|
+
pyspark.errors.PySparkValueError
|
|
1260
|
+
Raised by `_check_matched` (via `BaseSelector`) if zero columns
|
|
1261
|
+
match and `self.require_col_match` is `True`.
|
|
1262
|
+
|
|
1263
|
+
Examples
|
|
1264
|
+
--------
|
|
1265
|
+
>>> DTypeSelector([T.IntegerType]).resolve(df)
|
|
1266
|
+
['integer_col']
|
|
1267
|
+
"""
|
|
1268
|
+
|
|
1269
|
+
# if a set-operation (~ - & ^ |) built a combined resolver via
|
|
1270
|
+
# _selector_copy, honor it first. otherwise fall back to the
|
|
1271
|
+
# normal dtype-matching behavior. without this check, resolve()
|
|
1272
|
+
# would always recompute off of self.dtypes and silently ignore
|
|
1273
|
+
# any selector-selection-operations combinator applied to it.
|
|
1274
|
+
if self._resolver:
|
|
1275
|
+
return self._check_matched(self._resolver(df))
|
|
1276
|
+
|
|
1277
|
+
return self._check_matched(
|
|
1278
|
+
[field.name for field in df.schema.fields if isinstance(field.dataType, self.dtypes)]
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
# --------------------------------------------------------------------------------
|
|
1283
|
+
# create column selector for selecting index position in table
|
|
1284
|
+
# --------------------------------------------------------------------------------
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
class IndexSelector(BaseSelector):
|
|
1288
|
+
"""Select columns by positional index.
|
|
1289
|
+
|
|
1290
|
+
Mirrors Python's own negative-indexing rule: ``-1`` is the last column,
|
|
1291
|
+
``-2`` is second-to-last, etc, resolved against ``len(df.columns)``
|
|
1292
|
+
since the selector itself has no dataframe until `resolve` runs.
|
|
1293
|
+
|
|
1294
|
+
Parameters
|
|
1295
|
+
----------
|
|
1296
|
+
indexes : tuple of (int or range)
|
|
1297
|
+
One or more positional indexes to select. Each item is either a bare
|
|
1298
|
+
int, or a `range` object covering a span in one shot (e.g.
|
|
1299
|
+
``range(1, 4)`` for columns 1-3). Negative ints/ranges are supported
|
|
1300
|
+
(e.g. ``range(-3, 0)`` for the last 3 columns), same rule as a bare
|
|
1301
|
+
negative int.
|
|
1302
|
+
transforms : list of callable, optional
|
|
1303
|
+
Initial transform pipeline, forwarded to `BaseSelector.__init__`.
|
|
1304
|
+
strict_index_bounds : bool, default True
|
|
1305
|
+
Controls what happens when a requested index is out of bounds for
|
|
1306
|
+
the dataframe actually being resolved against: `True` raises
|
|
1307
|
+
immediately, naming the bad index and the dataframe's real column
|
|
1308
|
+
count; `False` silently skips any out-of-bounds index instead of
|
|
1309
|
+
raising, so partial matches are tolerated.
|
|
1310
|
+
|
|
1311
|
+
Attributes
|
|
1312
|
+
----------
|
|
1313
|
+
indexes : tuple of (int or range)
|
|
1314
|
+
The requested indexes, as given.
|
|
1315
|
+
strict_index_bounds : bool
|
|
1316
|
+
Whether an out-of-bounds index raises (`True`) or is skipped
|
|
1317
|
+
(`False`).
|
|
1318
|
+
|
|
1319
|
+
Notes
|
|
1320
|
+
-----
|
|
1321
|
+
Duplicate positions (e.g. an index reachable both directly and via an
|
|
1322
|
+
overlapping range) are de-duplicated, preserving first-seen order.
|
|
1323
|
+
``~ - & ^ |`` all work already -- `IndexSelector` only implements
|
|
1324
|
+
`resolve`; the set-algebra (and filter-condition) operators live entirely
|
|
1325
|
+
on `SelectorSelectionOperations`/`BaseSelector`, which every child class
|
|
1326
|
+
(this one, `DTypeSelector`, `RegexSelector`) inherits for free.
|
|
1327
|
+
|
|
1328
|
+
Examples
|
|
1329
|
+
--------
|
|
1330
|
+
>>> df.select(by_index(0, 1))
|
|
1331
|
+
DataFrame[...]
|
|
1332
|
+
>>> df.select(by_index(range(1, 4), -1))
|
|
1333
|
+
DataFrame[...]
|
|
1334
|
+
>>> df.select(by_index(0, strict_index_bounds=False))
|
|
1335
|
+
DataFrame[...]
|
|
1336
|
+
"""
|
|
1337
|
+
|
|
1338
|
+
def __init__(
|
|
1339
|
+
self,
|
|
1340
|
+
indexes: tuple[int | range, ...],
|
|
1341
|
+
transforms: list[Callable[[Column], Column]] | None = None,
|
|
1342
|
+
strict_index_bounds: bool = True,
|
|
1343
|
+
require_col_match: bool = True,
|
|
1344
|
+
) -> None:
|
|
1345
|
+
"""Initialize the requested indexes and out-of-bounds behavior.
|
|
1346
|
+
|
|
1347
|
+
Parameters
|
|
1348
|
+
----------
|
|
1349
|
+
indexes : tuple of (int or range)
|
|
1350
|
+
One or more positional indexes to select.
|
|
1351
|
+
transforms : list of callable, optional
|
|
1352
|
+
Initial transform pipeline.
|
|
1353
|
+
strict_index_bounds : bool, default True
|
|
1354
|
+
Whether an out-of-bounds index raises (`True`) or is skipped
|
|
1355
|
+
(`False`).
|
|
1356
|
+
require_col_match : bool, default True
|
|
1357
|
+
Whether `resolve` raises `pyspark.errors.PySparkValueError` when
|
|
1358
|
+
zero columns match (only reachable when `strict_index_bounds` is
|
|
1359
|
+
`False`, or via a combined `~ - & ^ |` resolver). Forwarded to
|
|
1360
|
+
`BaseSelector.__init__`.
|
|
1361
|
+
"""
|
|
1362
|
+
|
|
1363
|
+
super().__init__(transforms, require_col_match=require_col_match)
|
|
1364
|
+
|
|
1365
|
+
self.indexes = indexes
|
|
1366
|
+
self.strict_index_bounds = strict_index_bounds
|
|
1367
|
+
|
|
1368
|
+
@staticmethod
|
|
1369
|
+
def _positions_from(item: int | range) -> list[int]:
|
|
1370
|
+
"""Expand one raw index arg (an int, or a range) into a flat list of ints.
|
|
1371
|
+
|
|
1372
|
+
Parameters
|
|
1373
|
+
----------
|
|
1374
|
+
item : int or range
|
|
1375
|
+
A single index, or a `range` covering a span of indexes.
|
|
1376
|
+
|
|
1377
|
+
Returns
|
|
1378
|
+
-------
|
|
1379
|
+
list of int
|
|
1380
|
+
``[item]`` when `item` is a bare int, or ``list(item)`` when
|
|
1381
|
+
`item` is a `range`.
|
|
1382
|
+
"""
|
|
1383
|
+
|
|
1384
|
+
if isinstance(item, range):
|
|
1385
|
+
return list(item)
|
|
1386
|
+
|
|
1387
|
+
return [item]
|
|
1388
|
+
|
|
1389
|
+
def resolve(self, df: DataFrame) -> list[str]:
|
|
1390
|
+
"""Resolve to the column name(s) at every requested position on `df`.
|
|
1391
|
+
|
|
1392
|
+
Parameters
|
|
1393
|
+
----------
|
|
1394
|
+
df : pyspark.sql.DataFrame
|
|
1395
|
+
The dataframe to resolve matched column names against.
|
|
1396
|
+
|
|
1397
|
+
Returns
|
|
1398
|
+
-------
|
|
1399
|
+
list of str
|
|
1400
|
+
Matched column names, in first-seen (requested) order, with
|
|
1401
|
+
duplicate positions removed.
|
|
1402
|
+
|
|
1403
|
+
Raises
|
|
1404
|
+
------
|
|
1405
|
+
pyspark.errors.PySparkValueError
|
|
1406
|
+
If `self.strict_index_bounds` is `True` and any requested index is out
|
|
1407
|
+
of range for `df`'s column count. Also raised by
|
|
1408
|
+
`_check_matched` (via `BaseSelector`) if zero columns match and
|
|
1409
|
+
`self.require_col_match` is `True`.
|
|
1410
|
+
|
|
1411
|
+
Examples
|
|
1412
|
+
--------
|
|
1413
|
+
>>> IndexSelector((0, -1)).resolve(df)
|
|
1414
|
+
['string_col', 'integer_col']
|
|
1415
|
+
"""
|
|
1416
|
+
|
|
1417
|
+
# if a set-operation (~ - & ^ |) built a combined resolver via
|
|
1418
|
+
# _selector_copy, honor it first -- same pattern as DTypeSelector.resolve.
|
|
1419
|
+
if self._resolver:
|
|
1420
|
+
return self._check_matched(self._resolver(df))
|
|
1421
|
+
|
|
1422
|
+
columns = df.columns
|
|
1423
|
+
n = len(columns)
|
|
1424
|
+
|
|
1425
|
+
seen = set()
|
|
1426
|
+
ordered_positions = []
|
|
1427
|
+
|
|
1428
|
+
for item in self.indexes:
|
|
1429
|
+
for pos in self._positions_from(item):
|
|
1430
|
+
actual = pos if pos >= 0 else n + pos
|
|
1431
|
+
|
|
1432
|
+
if actual < 0 or actual >= n:
|
|
1433
|
+
if self.strict_index_bounds:
|
|
1434
|
+
raise PySparkValueError(
|
|
1435
|
+
errorClass="VALUE_OUT_OF_BOUNDS",
|
|
1436
|
+
messageParameters={
|
|
1437
|
+
"arg_name": "by_index() column index",
|
|
1438
|
+
"lower_bound": str(-n),
|
|
1439
|
+
"upper_bound": str(n - 1),
|
|
1440
|
+
"actual": str(pos),
|
|
1441
|
+
},
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
continue
|
|
1445
|
+
|
|
1446
|
+
if actual not in seen:
|
|
1447
|
+
seen.add(actual)
|
|
1448
|
+
ordered_positions.append(actual)
|
|
1449
|
+
|
|
1450
|
+
return self._check_matched([columns[p] for p in ordered_positions])
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
# --------------------------------------------------------------------------------
|
|
1454
|
+
# create column selector that can select column's by their column name regex pattern
|
|
1455
|
+
# --------------------------------------------------------------------------------
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
class RegexSelector(BaseSelector):
|
|
1459
|
+
"""Select columns whose name matches a regex pattern.
|
|
1460
|
+
|
|
1461
|
+
pyspark has a native regex column selector, `pyspark.sql.DataFrame.colRegex`
|
|
1462
|
+
-- but it has to be called directly off a `DataFrame` instance
|
|
1463
|
+
(``df.colRegex(...)``, not a free-standing selector built ahead of time),
|
|
1464
|
+
and it returns a single opaque `pyspark.sql.Column` representing "all
|
|
1465
|
+
matching columns" without ever exposing which real column *names*
|
|
1466
|
+
matched. Every override in this notebook (`select`, `agg`, `sort`,
|
|
1467
|
+
`withColumns`, etc, via `_resolve_selector_exprs`) depends on getting back
|
|
1468
|
+
real ``(name, expr)`` pairs so a chained transform (``.cast(...)``,
|
|
1469
|
+
``.desc()``, ``.upper()``, ...) can be applied per matched column and the
|
|
1470
|
+
result still knows what to call each output column -- `colRegex` can't
|
|
1471
|
+
give us that, so `resolve` instead matches each real column name in
|
|
1472
|
+
`df.columns` against the pattern directly, via `re.search`.
|
|
1473
|
+
|
|
1474
|
+
Parameters
|
|
1475
|
+
----------
|
|
1476
|
+
pattern : str
|
|
1477
|
+
A regular expression, matched against each column name via
|
|
1478
|
+
`re.search` (not anchored -- matches anywhere in the name unless the
|
|
1479
|
+
pattern itself anchors with ``^``/``$``).
|
|
1480
|
+
transforms : list of callable, optional
|
|
1481
|
+
Initial transform pipeline, forwarded to `BaseSelector.__init__`.
|
|
1482
|
+
require_col_match : bool, default True
|
|
1483
|
+
Whether `resolve` raises `pyspark.errors.PySparkValueError` when zero
|
|
1484
|
+
columns match `pattern`.
|
|
1485
|
+
|
|
1486
|
+
Attributes
|
|
1487
|
+
----------
|
|
1488
|
+
pattern : str
|
|
1489
|
+
The requested regex pattern, as given.
|
|
1490
|
+
|
|
1491
|
+
Notes
|
|
1492
|
+
-----
|
|
1493
|
+
``~ - & ^ |`` all work already, same reasoning as `IndexSelector`.
|
|
1494
|
+
|
|
1495
|
+
Examples
|
|
1496
|
+
--------
|
|
1497
|
+
>>> df.select(matches(r"_id$"))
|
|
1498
|
+
DataFrame[...]
|
|
1499
|
+
>>> df.select(matches(r"_id$").upper())
|
|
1500
|
+
DataFrame[...]
|
|
1501
|
+
"""
|
|
1502
|
+
|
|
1503
|
+
def __init__(
|
|
1504
|
+
self,
|
|
1505
|
+
pattern: str,
|
|
1506
|
+
transforms: list[Callable[[Column], Column]] | None = None,
|
|
1507
|
+
require_col_match: bool = True,
|
|
1508
|
+
) -> None:
|
|
1509
|
+
"""Initialize the requested regex pattern.
|
|
1510
|
+
|
|
1511
|
+
Parameters
|
|
1512
|
+
----------
|
|
1513
|
+
pattern : str
|
|
1514
|
+
A regular expression, matched against each column name.
|
|
1515
|
+
transforms : list of callable, optional
|
|
1516
|
+
Initial transform pipeline.
|
|
1517
|
+
require_col_match : bool, default True
|
|
1518
|
+
Whether `resolve` raises `pyspark.errors.PySparkValueError` when
|
|
1519
|
+
zero columns match `pattern`. Forwarded to `BaseSelector.__init__`.
|
|
1520
|
+
"""
|
|
1521
|
+
|
|
1522
|
+
super().__init__(transforms, require_col_match=require_col_match)
|
|
1523
|
+
|
|
1524
|
+
self.pattern = pattern
|
|
1525
|
+
|
|
1526
|
+
def resolve(self, df: DataFrame) -> list[str]:
|
|
1527
|
+
"""Resolve to every column name on `df` matching `self.pattern`.
|
|
1528
|
+
|
|
1529
|
+
Parameters
|
|
1530
|
+
----------
|
|
1531
|
+
df : pyspark.sql.DataFrame
|
|
1532
|
+
The dataframe to resolve matched column names against.
|
|
1533
|
+
|
|
1534
|
+
Returns
|
|
1535
|
+
-------
|
|
1536
|
+
list of str
|
|
1537
|
+
Matched column names, in dataframe-column order.
|
|
1538
|
+
|
|
1539
|
+
Raises
|
|
1540
|
+
------
|
|
1541
|
+
pyspark.errors.PySparkValueError
|
|
1542
|
+
Raised by `_check_matched` (via `BaseSelector`) if zero columns
|
|
1543
|
+
match and `self.require_col_match` is `True`.
|
|
1544
|
+
|
|
1545
|
+
Examples
|
|
1546
|
+
--------
|
|
1547
|
+
>>> RegexSelector(r"_id$").resolve(df)
|
|
1548
|
+
['string_col', 'data_column_5']
|
|
1549
|
+
"""
|
|
1550
|
+
|
|
1551
|
+
# if a set-operation (~ - & ^ |) built a combined resolver via
|
|
1552
|
+
# _selector_copy, honor it first -- same pattern as DTypeSelector.resolve.
|
|
1553
|
+
if self._resolver:
|
|
1554
|
+
return self._check_matched(self._resolver(df))
|
|
1555
|
+
|
|
1556
|
+
# case-insensitive by design: pyspark's own column-name resolution
|
|
1557
|
+
# (`df.col_name`, `df['Col_Name']`, `df.select('col_name')`, joins,
|
|
1558
|
+
# ...) is case-insensitive by default, so a case-sensitive regex here
|
|
1559
|
+
# would silently miss columns a plain pyspark reference would still
|
|
1560
|
+
# find. re.IGNORECASE keeps this selector's matching behavior
|
|
1561
|
+
# consistent with that.
|
|
1562
|
+
return self._check_matched(
|
|
1563
|
+
[dim for dim in df.columns if re.search(self.pattern, dim, flags=re.IGNORECASE) is not None]
|
|
1564
|
+
)
|