Skip to content
Open
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
22 changes: 22 additions & 0 deletions tests/test_contextual.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
import pandas as pd
import numpy as np
from tsauditor.anomaly.contextual import audit_contextual_anomalies
from tsauditor.report.summary import WARNING

Expand Down Expand Up @@ -92,3 +93,24 @@ def test_local_spike_fails_global_zscore(clean_financial_df):

spike_issues = [i for i in issues if i.code == "ANO003" and i.column == "Price"]
assert len(spike_issues) >= 1


def test_all_nan_column_skipped():
"""Column that is entirely NaN is skipped gracefully."""
dates = pd.date_range("2026-01-01", periods=20, freq="D")
df = pd.DataFrame(
{"all_nan": [np.nan] * 20, "valid": range(20)},
index=dates,
)
issues = audit_contextual_anomalies(df, domain="finance")
nan_issues = [i for i in issues if i.column == "all_nan"]
assert len(nan_issues) == 0


def test_single_row_df():
"""Single row DataFrame: returns empty list, no crash."""
dates = pd.date_range("2026-01-01", periods=1, freq="D")
df = pd.DataFrame({"val": [1.0]}, index=dates)
issues = audit_contextual_anomalies(df, domain="finance")
assert isinstance(issues, list)
assert len(issues) == 0
9 changes: 9 additions & 0 deletions tests/test_correlation.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,12 @@ def test_few_observations_skipped():
t = _iid_target(n, 8)
df = pd.DataFrame({"target": t, "leak": t.shift(-1)}, index=_idx(n))
assert audit_correlation_leakage(df, target="target", min_obs=30) == []


def test_single_row_df():
"""Single row DataFrame with target: returns empty list, no crash."""
dates = pd.date_range("2026-01-01", periods=1, freq="D")
df = pd.DataFrame({"target": [1.0], "feat": [2.0]}, index=dates)
issues = audit_correlation_leakage(df, target="target", min_obs=1)
assert isinstance(issues, list)
assert len(issues) == 0
9 changes: 9 additions & 0 deletions tests/test_equivalence.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,12 @@ def test_scattered_nans_use_pairwise_overlap():
issues = audit_equivalence(df, target="target")
assert "leak" in {i.column for i in issues}
assert next(i for i in issues if i.column == "leak").evidence["n_obs"] < n


def test_single_row_df():
"""Single row DataFrame with target: returns empty list, no crash."""
dates = pd.date_range("2026-01-01", periods=1, freq="D")
df = pd.DataFrame({"target": [1.0], "feat": [2.0]}, index=dates)
issues = audit_equivalence(df, target="target", min_obs=1)
assert isinstance(issues, list)
assert len(issues) == 0
28 changes: 28 additions & 0 deletions tests/test_missing.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,25 @@ def test_non_numeric_columns_ignored():
assert len(issues) == 0


def test_all_nan_column_reported_as_missing():
"""All-NaN column is correctly reported (100% missing rate, clustering).

Unlike anomaly detectors which skip all-NaN columns, the missing profiler
rightfully flags them — 100% missing is a genuine data quality issue.
"""
dates = pd.date_range("2026-01-01", periods=10, freq="D")
df = pd.DataFrame(
{"all_nan": [np.nan] * 10, "valid": range(10)},
index=dates,
)
issues = audit_missing(df, domain="finance")
nan_issues = [i for i in issues if i.column == "all_nan"]
# PRF006: high missing rate (100%), PRF002: clustered missing
codes = {i.code for i in nan_issues}
assert "PRF006" in codes
assert nan_issues[0].evidence["missing_percentage"] == 100.0


def test_sensor_domain_lower_threshold(sensor_df):
# Case 7 — Sensor domain with 3 consecutive NaNs -> PRF002 (lower threshold).
df = sensor_df.copy()
Expand All @@ -100,3 +119,12 @@ def test_sensor_domain_lower_threshold(sensor_df):
assert len(cluster_issues) == 1
assert cluster_issues[0].evidence["cluster_threshold"] == 3
assert cluster_issues[0].evidence["longest_consecutive_run"] == 3


