Skip to content

Commit 6bf1731

Browse files
feat: Add coalesce for all backends (#2647)
* add coalesce for all backends * add coalesce docstring, stable version, and protocol info * add simple coalesce v1 test * fix coalesce to appropriately cast literals * coalesce mark backends that do not upcast as xfail * coalesce to accept literals, not unwrapped scalars * fix up coalesce v1 typing * lazy coalesce backends use _with_elementwise * remove test coalesce upcast * enh coalesce tests explicit & implicit lit * enh coalesce push literal conversion to backends * use self._series._align_full_broadcast * replace _with_elementwise * remove coalesce manual literal guessing * Enh raise in coalesce non {str,lit,Series} inputs - add coalesce test for series inputs - add coalesce test for raising TypeError from inappropriate inputs - remove coalesce tests for scalar inputs, only accept valid IntoExpr * ref add in upstream elementwise changes * remove dask backend_version from coalesce * clean up _pandas_like namespace diff * cleanup coalesce ibis impl * enh coalesce docstring and error message --------- Co-authored-by: Francesco Bruzzesi <[email protected]> Co-authored-by: FBruzzesi <[email protected]>
1 parent ac44fe1 commit 6bf1731

File tree

14 files changed

+236
-3
lines changed

14 files changed

+236
-3
lines changed

docs/api-reference/narwhals.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Here are the top-level functions available in Narwhals.
99
- all
1010
- all_horizontal
1111
- any_horizontal
12+
- coalesce
1213
- col
1314
- concat
1415
- concat_str

narwhals/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
all_ as all,
5050
all_horizontal,
5151
any_horizontal,
52+
coalesce,
5253
col,
5354
concat,
5455
concat_str,
@@ -127,6 +128,7 @@
127128
"all",
128129
"all_horizontal",
129130
"any_horizontal",
131+
"coalesce",
130132
"col",
131133
"concat",
132134
"concat_str",

narwhals/_arrow/namespace.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,27 @@ def func(df: ArrowDataFrame) -> list[ArrowSeries]:
262262
context=self,
263263
)
264264

265+
def coalesce(self, *exprs: ArrowExpr) -> ArrowExpr:
266+
def func(df: ArrowDataFrame) -> list[ArrowSeries]:
267+
align = self._series._align_full_broadcast
268+
init_series, *series = align(*chain.from_iterable(expr(df) for expr in exprs))
269+
return [
270+
ArrowSeries(
271+
pc.coalesce(init_series.native, *(s.native for s in series)),
272+
name=init_series.name,
273+
version=self._version,
274+
)
275+
]
276+
277+
return self._expr._from_callable(
278+
func=func,
279+
depth=max(x._depth for x in exprs) + 1,
280+
function_name="coalesce",
281+
evaluate_output_names=combine_evaluate_output_names(*exprs),
282+
alias_output_names=combine_alias_output_names(*exprs),
283+
context=self,
284+
)
285+
265286

266287
class ArrowWhen(EagerWhen[ArrowDataFrame, ArrowSeries, ArrowExpr, "ChunkedArrayAny"]):
267288
@property

narwhals/_compliant/namespace.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def concat_str(
9696
def selectors(self) -> CompliantSelectorNamespace[Any, Any]: ...
9797
@property
9898
def _expr(self) -> type[CompliantExprT]: ...
99+
def coalesce(self, *exprs: CompliantExprT) -> CompliantExprT: ...
99100

100101

101102
class DepthTrackingNamespace(

narwhals/_dask/namespace.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,22 @@ def func(df: DaskLazyFrame) -> list[dx.Series]:
286286
version=self._version,
287287
)
288288

289+
def coalesce(self, *exprs: DaskExpr) -> DaskExpr:
290+
def func(df: DaskLazyFrame) -> list[dx.Series]:
291+
series = align_series_full_broadcast(
292+
df, *(s for _expr in exprs for s in _expr(df))
293+
)
294+
return [reduce(lambda x, y: x.fillna(y), series)]
295+
296+
return self._expr(
297+
call=func,
298+
depth=max(x._depth for x in exprs) + 1,
299+
function_name="coalesce",
300+
evaluate_output_names=combine_evaluate_output_names(*exprs),
301+
alias_output_names=combine_alias_output_names(*exprs),
302+
version=self._version,
303+
)
304+
289305

290306
class DaskWhen(CompliantWhen[DaskLazyFrame, "dx.Series", DaskExpr]):
291307
@property

narwhals/_duckdb/namespace.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ def func(_df: DuckDBLazyFrame) -> list[Expression]:
174174
version=self._version,
175175
)
176176

