Skip to content

Commit

Permalink
feat: Implement/fix unary minus operator -pl.col(...) (pola-rs#13776)
Browse files Browse the repository at this point in the history
  • Loading branch information
stinodego authored and r-brink committed Jan 24, 2024
1 parent c30e1d4 commit c00ccc6
Show file tree
Hide file tree
Showing 15 changed files with 185 additions and 30 deletions.
2 changes: 2 additions & 0 deletions crates/polars-ops/src/series/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod is_unique;
mod log;
#[cfg(feature = "moment")]
mod moment;
mod negate;
#[cfg(feature = "pct_change")]
mod pct_change;
#[cfg(feature = "rank")]
Expand Down Expand Up @@ -94,6 +95,7 @@ pub use is_unique::*;
pub use log::*;
#[cfg(feature = "moment")]
pub use moment::*;
pub use negate::*;
#[cfg(feature = "pct_change")]
pub use pct_change::*;
use polars_core::prelude::*;
Expand Down
42 changes: 42 additions & 0 deletions crates/polars-ops/src/series/ops/negate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use num_traits::Signed;
use polars_core::prelude::*;

fn negate_numeric<T>(ca: &ChunkedArray<T>) -> ChunkedArray<T>
where
T: PolarsNumericType,
T::Native: Signed,
{
ca.apply_values(|v| -v)
}

pub fn negate(s: &Series) -> PolarsResult<Series> {
use DataType::*;
let out = match s.dtype() {
#[cfg(feature = "dtype-i8")]
Int8 => negate_numeric(s.i8().unwrap()).into_series(),
#[cfg(feature = "dtype-i16")]
Int16 => negate_numeric(s.i16().unwrap()).into_series(),
Int32 => negate_numeric(s.i32().unwrap()).into_series(),
Int64 => negate_numeric(s.i64().unwrap()).into_series(),
Float32 => negate_numeric(s.f32().unwrap()).into_series(),
Float64 => negate_numeric(s.f64().unwrap()).into_series(),
#[cfg(feature = "dtype-decimal")]
Decimal(_, _) => {
let ca = s.decimal().unwrap();
let precision = ca.precision();
let scale = ca.scale();

let out = negate_numeric(ca.as_ref());
out.into_decimal_unchecked(precision, scale).into_series()
},
#[cfg(feature = "dtype-duration")]
Duration(_) => {
let physical = s.to_physical_repr();
let ca = physical.i64().unwrap();
let out = negate_numeric(ca).into_series();
out.cast(s.dtype())?
},
dt => polars_bail!(opq = neg, dt),
};
Ok(out)
}
10 changes: 9 additions & 1 deletion crates/polars-plan/src/dsl/arithmetic.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::ops::{Add, Div, Mul, Rem, Sub};
use std::ops::{Add, Div, Mul, Neg, Rem, Sub};

use super::*;

Expand Down Expand Up @@ -43,6 +43,14 @@ impl Rem for Expr {
}
}

impl Neg for Expr {
type Output = Expr;

fn neg(self) -> Self::Output {
self.map_private(FunctionExpr::Negate)
}
}

