|
6 | 6 | import pytest |
7 | 7 |
|
8 | 8 | import freshdata as fd |
| 9 | +from freshdata.config import CleanConfig |
9 | 10 | from freshdata.execution import EngineConfig |
| 11 | +from freshdata.execution._config import FallbackError |
10 | 12 |
|
11 | 13 | pl = pytest.importorskip("polars") |
12 | 14 |
|
@@ -73,3 +75,71 @@ def test_projection_pushdown_drops_empty_before_collect(native_config): |
73 | 75 | df = pd.DataFrame({"keep": [1, 2], "gone": [None, None]}) |
74 | 76 | out = fd.clean(df, config=native_config, engine="polars", output_format="polars") |
75 | 77 | assert list(out.columns) == ["keep"] |
| 78 | + |
| 79 | + |
| 80 | +def _native_nan_source(): |
| 81 | + return pl.DataFrame({ |
| 82 | + "a": [1.0, float("nan"), 3.0], |
| 83 | + "b": [float("nan")] * 3, |
| 84 | + "s": ["x", None, "y"], |
| 85 | + }) |
| 86 | + |
| 87 | + |
| 88 | +def _steps(report): |
| 89 | + return [(a.step, a.count) for a in report.actions] |
| 90 | + |
| 91 | + |
| 92 | +@pytest.mark.parametrize( |
| 93 | + "make_source", |
| 94 | + [lambda df: df, lambda df: df.lazy(), lambda df: df.to_arrow()], |
| 95 | + ids=["dataframe", "lazyframe", "arrow"], |
| 96 | +) |
| 97 | +def test_native_nan_counts_as_missing_like_pandas(native_config, make_source): |
| 98 | + src = _native_nan_source() |
| 99 | + ref_out, ref = fd.clean(src.to_pandas(), config=native_config, engine="pandas", |
| 100 | + return_report=True) |
| 101 | + out, report = fd.clean(make_source(src), config=native_config, engine="polars", |
| 102 | + return_report=True) |
| 103 | + assert list(out.columns) == list(ref_out.columns) == ["a", "s"] |
| 104 | + assert report.rows_after == ref.rows_after == 2 |
| 105 | + assert report.missing_before == ref.missing_before == 5 |
| 106 | + assert _steps(report) == _steps(ref) |
| 107 | + |
| 108 | + |
| 109 | +def test_polars_written_parquet_nan_counts_as_missing(tmp_path, native_config): |
| 110 | + path = str(tmp_path / "nan.parquet") |
| 111 | + _native_nan_source().write_parquet(path) |
| 112 | + out, report = fd.clean(path, config=native_config, engine="polars", return_report=True) |
| 113 | + assert list(out.columns) == ["a", "s"] |
| 114 | + assert report.missing_before == 5 |
| 115 | + |
| 116 | + |
| 117 | +def test_infinite_values_are_excluded_from_outlier_fences(): |
| 118 | + config = CleanConfig(strategy="conservative", fix_dtypes=False, verbose=False, |
| 119 | + outliers="clip", outlier_method="iqr") |
| 120 | + df = pd.DataFrame({"x": [1.0, 2.0, 3.0, 4.0] + [float("inf")] * 4}) |
| 121 | + ref, ref_report = fd.clean(df, config=config, engine="pandas", return_report=True) |
| 122 | + out, report = fd.clean(pl.from_pandas(df), config=config, engine="polars", |
| 123 | + return_report=True) |
| 124 | + assert out["x"].tolist() == ref["x"].tolist() |
| 125 | + assert _steps(report) == _steps(ref_report) |
| 126 | + |
| 127 | + |
| 128 | +@pytest.mark.parametrize( |
| 129 | + "kwargs", |
| 130 | + [{}, {"engine": "auto"}, {"engine": "polars", "strategy": "balanced"}, |
| 131 | + {"engine": "polars", "strategy": "conservative"}], |
| 132 | + ids=["default", "auto", "polars-balanced", "polars-conservative"], |
| 133 | +) |
| 134 | +def test_lazy_frame_source_survives_pandas_fallback(kwargs): |
| 135 | + lf = pl.DataFrame({"a": [1.0, None, 3.0], "b": ["x", "y", "z"]}).lazy() |
| 136 | + out = fd.clean(lf, verbose=False, **kwargs) |
| 137 | + frame = out.collect() if isinstance(out, pl.LazyFrame) else out |
| 138 | + assert len(frame) == 3 |
| 139 | + |
| 140 | + |
| 141 | +def test_lazy_frame_fallback_still_honours_error_policy(): |
| 142 | + lf = pl.DataFrame({"a": [1.0, None, 3.0]}).lazy() |
| 143 | + with pytest.raises(FallbackError): |
| 144 | + fd.clean(lf, engine="polars", strategy="balanced", fallback_policy="error", |
| 145 | + verbose=False) |
0 commit comments