177+
def coalesce(self, *exprs: DuckDBExpr) -> DuckDBExpr:
178+
def func(cols: Iterable[Expression]) -> Expression:
179+
return CoalesceOperator(*cols)
180+
181+
return self._expr._from_elementwise_horizontal_op(func, *exprs)
182+
177183

178184
class DuckDBWhen(LazyWhen["DuckDBLazyFrame", Expression, DuckDBExpr]):
179185
@property

narwhals/_ibis/namespace.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ def func(_df: IbisLazyFrame) -> list[ir.Value]:
160160
version=self._version,
161161
)
162162

163+
def coalesce(self, *exprs: IbisExpr) -> IbisExpr:
164+
def func(cols: Iterable[ir.Value]) -> ir.Value:
165+
return ibis.coalesce(*cols)
166+
167+
return self._expr._from_elementwise_horizontal_op(func, *exprs)
168+
163169

164170
class IbisWhen(LazyWhen["IbisLazyFrame", "ir.Value", IbisExpr]):
165171
lit = lit

narwhals/_pandas_like/namespace.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ def __init__(self, implementation: Implementation, version: Version) -> None:
6868
self._implementation = implementation
6969
self._version = version
7070

71+
def coalesce(self, *exprs: PandasLikeExpr) -> PandasLikeExpr:
72+
def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
73+
align = self._series._align_full_broadcast
74+
series = align(*(s for _expr in exprs for s in _expr(df)))
75+
return [
76+
reduce(lambda x, y: x.fill_null(y, strategy=None, limit=None), series)
77+
]
78+
79+
return self._expr._from_callable(
80+
func=func,
81+
depth=max(x._depth for x in exprs) + 1,
82+
function_name="coalesce",
83+
evaluate_output_names=combine_evaluate_output_names(*exprs),
84+
alias_output_names=combine_alias_output_names(*exprs),
85+
context=self,
86+
)
87+
7188
def lit(self, value: NonNestedLiteral, dtype: IntoDType | None) -> PandasLikeExpr:
7289
def _lit_pandas_series(df: PandasLikeDataFrame) -> PandasLikeSeries:
7390
pandas_series = self._series.from_iterable(

narwhals/_polars/namespace.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
class PolarsNamespace:
2828
all: Method[PolarsExpr]
29+
coalesce: Method[PolarsExpr]
2930
col: Method[PolarsExpr]
3031
exclude: Method[PolarsExpr]
3132
sum_horizontal: Method[PolarsExpr]

narwhals/_spark_like/namespace.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,12 @@ def func(df: SparkLikeLazyFrame) -> list[Column]:
236236
def when(self, predicate: SparkLikeExpr) -> SparkLikeWhen:
237237
return SparkLikeWhen.from_expr(predicate, context=self)
238238

239+
def coalesce(self, *exprs: SparkLikeExpr) -> SparkLikeExpr:
240+
def func(cols: Iterable[Column]) -> Column:
241+
return self._F.coalesce(*cols)
242+
243+
return self._expr._from_elementwise_horizontal_op(func, *exprs)
244+
239245

240246
class SparkLikeWhen(LazyWhen[SparkLikeLazyFrame, "Column", SparkLikeExpr]):
241247
@property

0 commit comments

Comments
 (0)