def test_single_row_df():
"""Single row DataFrame: returns empty list, no crash."""
dates = pd.date_range("2026-01-01", periods=1, freq="D")
df = pd.DataFrame({"val": [1.0]}, index=dates)
issues = audit_missing(df, domain="finance")
assert isinstance(issues, list)
assert len(issues) == 0
20 changes: 20 additions & 0 deletions tests/test_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,23 @@ def test_audit_point_anomalies_finance_threshold(base_date_index):
# In finance (5.0), 4.5 is not an outlier
issues = audit_point_anomalies(df, domain="finance")
assert len(issues) == 0


def test_audit_point_anomalies_all_nan_column_skipped(base_date_index):
"""Column that is entirely NaN is skipped gracefully."""
df = pd.DataFrame(
{"all_nan": [np.nan] * 100, "valid": np.random.default_rng(42).normal(0, 1, 100)},
index=base_date_index,
)
issues = audit_point_anomalies(df)
nan_issues = [i for i in issues if i.column == "all_nan"]
assert len(nan_issues) == 0


def test_single_row_df():
"""Single row DataFrame: returns empty list, no crash."""
dates = pd.date_range("2026-01-01", periods=1, freq="D")
df = pd.DataFrame({"val": [1.0]}, index=dates)
issues = audit_point_anomalies(df)
assert isinstance(issues, list)
assert len(issues) == 0
9 changes: 9 additions & 0 deletions tests/test_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,12 @@ def test_sensor_domain_median_threshold(sensor_df):
issue = gap_issues[0]
assert issue.severity == WARNING
assert issue.evidence["maximum_gap_days"] >= 1.0


def test_single_row_df_missing():
"""Single row DataFrame for frequency: returns empty list, no crash."""
dates = pd.date_range("2026-01-01", periods=1, freq="D")
df = pd.DataFrame({"val": [1.0]}, index=dates)
issues = audit_frequency(df, domain="finance")
assert isinstance(issues, list)
assert len(issues) == 0
21 changes: 21 additions & 0 deletions tests/test_stationarity.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,24 @@ def test_audit_stationarity_with_nan_and_inf(base_date_index):

issues = audit_stationarity(df, min_obs=25)
assert isinstance(issues, list)


def test_all_nan_column_skipped(base_date_index):
"""Column that is entirely NaN is skipped gracefully."""
df = pd.DataFrame(
{"all_nan": [np.nan] * 100, "valid": np.random.randn(100)},
index=base_date_index,
)
issues = audit_stationarity(df, min_obs=25)
nan_issues = [i for i in issues if i.column == "all_nan"]
assert len(nan_issues) == 0


def test_single_row_df_raises_without_guard():
"""Single row DataFrame passes min_obs=0 but statsmodels adfuller
rejects constant input — guard is missing (known issue)."""
dates = pd.date_range("2026-01-01", periods=1, freq="D")
df = pd.DataFrame({"val": [1.0]}, index=dates)
# TODO: remove pytest.raises once a min_obs guard is added upstream
with pytest.raises(ValueError, match="constant"):
audit_stationarity(df, min_obs=0)
9 changes: 9 additions & 0 deletions tests/test_temporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,12 @@ def test_few_observations_skipped():
t = _ar1(n, seed=8)
df = pd.DataFrame({"target": t, "leak": t.shift(-1)}, index=_idx(n))
assert audit_temporal_leakage(df, target="target", min_obs=30) == []


def test_single_row_df():
"""Single row DataFrame with target: returns empty list, no crash."""
dates = pd.date_range("2026-01-01", periods=1, freq="D")
df = pd.DataFrame({"target": [1.0], "feat": [2.0]}, index=dates)
issues = audit_temporal_leakage(df, target="target", min_obs=1)
assert isinstance(issues, list)
assert len(issues) == 0