[FLINK-40196][python] Add missing-value handling to DataFrame API - #28979
[FLINK-40196][python] Add missing-value handling to DataFrame API#28979MattBelle wants to merge 2 commits into
Conversation
| .. versionadded:: 2.4.0 | ||
| """ | ||
| subset = self._validate_subset(subset) | ||
| conditions = [table_col(col_name).is_not_nan for col_name in subset] |
There was a problem hiding this comment.
For NULL input, IS_NOT_NAN(NULL) returns NULL, and Table.filter only retains rows for which the predicate is TRUE. Therefore, the current implementation incorrectly drops NULL rows which violates the doc: NULL values are preserved.
There was a problem hiding this comment.
Good catch! Wrote a new test for this scenario (test_drop_nan_preserves_null_values). Added OR col IS NULL to the filter condition in drop_nan() to explicitly preserve NULL values. All DataFrameNullNanITTests are passing.
| .. versionadded:: 2.4.0 | ||
| """ | ||
| subset = self._validate_subset(subset) | ||
| conditions = [table_col(col_name).is_not_nan for col_name in subset] |
There was a problem hiding this comment.
With subset=None, the current implementation applies IS_NAN/IS_NOT_NAN to every column for drop_nan/fill_nan. A common mixed schema containing STRING or BOOLEAN columns will therefore fail during type inference.
I think we could select only FLOAT/DOUBLE columns from the schema and return an equivalent DataFrame when no floating-point columns exist. I checked that Polars, Daft follow this behavior which seem reasonable for me.
There was a problem hiding this comment.
Fixed! Added auto-filtering to FLOAT/DOUBLE columns when subset=None for both drop_nan() and fill_nan(). This prevents validation errors on mixed schemas. Added 4 new tests covering mixed schemas and edge cases. All DataFrameNullNanITTests are passing. This matches the behavior of Polars and Daft.
| return self._fill_values(value, subset, lambda col: col.is_null) | ||
|
|
||
| @PublicEvolving() | ||
| def fill_nan(self, value: Any, subset: Optional[List[str]] = None) -> "DataFrame": |
There was a problem hiding this comment.
For drop_nan/fill_nan API, we should define clearly the behavior of non-number columns, eg. String, Boolean, etc. I suggest validating column existence first and then ignoring non-floating-point columns, consistent with Daft, and Polars’ default behavior.
There was a problem hiding this comment.
Addressed. I updated drop_nan() and fill_nan() so that when subset=None, they only operate on floating-point columns instead of attempting to apply IS_NAN/IS_NOT_NAN across the full schema. That resolves the undefined behavior for non-numeric columns in the default path and aligns the behavior noted in the review for Polars and Daft.
| raise TypeError("subset must be a list of strings") | ||
|
|
||
| if not subset: | ||
| raise ValueError("subset cannot be empty") |
There was a problem hiding this comment.
Nit: Polars and Pandas treat empty subset as a no-op instead of raising exceptions. I have no preference. Just comment here for your reference.
There was a problem hiding this comment.
Addressed. I updated drop_null() to treat empty subset as a no-op (returns DataFrame unchanged), aligning with Pandas/Polars behavior and matching the existing behavior in fill_null() and fill_nan().
| col_expr = table_col(col_name) | ||
| if col_name in subset_set: | ||
| col_type = schema.get_field_data_type(col_name) | ||
| typed_value = table_lit(value).cast(col_type) |
There was a problem hiding this comment.
The current implementation casts the replacement to every target column type. For example, fill_null(0) may replace a STRING NULL with "0" and may fail for ARRAY, ROW, or TIMESTAMP columns.
It only handles the columns which supports the type cast in Spark and Daft.
There was a problem hiding this comment.
Addressed. I moved the execution-based fill_null compatibility coverage into DataFrameNullNanITTests, since these assertions depend on collecting real results rather than just validating schema. The incompatible-type behavior is covered by:
test_fill_null_type_compatibilitytest_fill_null_skips_incompatible_array_columntest_fill_null_skips_incompatible_string_column
These tests are now passing.
I also decided to treat STRING columns as incompatible with numeric fill values and not coerce values like 0 to "0", since that matches Spark behavior.
| @PublicEvolving() | ||
| def fill_nan(self, value: Any, subset: Optional[List[str]] = None) -> "DataFrame": | ||
| """ | ||
| Replace NaN values with a specified value (for float/double columns). |
There was a problem hiding this comment.
The current implementation appears to support fill_nan(None) which converts NaN values to NULL. I think this is reasonable. What about documenting this behavior explicitly?
There was a problem hiding this comment.
Addressed. I updated the fill_nan() docstring to document the fill_nan(None) behavior explicitly, including that None converts NaN values to NULL.
|
|
||
| /** Implementation of {@link BuiltInFunctionDefinitions#IS_NOT_NAN}. */ | ||
| @Internal | ||
| public final class IsNotNanFunction extends BuiltInScalarFunction { |
There was a problem hiding this comment.
Do we really need IsNotNan?
There was a problem hiding this comment.
I originally did not add it, but I noticed IS_NULL had a corresponding IS_NOT_NULL, so I mirrored that behavior for NaN for consistency. If you prefer, I can revert it back to just IS_NAN.
…handling - Fix NULL preservation in drop_nan() by adding OR col IS NULL condition - Add auto-filtering to FLOAT/DOUBLE columns for drop_nan/fill_nan with subset=None - Add type compatibility checking in fill_null() to skip incompatible columns - Change empty subset behavior to no-op (align with Pandas/Polars) - Update fill_nan() docstring to document fill_nan(None) behavior - Add 8 new integration tests for edge cases and type compatibility Generated-by: Bob Shell 1.0.6
What is the purpose of the change
This pull request adds missing-value handling methods to the PyFlink DataFrame API, enabling users to easily drop or fill NULL and NaN values in their data processing pipelines.
Brief change log
drop_null()method to remove rows containing NULL valuesdrop_nan()method to remove rows containing NaN valuesfill_null()method to replace NULL values with specified valuesfill_nan()method to replace NaN values with specified valuesIS_NANandIS_NOT_NANbuilt-in functions in Flink Table API (required infrastructure for NaN detection)is_nanandis_not_nanhelper functions to Expression API (mirrorsis_null/is_not_nullpattern)_validate_subset()and_fill_values()for DRY code organizationVerifying this change
This change added tests and can be verified as follows:
DataFrameDropNullTests,DataFrameDropNanTests,DataFrameFillNullTests,DataFrameFillNanTests) that verify:ValueErrorValueErrorwith clear error messagesTypeErrorDataFrameNullNanITTests) that verify correct behavior with real data:drop_null()correctly removes rows with NULL valuesdrop_nan()correctly removes rows with NaN valuesfill_null()correctly replaces NULL with specified values (numeric and string)fill_nan()correctly replaces NaN with specified valuessubsetparameter to target specific columnsDoes this pull request potentially affect one of the following parts:
@Public(Evolving): yes (new@PublicEvolvingDataFrame methods)Documentation
Was generative AI tooling used to co-author this PR?
Generated-by: Bob Shell 1.0.6