-
Notifications
You must be signed in to change notification settings - Fork 7k
[Data] Add Polars batch format support to map_batches #58896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,7 @@ | |
|
|
||
| if TYPE_CHECKING: | ||
| import pandas | ||
| import polars | ||
| import pyarrow | ||
|
|
||
| from ray.data._internal.planner.exchange.sort_task_spec import SortKey | ||
|
|
@@ -479,6 +480,55 @@ def to_arrow(self) -> "pyarrow.Table": | |
|
|
||
| return arrow_table | ||
|
|
||
| def to_polars(self) -> "polars.DataFrame": | ||
| """Convert this Pandas block into a Polars DataFrame. | ||
|
|
||
| Converts a Pandas DataFrame to a Polars DataFrame. See | ||
| https://docs.pola.rs/ for Polars documentation. | ||
|
|
||
| Note: This conversion creates a copy of the data. Zero-copy conversion | ||
| from Pandas to Polars is not possible. | ||
|
|
||
| Returns: | ||
| A Polars DataFrame containing the data. | ||
|
|
||
| Raises: | ||
| ImportError: If Polars is not installed. | ||
| ValueError: If the Pandas DataFrame has duplicate column names or | ||
| invalid column names. | ||
| """ | ||
| try: | ||
| import polars as pl | ||
| except ImportError: | ||
| raise ImportError( | ||
| "Polars is not installed. Install with `pip install polars`. " | ||
| "See https://docs.pola.rs/ for more information." | ||
| ) | ||
|
|
||
| # Validate column names before conversion | ||
| # Polars doesn't allow duplicate column names | ||
| if len(self._table.columns) != len(set(self._table.columns)): | ||
| duplicates = [ | ||
| col for col in self._table.columns | ||
| if list(self._table.columns).count(col) > 1 | ||
| ] | ||
| raise ValueError( | ||
| f"Pandas DataFrame has duplicate column names: {duplicates}. " | ||
| "Rename duplicate columns before converting to Polars." | ||
| ) | ||
|
Comment on lines
+510
to
+518
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check for duplicate columns is a bit inefficient. A more idiomatic and performant way to check for and find duplicates in a pandas Index is to use if not self._table.columns.is_unique:
duplicates = self._table.columns[self._table.columns.duplicated()].unique().tolist()
raise ValueError(
f"Pandas DataFrame has duplicate column names: {duplicates}. "
"Rename duplicate columns before converting to Polars."
) |
||
|
|
||
| # Validate column names are strings | ||
| for col in self._table.columns: | ||
| if not isinstance(col, str): | ||
| raise ValueError( | ||
| f"Pandas DataFrame has non-string column name: {col} (type: {type(col)}). " | ||
| "All column names must be strings for Polars conversion." | ||
| ) | ||
|
|
||
| # Convert to Polars DataFrame using from_pandas() | ||
| # See https://docs.pola.rs/api/dataframe/#polars.DataFrame.from_pandas | ||
| return pl.from_pandas(self._table) | ||
|
||
|
|
||
| def num_rows(self) -> int: | ||
| return self._table.shape[0] | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -420,6 +420,29 @@ def _try_wrap_udf_exception(e: Exception, item: Any = None): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _validate_batch_output(batch: Block) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Validate that a batch output from a UDF is a supported type. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| See https://docs.pola.rs/ for Polars documentation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Check for Polars DataFrame | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Polars is an optional dependency, so we check for it here | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import polars as pl | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(batch, pl.DataFrame): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Polars DataFrames are valid - DataFrame is always eager | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # LazyFrame is a separate class, so if we get here it's already a DataFrame | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif isinstance(batch, pl.LazyFrame): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "The `fn` you passed to `map_batches` returned a Polars LazyFrame. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "LazyFrames must be collected before returning. Use `.collect()` to " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "materialize the LazyFrame into a DataFrame. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "See https://docs.pola.rs/api/lazyframe/#collect for details." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except ImportError: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| batch, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -434,7 +457,7 @@ def _validate_batch_output(batch: Block) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "The `fn` you passed to `map_batches` returned a value of type " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"{type(batch)}. This isn't allowed -- `map_batches` expects " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "`fn` to return a `pandas.DataFrame`, `pyarrow.Table`, " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "`fn` to return a `pandas.DataFrame`, `polars.DataFrame`, `pyarrow.Table`, " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "`numpy.ndarray`, `list`, or `dict[str, numpy.ndarray]`." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -510,8 +533,33 @@ def transform_fn( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise e from None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Validate all yielded batches (for generators, validate each item) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for out_batch in res: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _validate_batch_output(out_batch) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Additional validation: ensure Polars DataFrames are eager | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # See https://docs.pola.rs/ for Polars documentation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import polars as pl | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(out_batch, pl.LazyFrame): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Generator yielded a Polars LazyFrame. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "All yielded frames must be materialized. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Call .collect() on LazyFrames before yielding. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "See https://docs.pola.rs/api/lazyframe/#collect for details." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif isinstance(out_batch, pl.DataFrame): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # DataFrame is always eager, but verify it's valid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Access schema to ensure DataFrame is valid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = out_batch.schema | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Polars DataFrame is in invalid state: {e}. " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Ensure the DataFrame is properly constructed." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) from e | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+544
to
+560
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The check for
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except ImportError: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield out_batch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return transform_fn | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states 'This conversion creates a copy of the data', but the code uses
copy=Falseincombine_chunks(). This is inconsistent and may confuse developers. Either update the comment to clarify thatcombine_chunksdoesn't copy but the subsequent Polars conversion does, or explain the complete copy behavior more accurately.