impl Expr {
/// Floor divide `self` by `rhs`.
pub fn floor_div(self, rhs: Self) -> Self {
Expand Down
4 changes: 4 additions & 0 deletions crates/polars-plan/src/dsl/function_expr/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,7 @@ pub(super) fn gather_every(s: &Series, n: usize, offset: usize) -> PolarsResult<
pub(super) fn reinterpret(s: &Series, signed: bool) -> PolarsResult<Series> {
polars_ops::series::reinterpret(s, signed)
}

pub(super) fn negate(s: &Series) -> PolarsResult<Series> {
polars_ops::series::negate(s)
}
4 changes: 4 additions & 0 deletions crates/polars-plan/src/dsl/function_expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ pub enum FunctionExpr {
Boolean(BooleanFunction),
#[cfg(feature = "abs")]
Abs,
Negate,
#[cfg(feature = "hist")]
Hist {
bin_count: Option<usize>,
Expand Down Expand Up @@ -383,6 +384,7 @@ impl Hash for FunctionExpr {
Mode => {},
#[cfg(feature = "abs")]
Abs => {},
Negate => {},
NullCount => {},
#[cfg(feature = "date_offset")]
DateOffset => {},
Expand Down Expand Up @@ -558,6 +560,7 @@ impl Display for FunctionExpr {
Boolean(func) => return write!(f, "{func}"),
#[cfg(feature = "abs")]
Abs => "abs",
Negate => "negate",
NullCount => "null_count",
Pow(func) => return write!(f, "{func}"),
#[cfg(feature = "row_hash")]
Expand Down Expand Up @@ -805,6 +808,7 @@ impl From<FunctionExpr> for SpecialEq<Arc<dyn SeriesUdf>> {
Boolean(func) => func.into(),
#[cfg(feature = "abs")]
Abs => map!(abs::abs),
Negate => map!(dispatch::negate),
NullCount => {
let f = |s: &mut [Series]| {
let s = &s[0];
Expand Down
1 change: 1 addition & 0 deletions crates/polars-plan/src/dsl/function_expr/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ impl FunctionExpr {
Boolean(func) => func.get_field(mapper),
#[cfg(feature = "abs")]
Abs => mapper.with_same_dtype(),
Negate => mapper.with_same_dtype(),
NullCount => mapper.with_dtype(IDX_DTYPE),
Pow(pow_function) => match pow_function {
PowFunction::Generic => mapper.pow_dtype(),
Expand Down
1 change: 1 addition & 0 deletions py-polars/docs/source/reference/expressions/operators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Numeric
Expr.floordiv
Expr.mod
Expr.mul
Expr.neg
Expr.sub
Expr.truediv
Expr.pow
Expand Down
34 changes: 25 additions & 9 deletions py-polars/polars/expr/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,8 @@ def __ne__(self, other: IntoExpr) -> Self: # type: ignore[override]
other = parse_as_expression(other, str_as_lit=True)
return self._from_pyexpr(self._pyexpr.neq(other))

def __neg__(self) -> Expr:
neg_expr = F.lit(0) - self
if (name := self.meta.output_name(raise_if_undetermined=False)) is not None:
neg_expr = neg_expr.alias(name)
return neg_expr
def __neg__(self) -> Self:
return self._from_pyexpr(-self._pyexpr)

def __or__(self, other: IntoExprColumn | int | bool) -> Self:
other = parse_as_expression(other)
Expand All @@ -239,10 +236,7 @@ def __ror__(self, other: IntoExprColumn | int | bool) -> Self:
return self._from_pyexpr(other_expr._or(self._pyexpr))

def __pos__(self) -> Expr:
pos_expr = F.lit(0) + self
if (name := self.meta.output_name(raise_if_undetermined=False)) is not None:
pos_expr = pos_expr.alias(name)
return pos_expr
return self

def __pow__(self, exponent: IntoExprColumn | int | float) -> Self:
exponent = parse_as_expression(exponent)
Expand Down Expand Up @@ -5120,6 +5114,28 @@ def sub(self, other: Any) -> Self:
"""
return self.__sub__(other)

def neg(self) -> Self:
"""
Method equivalent of unary minus operator `-expr`.
Examples
--------
>>> df = pl.DataFrame({"a": [-1, 0, 2, None]})
>>> df.with_columns(pl.col("a").neg())
shape: (4, 1)
┌──────┐
│ a │
│ --- │
│ i64 │
╞══════╡
│ 1 │
│ 0 │
│ -2 │
│ null │
└──────┘
"""
return self.__neg__()

def truediv(self, other: Any) -> Self:
"""
Method equivalent of float division operator `expr / other`.
Expand Down
4 changes: 2 additions & 2 deletions py-polars/polars/series/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -1185,10 +1185,10 @@ def __rmatmul__(self, other: Any) -> float | Series | None:
return other.dot(self)

def __neg__(self) -> Series:
return 0 - self
return self.to_frame().select_seq(-F.col(self.name)).to_series()

def __pos__(self) -> Series:
return 0 + self
return self

def __abs__(self) -> Series:
return self.abs()
Expand Down
5 changes: 5 additions & 0 deletions py-polars/src/expr/general.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::ops::Neg;

use polars::lazy::dsl;
use polars::prelude::*;
use polars::series::ops::NullBehavior;
Expand Down Expand Up @@ -43,6 +45,9 @@ impl PyExpr {
fn __floordiv__(&self, rhs: Self) -> PyResult<Self> {
Ok(dsl::binary_expr(self.inner.clone(), Operator::FloorDivide, rhs.inner).into())
}
fn __neg__(&self) -> PyResult<Self> {
Ok(self.inner.clone().neg().into())
}

fn to_str(&self) -> String {
format!("{:?}", self.inner)
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,6 @@ def test_floor_division_float_int_consistency() -> None:
)


def test_unary_plus() -> None:
data = [1, 2]
df = pl.DataFrame({"x": data})
assert df.select(+pl.col("x"))[:, 0].to_list() == data

with pytest.raises(pl.exceptions.ComputeError):
pl.select(+pl.lit(""))


def test_series_expr_arithm() -> None:
s = pl.Series([1, 2, 3])
assert (s + pl.col("a")).meta == pl.lit(s) + pl.col("a")
Expand Down
70 changes: 70 additions & 0 deletions py-polars/tests/unit/operations/arithmetic/test_neg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from __future__ import annotations

from datetime import timedelta
from decimal import Decimal as D

import pytest

import polars as pl
from polars.testing import assert_frame_equal
from polars.testing.asserts.series import assert_series_equal


@pytest.mark.parametrize(
"dtype", [pl.Int8, pl.Int16, pl.Int32, pl.Int64, pl.Float32, pl.Float64]
)
def test_neg_operator(dtype: pl.PolarsDataType) -> None:
lf = pl.LazyFrame({"a": [-1, 0, 1, None]}, schema={"a": dtype})
result = lf.select(-pl.col("a"))
expected = pl.LazyFrame({"a": [1, 0, -1, None]}, schema={"a": dtype})
assert_frame_equal(result, expected)


def test_neg_method() -> None:
lf = pl.LazyFrame({"a": [-1, 0, 1, None]})
result_op = lf.select(-pl.col("a"))
result_method = lf.select(pl.col("a").neg())
assert_frame_equal(result_op, result_method)


def test_neg_decimal() -> None:
lf = pl.LazyFrame({"a": [D("-1.5"), D("0.0"), D("5.0"), None]})
result = lf.select(-pl.col("a"))
expected = pl.LazyFrame({"a": [D("1.5"), D("0.0"), D("-5.0"), None]})
assert_frame_equal(result, expected)


def test_neg_duration() -> None:
lf = pl.LazyFrame({"a": [timedelta(hours=2), timedelta(days=-2), None]})
result = lf.select(-pl.col("a"))
expected = pl.LazyFrame({"a": [timedelta(hours=-2), timedelta(days=2), None]})
assert_frame_equal(result, expected)


def test_neg_overflow() -> None:
df = pl.DataFrame({"a": [-128]}, schema={"a": pl.Int8})
with pytest.raises(pl.PolarsPanicError, match="attempt to negate with overflow"):
df.select(-pl.col("a"))


def test_neg_unsigned_int() -> None:
df = pl.DataFrame({"a": [1, 2, 3]}, schema={"a": pl.UInt8})
with pytest.raises(
pl.InvalidOperationError, match="`neg` operation not supported for dtype `u8`"
):
df.select(-pl.col("a"))


def test_neg_non_numeric() -> None:
df = pl.DataFrame({"a": ["p", "q", "r"]})
with pytest.raises(
pl.InvalidOperationError, match="`neg` operation not supported for dtype `str`"
):
df.select(-pl.col("a"))


def test_neg_series_operator() -> None:
s = pl.Series("a", [-1, 0, 1, None])
result = -s
expected = pl.Series("a", [1, 0, -1, None])
assert_series_equal(result, expected)
20 changes: 20 additions & 0 deletions py-polars/tests/unit/operations/arithmetic/test_pos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from datetime import datetime

import polars as pl
from polars.testing import assert_frame_equal, assert_series_equal


def test_pos() -> None:
df = pl.LazyFrame({"x": [1, 2]})
result = df.select(+pl.col("x"))
assert_frame_equal(result, df)


def test_pos_string() -> None:
a = pl.Series("a", [""])
assert_series_equal(+a, a)


def test_pos_datetime() -> None:
a = pl.Series("a", [datetime(2022, 1, 1)])
assert_series_equal(+a, a)
9 changes: 0 additions & 9 deletions py-polars/tests/unit/series/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,15 +430,6 @@ def test_arithmetic_datetime() -> None:
with pytest.raises(TypeError):
2**a

with pytest.raises(TypeError):
+a


def test_arithmetic_string() -> None:
a = pl.Series("a", [""])
with pytest.raises(TypeError):
+a


def test_power() -> None:
a = pl.Series([1, 2], dtype=Int64)
Expand Down

0 comments on commit c00ccc6

Please sign in to comment.