Skip to content
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ python -m pytest
python scripts/run_all.py
```

Python 3.11+. SQLite is still the stdlib path for the main lab. DuckDB now runs in CI on that compact fixture.
Python 3.11+. SQLite is the stdlib path and the tests that do not need DuckDB run after `pip install -e .`. The DuckDB comparisons need the extra; they are skipped if it is not installed. CI installs `.[duckdb]`.

## Query design

Expand Down
38 changes: 29 additions & 9 deletions tests/test_cross_engine_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import pandas as pd
import pytest

from sqlfeat.cross_engine import (
duckdb_point_in_time_features,
Expand All @@ -12,6 +13,17 @@
sqlite_rank_results,
)

try:
import duckdb as _duckdb # noqa: F401
except ImportError:
_HAS_DUCKDB = False
else:
_HAS_DUCKDB = True

_duckdb_only = pytest.mark.skipif(
not _HAS_DUCKDB, reason="install sqlfeat[duckdb] for the second-engine checks"
)


def _normalise(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy().sort_values("prediction_id").reset_index(drop=True)
Expand All @@ -22,17 +34,21 @@ def _normalise(df: pd.DataFrame) -> pd.DataFrame:
return out


def test_sqlite_duckdb_and_pandas_agree_on_point_in_time_features() -> None:
def test_sqlite_and_pandas_agree_on_point_in_time_features() -> None:
sqlite = _normalise(sqlite_point_in_time_features())
duckdb = _normalise(duckdb_point_in_time_features())
expected = _normalise(independent_expected_features())

pd.testing.assert_frame_equal(sqlite, expected, check_dtype=False, atol=1e-12, rtol=0)
pd.testing.assert_frame_equal(duckdb, expected, check_dtype=False, atol=1e-12, rtol=0)


@_duckdb_only
def test_duckdb_agrees_with_sqlite_on_point_in_time_features() -> None:
sqlite = _normalise(sqlite_point_in_time_features())
duckdb = _normalise(duckdb_point_in_time_features())
pd.testing.assert_frame_equal(duckdb, sqlite, check_dtype=False, atol=1e-12, rtol=0)


def test_future_sentinel_is_excluded_without_using_model_performance() -> None:
result = _normalise(duckdb_point_in_time_features())
result = _normalise(sqlite_point_in_time_features())
row = result.loc[result["prediction_id"] == "p1"].iloc[0]
assert row["txn_count"] == 2
assert row["spend"] == 30.0
Expand All @@ -48,13 +64,17 @@ def test_no_history_preserves_zero_aggregates_and_null_recency() -> None:
assert pd.isna(row["recency_days"])


def test_window_rank_semantics_match_across_engines() -> None:
def test_sqlite_rank_semantics() -> None:
sqlite = sqlite_rank_results().sort_values("id").reset_index(drop=True)
duckdb = duckdb_rank_results().sort_values("id").reset_index(drop=True)
pd.testing.assert_frame_equal(sqlite, duckdb, check_dtype=False)

# Tied score 9 gets rank 1 in both rows; the next ordinary rank is 3,
# while dense rank advances to 2. ROW_NUMBER is made deterministic by id.
assert list(sqlite["rank_value"]) == [1, 1, 3, 4]
assert list(sqlite["dense_rank_value"]) == [1, 1, 2, 3]
assert list(sqlite["row_number_value"]) == [1, 2, 3, 4]


@_duckdb_only
def test_window_rank_semantics_match_across_engines() -> None:
sqlite = sqlite_rank_results().sort_values("id").reset_index(drop=True)
duckdb = duckdb_rank_results().sort_values("id").reset_index(drop=True)
pd.testing.assert_frame_equal(sqlite, duckdb, check_dtype=False)
Loading