Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions tests/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,21 @@ def test_none_target():
to_numpy(series_pd.drop(columns=["unique_id", "ds"])),
)


@pytest.mark.parametrize("engine", ["pandas", "polars"])
def test_process_df_null_ids(engine):
constructor = pd.DataFrame if engine == "pandas" else pl.DataFrame
df = constructor(
{
"unique_id": ["a", "a", None, "b"],
"ds": [1, 2, 1, 1],
"y": [1.0, 2.0, 3.0, 4.0],
}
)
with pytest.raises(ValueError, match="id column .* contains null values"):
process_df(df, "unique_id", "ds", "y")


@pytest.mark.parametrize("n_static_features", [0, 2])
def test_n_static_features(static_features, n_static_features):
series_pl = generate_series(
Expand Down
44 changes: 44 additions & 0 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import datetime

import numpy as np
import pandas as pd
import polars as pl
import pytest
from conftest import assert_raises_with_message

from utilsforecast.compat import POLARS_INSTALLED
Expand Down Expand Up @@ -71,6 +73,48 @@ def test_validate_format():
assert_raises_with_message(validate_format, "('sales') should have a numeric data type", df, target_col='sales')


@pytest.mark.parametrize(
"ids",
[
pd.Series(["a", None]),
pd.Series(["a", np.nan]),
pd.Series(["a", None], dtype="string[pyarrow]"),
pd.Series(["a", np.nan], dtype="category"),
pd.Series([1, None], dtype="Int64"),
pd.Series([1.0, np.nan]),
pl.Series(["a", None]),
pl.Series(["a", None], dtype=pl.Categorical),
pl.Series([1, None]),
],
)
@pytest.mark.parametrize("target_col", ["y", None])
def test_validate_format_null_ids(ids, target_col):
constructor = pd.DataFrame if isinstance(ids, pd.Series) else pl.DataFrame
df = constructor({"uid": ids, "ds": [1, 2], "y": [1.0, 2.0]})
with pytest.raises(ValueError, match=r"The id column \('uid'\) contains null"):
validate_format(df, id_col="uid", target_col=target_col)


@pytest.mark.parametrize(
"times",
[
pd.Series(pd.to_datetime(["2000-01-01", None])),
pd.Series([datetime.date(2000, 1, 1), None], dtype="date32[pyarrow]"),
pd.Series([1, None], dtype="Int64"),
pd.Series([1, None]),
pl.Series([datetime.datetime(2000, 1, 1), None]),
pl.Series([datetime.date(2000, 1, 1), None]),
pl.Series([1, None]),
],
)
@pytest.mark.parametrize("target_col", ["y", None])
def test_validate_format_null_times(times, target_col):
constructor = pd.DataFrame if isinstance(times, pd.Series) else pl.DataFrame
df = constructor({"unique_id": [1, 1], "time": times, "y": [1.0, 2.0]})
with pytest.raises(ValueError, match=r"The time column \('time'\) contains null"):
validate_format(df, time_col="time", target_col=target_col)


def test_validate_freq():
assert_raises_with_message(validate_freq, 'provide a valid integer',pd.Series([1, 2]), 'D')
assert_raises_with_message(validate_freq, 'provide a valid pandas or polars offset', pd.to_datetime(['2000-01-01']).to_series(), 1),
Expand Down
12 changes: 12 additions & 0 deletions utilsforecast/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def _is_dt_or_int(s: Series) -> bool:
return _is_dt_dtype(s) or _is_int_dtype(s)


def _has_nulls(s: Series) -> bool:
if isinstance(s, pd.Series):
return s.hasnans
return s.null_count() > 0

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.

null_count() doesn't count NaN in polars, so a float id column with NaN passes here while the pandas branch (hasnans) rejects it:

validate_format(pl.DataFrame({'unique_id': [1.0, float('nan')], 'ds': [1, 2], 'y': [1., 2.]}))  # no error

Those ids then reach process_df/fill_gaps and get misgrouped. Something like s.null_count() > 0 or (s.dtype.is_float() and s.is_nan().any()) would match pandas. Adding pl.Series([1.0, float('nan')]) to test_validate_format_null_ids would cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

NaNs get grouped together in polars



def ensure_shallow_copy(df: pd.DataFrame) -> pd.DataFrame:
from packaging.version import Version

Expand Down Expand Up @@ -106,6 +112,12 @@ def validate_format(
if missing_cols:
raise ValueError(f"The following columns are missing: {missing_cols}")

# nulls
if _has_nulls(df[id_col]):
raise ValueError(f"The id column ('{id_col}') contains null values.")
if _has_nulls(df[time_col]):
raise ValueError(f"The time column ('{time_col}') contains null values.")

# time col
if not _is_dt_or_int(df[time_col]):
times_dtype = df[time_col].dtype
Expand Down
Loading