Skip to content

Commit 005eb9a

Browse files
authored
feat: Allow specifying the engine for validate and filter (#364)
1 parent 315bbc2 commit 005eb9a

4 files changed

Lines changed: 72 additions & 35 deletions

File tree

dataframely/_polars.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# SPDX-License-Identifier: BSD-3-Clause
33

44
import datetime as dt
5-
from typing import TypeVar
5+
from typing import Any, TypeVar
66

77
import polars as pl
88
from polars.datatypes import DataTypeClass
@@ -41,18 +41,18 @@ def timedelta_matches_resolution(d: dt.timedelta, resolution: str) -> bool:
4141
return datetime_matches_resolution(EPOCH_DATETIME + d, resolution)
4242

4343

44-
def collect_if(lf: pl.LazyFrame, condition: bool) -> pl.LazyFrame:
44+
def collect_if(lf: pl.LazyFrame, condition: bool, **kwargs: Any) -> pl.LazyFrame:
4545
"""Collect a lazy frame based on `condition`."""
4646
if condition:
47-
return lf.collect().lazy()
47+
return lf.collect(**kwargs).lazy()
4848
return lf
4949

5050

5151
def collect_all_if(
52-
lfs: dict[str, pl.LazyFrame], condition: bool
52+
lfs: dict[str, pl.LazyFrame], condition: bool, **kwargs: Any
5353
) -> dict[str, pl.LazyFrame]:
5454
"""Collect the lazy frames in the dictionary based on `condition`."""
5555
if condition:
56-
dfs = pl.collect_all(lfs.values())
56+
dfs = pl.collect_all(lfs.values(), **kwargs)
5757
return {k: v.lazy() for k, v in zip(lfs.keys(), dfs)}
5858
return lfs

dataframely/collection/collection.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,13 @@ def _preprocess_sample(
380380

381381
@classmethod
382382
def validate(
383-
cls, data: Mapping[str, FrameType], /, *, cast: bool = False, eager: bool = True
383+
cls,
384+
data: Mapping[str, FrameType],
385+
/,
386+
*,
387+
cast: bool = False,
388+
eager: bool = True,
389+
**kwargs: Any,
384390
) -> Self:
385391
"""Validate that a set of data frames satisfy the collection's invariants.
386392
@@ -400,6 +406,8 @@ def validate(
400406
:meth:`~polars.LazyFrame.collect` on the individual member or
401407
:meth:`collect_all` on the collection. Note that, in the latter case,
402408
information from error messages is limited.
409+
kwargs: Keyword arguments passed directly to :meth:`polars.collect_all` and
410+
:meth:`polars.LazyFrame.collect` when `eager=True`.
403411
404412
Raises:
405413
ValueError: If an insufficient set of input data frames is provided, i.e. if
@@ -421,7 +429,7 @@ def validate(
421429
if eager:
422430
# If we perform the validation eagerly, we call filter and check the failure
423431
# information to properly construct a useful error message.
424-
filtered, failures = cls.filter(data, cast=cast, eager=True)
432+
filtered, failures = cls.filter(data, cast=cast, eager=True, **kwargs)
425433
if any(len(failure) > 0 for failure in failures.values()):
426434
errors: dict[str, str] = {}
427435
for member, failure in failures.items():
@@ -489,7 +497,9 @@ def validate(
489497
return cls._init(members)
490498

491499
@classmethod
492-
def is_valid(cls, data: Mapping[str, FrameType], /, *, cast: bool = False) -> bool:
500+
def is_valid(
501+
cls, data: Mapping[str, FrameType], /, *, cast: bool = False, **kwargs: Any
502+
) -> bool:
493503
"""Utility method to check whether :meth:`validate` raises an exception.
494504
495505
Args:
@@ -498,6 +508,8 @@ def is_valid(cls, data: Mapping[str, FrameType], /, *, cast: bool = False) -> bo
498508
the member as key.
499509
cast: Whether columns with a wrong data type in the member data frame are
500510
cast to their schemas' defined data types if possible.
511+
kwargs: Keyword arguments passed directly to :meth:`polars.collect_all` and
512+
:meth:`polars.LazyFrame.collect`.
501513
502514
Returns:
503515
Whether the provided members satisfy the invariants of the collection.
@@ -512,7 +524,7 @@ def is_valid(cls, data: Mapping[str, FrameType], /, *, cast: bool = False) -> bo
512524
members: dict[str, pl.LazyFrame] = {}
513525
for member, schema in cls.member_schemas().items():
514526
if member in data:
515-
if not schema.is_valid(data[member], cast=cast):
527+
if not schema.is_valid(data[member], cast=cast, **kwargs):
516528
return False
517529
members[member] = data[member].lazy()
518530

@@ -523,9 +535,12 @@ def is_valid(cls, data: Mapping[str, FrameType], /, *, cast: bool = False) -> bo
523535
keep = [filter.logic(result_cls).select(primary_key) for filter in filters]
524536
joined = _join_all(*keep, on=primary_key, how="inner")
525537
removed_rows = pl.collect_all(
526-
data[member].lazy().join(joined, on=primary_key, how="anti")
527-
for member in cls.members()
528-
if member in data
538+
(
539+
data[member].lazy().join(joined, on=primary_key, how="anti")
540+
for member in cls.members()
541+
if member in data
542+
),
543+
**kwargs,
529544
)
530545
return all(df.is_empty() for df in removed_rows)
531546

@@ -535,7 +550,13 @@ def is_valid(cls, data: Mapping[str, FrameType], /, *, cast: bool = False) -> bo
535550

536551
@classmethod
537552
def filter(
538-
cls, data: Mapping[str, FrameType], /, *, cast: bool = False, eager: bool = True
553+
cls,
554+
data: Mapping[str, FrameType],
555+
/,
556+
*,
557+
cast: bool = False,
558+
eager: bool = True,
559+
**kwargs: Any,
539560
) -> CollectionFilterResult[Self]:
540561
"""Filter the members data frame by their schemas and the collection's filters.
541562
@@ -551,6 +572,8 @@ def filter(
551572
eager: Whether the filter operation should be performed eagerly.
552573
Note that until https://github.com/pola-rs/polars/pull/24129 is
553574
released, eagerly filtering can provide significant speedups.
575+
kwargs: Keyword arguments passed directly to :meth:`polars.collect_all` and
576+
:meth:`polars.LazyFrame.collect` when `eager=True`.
554577
555578
Returns:
556579
A named tuple with fields `result` and `failure`. The `result` field
@@ -593,7 +616,7 @@ class HospitalInvoiceData(dy.Collection):
593616
continue
594617

595618
member_result, failures[member_name] = member.schema.filter(
596-
data[member_name].lazy(), cast=cast, eager=eager
619+
data[member_name].lazy(), cast=cast, eager=eager, **kwargs
597620
)
598621
results[member_name] = member_result.lazy()
599622

@@ -609,7 +632,7 @@ class HospitalInvoiceData(dy.Collection):
609632
name: filter.logic(result_cls).select(primary_key)
610633
for name, filter in filters.items()
611634
}
612-
keep = collect_all_if(keep, eager)
635+
keep = collect_all_if(keep, eager, **kwargs)
613636

614637
drop: dict[str, pl.LazyFrame] = {
615638
f"{failure_propagating_member}|failure_propagation": (
@@ -619,7 +642,7 @@ class HospitalInvoiceData(dy.Collection):
619642
)
620643
for failure_propagating_member in failure_propagating_members
621644
}
622-
drop = collect_all_if(drop, eager)
645+
drop = collect_all_if(drop, eager, **kwargs)
623646

624647
# Now we can iterate over the results and left-join onto each individual
625648
# filter to obtain independent boolean indicators of whether to keep the row.
@@ -647,7 +670,7 @@ class HospitalInvoiceData(dy.Collection):
647670

648671
lfs_with_eval[member_name] = lf_with_eval
649672

650-
lfs_with_eval = collect_all_if(lfs_with_eval, eager)
673+
lfs_with_eval = collect_all_if(lfs_with_eval, eager, **kwargs)
651674
for member_name, lf_with_eval in lfs_with_eval.items():
652675
member_info = cls.members()[member_name]
653676

@@ -714,7 +737,7 @@ class HospitalInvoiceData(dy.Collection):
714737

715738
result = CollectionFilterResult(cls._init(results), failures)
716739
if eager:
717-
return result.collect_all()
740+
return result.collect_all(**kwargs)
718741
return result
719742

720743
def join(
@@ -822,7 +845,7 @@ def collect_all(self, **kwargs: Any) -> Self:
822845
The same collection with all members collected once. Members annotated
823846
with :class:`~dataframely.DataFrame` are returned as DataFrames, while
824847
members annotated with :class:`~dataframely.LazyFrame` are returned as
825-
"shallow-lazy" frames (obtained by calling ``.collect().lazy()``).
848+
"shallow-lazy" frames (obtained by calling `.collect().lazy()`).
826849
"""
827850
lazy_dict = self.to_dict()
828851
dfs = pl.collect_all(lazy_dict.values(), **kwargs)

dataframely/schema.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ def validate(
501501
*,
502502
cast: bool = False,
503503
eager: Literal[True] = True,
504+
**kwargs: Any,
504505
) -> DataFrame[Self]: ...
505506

506507
@overload
@@ -512,6 +513,7 @@ def validate(
512513
*,
513514
cast: bool = False,
514515
eager: Literal[False],
516+
**kwargs: Any,
515517
) -> LazyFrame[Self]: ...
516518

517519
@overload
@@ -523,6 +525,7 @@ def validate(
523525
*,
524526
cast: bool = False,
525527
eager: bool,
528+
**kwargs: Any,
526529
) -> DataFrame[Self] | LazyFrame[Self]: ...
527530

528531
@classmethod
@@ -533,6 +536,7 @@ def validate(
533536
*,
534537
cast: bool = False,
535538
eager: bool = True,
539+
**kwargs: Any,
536540
) -> DataFrame[Self] | LazyFrame[Self]:
537541
"""Validate that a data frame satisfies the schema.
538542
@@ -554,6 +558,8 @@ def validate(
554558
not surface *all* validation issues as the validation is aborted
555559
once the first failure is encountered. Likewise, the reported
556560
validation failure can be non-deterministic.
561+
kwargs: Keyword arguments passed directly to :meth:`polars.LazyFrame.collect`
562+
when `eager=True`.
557563
558564
Returns:
559565
The input eager or lazy frame, wrapped in a generic version of the
@@ -574,7 +580,7 @@ def validate(
574580
`eager=False`.
575581
"""
576582
if eager:
577-
out, failure = cls.filter(df, cast=cast, eager=True)
583+
out, failure = cls.filter(df, cast=cast, eager=True, **kwargs)
578584
if len(failure) > 0:
579585
counts = failure.counts()
580586
raise ValidationError(
@@ -608,7 +614,7 @@ def validate(
608614

609615
@classmethod
610616
def is_valid(
611-
cls, df: pl.DataFrame | pl.LazyFrame, /, *, cast: bool = False
617+
cls, df: pl.DataFrame | pl.LazyFrame, /, *, cast: bool = False, **kwargs: Any
612618
) -> bool:
613619
"""Check whether a data frame satisfies the schema.
614620
@@ -627,6 +633,7 @@ def is_valid(
627633
cast to the schema's defined data type before running validation. If set
628634
to `False`, a wrong data type will result in a return value of
629635
`False`.
636+
kwargs: Keyword arguments passed directly to :meth:`polars.LazyFrame.collect`.
630637
631638
Returns:
632639
Whether the provided dataframe can be validated with this schema.
@@ -646,12 +653,12 @@ def is_valid(
646653
return (
647654
lf.pipe(with_evaluation_rules, rules)
648655
.select(all_rules(rules.keys()))
649-
.collect()
656+
.collect(**kwargs)
650657
.item()
651658
)
652659
# NOTE: We cannot simply return `True` here as, otherwise, we wouldn't
653660
# validate the schema.
654-
return lf.select(pl.lit(True)).collect().item()
661+
return lf.select(pl.lit(True)).collect(**kwargs).item()
655662
except SchemaError:
656663
# If we encounter a schema error, we gracefully handle this as 'invalid'
657664
return False
@@ -699,6 +706,7 @@ def filter(
699706
*,
700707
cast: bool = False,
701708
eager: bool = True,
709+
**kwargs: Any,
702710
) -> FilterResult[Self] | LazyFilterResult[Self]:
703711
"""Filter the data frame by the rules of this schema, returning `(valid,
704712
failures)`.
@@ -709,16 +717,16 @@ def filter(
709717
succeeds.
710718
711719
Args:
712-
df: The data frame to filter for valid rows.
713-
The data frame is collected within this method, regardless of whether
714-
a :class:`~polars.DataFrame` or :class:`~polars.LazyFrame` is passed.
715-
cast:
716-
Whether columns with a wrong data type in the input data frame are
720+
df: The data frame to filter for valid rows. The data frame is collected
721+
within this method, regardless of whether a :class:`~polars.DataFrame`
722+
or :class:`~polars.LazyFrame` is passed.
723+
cast: Whether columns with a wrong data type in the input data frame are
717724
cast to the schema's defined data type if possible. Rows for which the
718725
cast fails for any column are filtered out.
719726
eager: Whether the filter operation should be performed eagerly. If `False`, the
720-
returned lazy frame will
721-
fail to collect if the validation does not pass.
727+
returned lazy frame will fail to collect if the validation does not pass.
728+
kwargs: Keyword arguments passed directly to :meth:`polars.LazyFrame.collect`
729+
when `eager=True`.
722730
723731
Returns:
724732
A tuple of the validated rows in the input data frame (potentially
@@ -755,7 +763,7 @@ def filter(
755763
)
756764
if rules := cls._validation_rules(with_cast=cast):
757765
evaluated = lf.pipe(cls._with_evaluated_rules, rules).pipe(
758-
collect_if, eager
766+
collect_if, eager, **kwargs
759767
)
760768
filtered = evaluated.filter(pl.col(_COLUMN_VALID)).select(
761769
cls.column_names()

tests/benches/test_collection.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,24 @@ def one_to_at_least_one_reverse(self) -> pl.LazyFrame:
9595

9696

9797
@pytest.mark.benchmark(group="collection-filter-multi")
98+
@pytest.mark.parametrize("engine", ["in-memory", "streaming"])
9899
def test_multi_filter_validate(
99-
benchmark: BenchmarkFixture, partitioned_dataset: dict[str, pl.DataFrame]
100+
benchmark: BenchmarkFixture,
101+
partitioned_dataset: dict[str, pl.DataFrame],
102+
engine: str,
100103
) -> None:
101-
benchmark(MultiFilterCollection.validate, partitioned_dataset)
104+
benchmark(MultiFilterCollection.validate, partitioned_dataset, engine=engine)
102105

103106

104107
@pytest.mark.benchmark(group="collection-filter-multi")
108+
@pytest.mark.parametrize("engine", ["in-memory", "streaming"])
105109
def test_multi_filter_filter(
106-
benchmark: BenchmarkFixture, partitioned_dataset: dict[str, pl.DataFrame]
110+
benchmark: BenchmarkFixture,
111+
partitioned_dataset: dict[str, pl.DataFrame],
112+
engine: str,
107113
) -> None:
108114
def benchmark_fn() -> None:
109-
_, failure = MultiFilterCollection.filter(partitioned_dataset)
115+
_, failure = MultiFilterCollection.filter(partitioned_dataset, engine=engine)
110116
_ = [len(f) for f in failure.values()]
111117

112118
benchmark(benchmark_fn)

0 commit comments

Comments
 (0)