Skip to content

[FLINK-40196][python] Add missing-value handling to DataFrame API - #28979

Open
MattBelle wants to merge 1 commit into
apache:masterfrom
MattBelle:FLIP-40196
Open

[FLINK-40196][python] Add missing-value handling to DataFrame API#28979
MattBelle wants to merge 1 commit into
apache:masterfrom
MattBelle:FLIP-40196

Conversation

@MattBelle

Copy link
Copy Markdown

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

  • Added drop_null() method to remove rows containing NULL values
  • Added drop_nan() method to remove rows containing NaN values
  • Added fill_null() method to replace NULL values with specified values
  • Added fill_nan() method to replace NaN values with specified values
  • Implemented IS_NAN and IS_NOT_NAN built-in functions in Flink Table API (required infrastructure for NaN detection)
  • Added is_nan and is_not_nan helper functions to Expression API (mirrors is_null/is_not_null pattern)
  • Added helper methods _validate_subset() and _fill_values() for DRY code organization
  • Added comprehensive unit and integration tests for all new methods

Verifying this change

This change added tests and can be verified as follows:

  • Added unit tests (DataFrameDropNullTests, DataFrameDropNanTests, DataFrameFillNullTests, DataFrameFillNanTests) that verify:
    • Schema preservation for all operations
    • Parameter validation with comprehensive error case coverage:
      • Empty subset lists raise ValueError
      • Invalid column names raise ValueError with clear error messages
      • Non-list subset parameters raise TypeError
  • Added integration tests (DataFrameNullNanITTests) that verify correct behavior with real data:
    • drop_null() correctly removes rows with NULL values
    • drop_nan() correctly removes rows with NaN values
    • fill_null() correctly replaces NULL with specified values (numeric and string)
    • fill_nan() correctly replaces NaN with specified values
    • All methods work correctly with subset parameter to target specific columns
  • Tests cover success cases, error conditions, and different data types

Does this pull request potentially affect one of the following parts:

  • Dependencies (does it add or upgrade a dependency): no
  • The public API, i.e., is any changed class annotated with @Public(Evolving): yes (new @PublicEvolving DataFrame methods)
  • The serializers: no
  • The runtime per-record code paths (performance sensitive): no
  • Anything that affects deployment or recovery: JobManager (and its components), Checkpointing, Kubernetes/Yarn, ZooKeeper: no
  • The S3 file system connector: no

Documentation

  • Does this pull request introduce a new feature? yes
  • If yes, how is the feature documented? JavaDocs (comprehensive Python docstrings with examples and version tags)

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Bob Shell 1.0.6

@flinkbot

flinkbot commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands The @flinkbot bot supports the following commands:
  • @flinkbot run azure re-run the last Azure build

.. versionadded:: 2.4.0
"""
subset = self._validate_subset(subset)
conditions = [table_col(col_name).is_not_nan for col_name in subset]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

.. versionadded:: 2.4.0
"""
subset = self._validate_subset(subset)
conditions = [table_col(col_name).is_not_nan for col_name in subset]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

return self._fill_values(value, subset, lambda col: col.is_null)

@PublicEvolving()
def fill_nan(self, value: Any, subset: Optional[List[str]] = None) -> "DataFrame":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

raise TypeError("subset must be a list of strings")

if not subset:
raise ValueError("subset cannot be empty")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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)

@dianfu dianfu Aug 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@PublicEvolving()
def fill_nan(self, value: Any, subset: Optional[List[str]] = None) -> "DataFrame":
"""
Replace NaN values with a specified value (for float/double columns).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?


/** Implementation of {@link BuiltInFunctionDefinitions#IS_NOT_NAN}. */
@Internal
public final class IsNotNanFunction extends BuiltInScalarFunction {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need IsNotNan?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants