core-lens 0.1.dev189__tar.gz → 0.1.dev191__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/PKG-INFO +1 -1
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/namespaces/stats.py +87 -50
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.gitignore +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.gitmessage +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.python-version +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/LICENSE +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/README.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/SKILLS.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/README.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_aoi.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_entity.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_export.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_polars_utils.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_result.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_schema.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_season.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_spatial.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_view.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/run_all.sh +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/Makefile +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/make.bat +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/export.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/logging.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/queries.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/stats.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/pyproject.toml +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/__init__.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/aoi.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/entity.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/namespaces/plot.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/result.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/view.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/farms.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/mws.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/tehsil.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/export/formats.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/schema/detection.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/schema/profile.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/paths.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/polars_utils.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/season.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/spatial.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_entity.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_plot.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_view.py +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/usage.md +0 -0
- {core_lens-0.1.dev189 → core_lens-0.1.dev191}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: core-lens
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.dev191
|
|
4
4
|
Summary: Query, analyse, and visualise CoreStack's microwatershed and Earth science data through a clean, composable Python API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ApoorvaKashyap/core-lens
|
|
6
6
|
Project-URL: Issues, https://github.com/ApoorvaKashyap/core-lens/issues
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, '
|
|
21
|
+
__version__ = version = '0.1.dev191'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev191')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -741,6 +741,8 @@ class StatsNamespace:
|
|
|
741
741
|
}
|
|
742
742
|
return self._r._replace(data=data, has_geometry=False, metadata=metadata)
|
|
743
743
|
|
|
744
|
+
import warnings
|
|
745
|
+
|
|
744
746
|
def anomaly(
|
|
745
747
|
self,
|
|
746
748
|
column: str,
|
|
@@ -760,20 +762,32 @@ class StatsNamespace:
|
|
|
760
762
|
used as the reference/"normal" period. Required for timeseries; optional
|
|
761
763
|
for cross-sectional.
|
|
762
764
|
target_years (tuple[int, int] | None, optional): ``(from_year, to_year)`` inclusive,
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
765
|
+
years actually scored. Timeseries: defaults to "everything after
|
|
766
|
+
``baseline[1]``" if omitted. Cross-sectional: defaults to all rows.
|
|
767
|
+
Must not overlap ``baseline``.
|
|
766
768
|
threshold (float, optional): Sigma / score threshold for anomaly flag (default 2.0).
|
|
767
769
|
|
|
768
770
|
Returns:
|
|
769
771
|
Result: data has ``key_col | anomaly_score | is_anomaly`` (cross-sectional) or
|
|
770
|
-
``key_col |
|
|
771
|
-
restricted to ``target_years`` if given).
|
|
772
|
-
|
|
772
|
+
``key_col | <time_col> | anomaly_score | is_anomaly`` (timeseries, baseline
|
|
773
|
+
excluded, restricted to ``target_years`` if given). ``<time_col>`` is the
|
|
774
|
+
finer-grained date/period column when the data is sub-annual, falling back
|
|
775
|
+
to plain ``year`` for annual data. ``metadata`` includes ``n_entities_dropped``.
|
|
773
776
|
|
|
774
777
|
Raises:
|
|
775
778
|
ValueError: If ``mode``/``method`` invalid, observation count too low, or
|
|
776
779
|
``baseline`` and ``target_years`` overlap.
|
|
780
|
+
|
|
781
|
+
Notes:
|
|
782
|
+
**STL fitting scope.** For ``AnomalyTsMethod.STL`` the decomposition
|
|
783
|
+
(trend + seasonal + remainder) is fit on the *entire* series of each entity,
|
|
784
|
+
baseline and target years together. ``baseline`` is used only to calibrate the scale (standard deviation) of the remainder; ``target_years`` selects which points are scored. Scores are ``remainder / baseline_remainder_std``.
|
|
785
|
+
|
|
786
|
+
Because target observations take part in the fit, the trend and seasonal components can partly absorb target-period anomalies, shrinking the remainder and lowering the score. The effect grows with the size and duration of the anomaly; a sustained shift can be absorbed almost entirely into the trend. Scores therefore measure deviation from a locally fitted trend and seasonal pattern, not from the baseline's "normal" behaviour, and the method is conservative: it may miss anomalies but is unlikely to over-flag them.
|
|
787
|
+
|
|
788
|
+
A stricter alternative is to fit on the baseline only and forecast the target period (e.g. ``statsmodels.tsa.forecasting.stl.STLForecast``), scoring forecast errors against the baseline error spread. This is not implemented.
|
|
789
|
+
|
|
790
|
+
For sub-annual data, residuals are matched to rows by the finer time column (``date``/``period``), not by year, so each observation gets its own score.
|
|
777
791
|
"""
|
|
778
792
|
if baseline is not None and target_years is not None:
|
|
779
793
|
b_lo, b_hi = baseline
|
|
@@ -784,8 +798,6 @@ class StatsNamespace:
|
|
|
784
798
|
f"target_years={target_years} overlap. They must be disjoint."
|
|
785
799
|
)
|
|
786
800
|
|
|
787
|
-
# Single explicit collect boundary — everything below is eager by necessity
|
|
788
|
-
# (numpy/scipy/statsmodels have no lazy path).
|
|
789
801
|
df = self._r.df()
|
|
790
802
|
if isinstance(df, pl.LazyFrame):
|
|
791
803
|
df = df.collect()
|
|
@@ -819,7 +831,6 @@ class StatsNamespace:
|
|
|
819
831
|
f"but only got {len(ref_vals)}."
|
|
820
832
|
)
|
|
821
833
|
|
|
822
|
-
# target_years scopes which rows get scored/returned; baseline stays reference-only.
|
|
823
834
|
eval_df = df
|
|
824
835
|
if target_years is not None and has_year:
|
|
825
836
|
eval_df = df.filter(
|
|
@@ -889,7 +900,6 @@ class StatsNamespace:
|
|
|
889
900
|
"threshold": threshold,
|
|
890
901
|
}
|
|
891
902
|
|
|
892
|
-
# Build straight from numpy — no .tolist() round-trip.
|
|
893
903
|
data = eval_df.select(pl.col(key)).with_columns(
|
|
894
904
|
pl.Series("anomaly_score", scores, dtype=pl.Float64),
|
|
895
905
|
pl.Series("is_anomaly", flags, dtype=pl.Boolean),
|
|
@@ -913,6 +923,13 @@ class StatsNamespace:
|
|
|
913
923
|
"Ensure data is at annual or sub_annual resolution."
|
|
914
924
|
)
|
|
915
925
|
|
|
926
|
+
# Finer-grained time column for sub-annual data, if present.
|
|
927
|
+
# `year_col` still drives baseline/target-year windowing regardless.
|
|
928
|
+
date_col = next(
|
|
929
|
+
(c for c in ("date", "period", "period_start") if c in df.columns), None
|
|
930
|
+
)
|
|
931
|
+
output_time_col = date_col or year_col
|
|
932
|
+
|
|
916
933
|
min_obs = _MIN_OBS[method.value]
|
|
917
934
|
n_dropped = 0
|
|
918
935
|
|
|
@@ -963,14 +980,15 @@ class StatsNamespace:
|
|
|
963
980
|
.with_columns(
|
|
964
981
|
(pl.col("anomaly_score").abs() > threshold).alias("is_anomaly")
|
|
965
982
|
)
|
|
966
|
-
.select(key,
|
|
983
|
+
.select(key, output_time_col, "anomaly_score", "is_anomaly")
|
|
967
984
|
.drop_nulls("anomaly_score")
|
|
968
985
|
)
|
|
969
986
|
|
|
970
|
-
# ---- CUSUM / STL: inherently sequential/per-entity,
|
|
987
|
+
# ---- CUSUM / STL: inherently sequential/per-entity, partition_by instead of filter-loop ----
|
|
971
988
|
else:
|
|
972
989
|
rows: list[dict[str, Any]] = []
|
|
973
|
-
|
|
990
|
+
sort_cols = list(dict.fromkeys([year_col, output_time_col]))
|
|
991
|
+
for sub in df.sort(sort_cols).partition_by(key, maintain_order=True):
|
|
974
992
|
eid = sub[key][0]
|
|
975
993
|
base_vals = (
|
|
976
994
|
sub.filter(
|
|
@@ -992,10 +1010,15 @@ class StatsNamespace:
|
|
|
992
1010
|
)
|
|
993
1011
|
else:
|
|
994
1012
|
eval_sub = sub.filter(pl.col(year_col) > baseline[1])
|
|
995
|
-
|
|
996
|
-
|
|
1013
|
+
eval_sub = eval_sub.filter(pl.col(column).is_not_null())
|
|
1014
|
+
|
|
1015
|
+
eval_vals = eval_sub[column].to_numpy().astype(float)
|
|
1016
|
+
eval_years_lookup = eval_sub[
|
|
997
1017
|
year_col
|
|
998
|
-
].to_list()
|
|
1018
|
+
].to_list() # for baseline/dict matching
|
|
1019
|
+
eval_times = eval_sub[
|
|
1020
|
+
output_time_col
|
|
1021
|
+
].to_list() # for display in output
|
|
999
1022
|
|
|
1000
1023
|
ts_scores: list[float] = []
|
|
1001
1024
|
ts_flags: list[bool] = []
|
|
@@ -1013,43 +1036,56 @@ class StatsNamespace:
|
|
|
1013
1036
|
ts_flags.append(s > h)
|
|
1014
1037
|
|
|
1015
1038
|
else: # STL
|
|
1039
|
+
full = sub.filter(pl.col(column).is_not_null())
|
|
1040
|
+
full_vals = full[column].to_numpy().astype(float)
|
|
1041
|
+
full_years = full[year_col].to_list()
|
|
1042
|
+
full_times = full[output_time_col].to_list()
|
|
1043
|
+
|
|
1044
|
+
period = (
|
|
1045
|
+
26 if len(full_vals) >= 52 else max(2, len(full_vals) // 2)
|
|
1046
|
+
)
|
|
1047
|
+
min_required = 2 * period
|
|
1048
|
+
min_reliable = (
|
|
1049
|
+
3 * period
|
|
1050
|
+
) # want ~3 cycles minimum for a trustworthy seasonal fit, not just 2
|
|
1051
|
+
|
|
1052
|
+
# if len(full_vals) < min_reliable:
|
|
1053
|
+
# warnings.warn(
|
|
1054
|
+
# f"StatsNamespace.anomaly: entity {eid!r} has {len(full_vals)} points, "
|
|
1055
|
+
# f"period={period} needs >= {min_reliable} for a reliable STL fit. Skipping."
|
|
1056
|
+
# )
|
|
1057
|
+
# n_dropped += 1
|
|
1058
|
+
# continue
|
|
1059
|
+
|
|
1060
|
+
if len(full_vals) < min_required:
|
|
1061
|
+
warnings.warn(
|
|
1062
|
+
f"StatsNamespace.anomaly: entity {eid!r} has {len(full_vals)} points, "
|
|
1063
|
+
f"needs >= {min_required} for period={period}. Skipping."
|
|
1064
|
+
)
|
|
1065
|
+
n_dropped += 1
|
|
1066
|
+
continue
|
|
1067
|
+
|
|
1016
1068
|
try:
|
|
1017
1069
|
from statsmodels.tsa.seasonal import STL # type: ignore[import-untyped]
|
|
1018
1070
|
|
|
1019
|
-
full = sub.filter(pl.col(column).is_not_null())
|
|
1020
|
-
full_vals = full[column].to_numpy().astype(float)
|
|
1021
|
-
full_years = full[year_col].to_list()
|
|
1022
|
-
if len(full_vals) < min_obs:
|
|
1023
|
-
n_dropped += 1
|
|
1024
|
-
continue
|
|
1025
|
-
|
|
1026
|
-
# Seasonal period: sub-annual cadence assumed 24 steps/year
|
|
1027
|
-
# (e.g. fortnightly). Falls back to half the series length
|
|
1028
|
-
# for shorter records. Adjust if your data's cadence differs.
|
|
1029
|
-
period = (
|
|
1030
|
-
24
|
|
1031
|
-
if len(full_vals) >= 24
|
|
1032
|
-
else max(2, len(full_vals) // 2)
|
|
1033
|
-
)
|
|
1034
1071
|
res = STL(full_vals, period=period).fit()
|
|
1035
1072
|
resid = res.resid
|
|
1036
1073
|
|
|
1037
|
-
#
|
|
1038
|
-
#
|
|
1039
|
-
|
|
1040
|
-
resid_by_year = dict(zip(full_years, resid))
|
|
1074
|
+
# Key residuals by the fine-grained time column (date/period).
|
|
1075
|
+
# Keying by year collapses ~26 fortnights into one value.
|
|
1076
|
+
resid_by_time = dict(zip(full_times, resid))
|
|
1041
1077
|
base_resid = [
|
|
1042
|
-
|
|
1043
|
-
for y in full_years
|
|
1078
|
+
r
|
|
1079
|
+
for y, r in zip(full_years, resid)
|
|
1044
1080
|
if baseline[0] <= y <= baseline[1]
|
|
1045
1081
|
]
|
|
1046
1082
|
std = _sf(pl.Series(base_resid).std(ddof=1)) or 1.0
|
|
1047
1083
|
|
|
1048
1084
|
ts_scores = [
|
|
1049
|
-
float(
|
|
1050
|
-
if
|
|
1085
|
+
float(resid_by_time[t] / std)
|
|
1086
|
+
if t in resid_by_time
|
|
1051
1087
|
else float("nan")
|
|
1052
|
-
for
|
|
1088
|
+
for t in eval_times
|
|
1053
1089
|
]
|
|
1054
1090
|
ts_flags = [
|
|
1055
1091
|
abs(s) > threshold if s == s else False
|
|
@@ -1060,31 +1096,32 @@ class StatsNamespace:
|
|
|
1060
1096
|
warnings.warn(
|
|
1061
1097
|
f"StatsNamespace.anomaly: STL fit failed for entity {eid!r}: {e}"
|
|
1062
1098
|
)
|
|
1063
|
-
ts_scores = [float("nan")] * len(
|
|
1064
|
-
ts_flags = [False] * len(
|
|
1099
|
+
ts_scores = [float("nan")] * len(eval_years_lookup)
|
|
1100
|
+
ts_flags = [False] * len(eval_years_lookup)
|
|
1065
1101
|
|
|
1066
|
-
for
|
|
1102
|
+
for t, sc, fl in zip(eval_times, ts_scores, ts_flags):
|
|
1067
1103
|
rows.append(
|
|
1068
1104
|
{
|
|
1069
1105
|
key: eid,
|
|
1070
|
-
|
|
1106
|
+
output_time_col: t,
|
|
1071
1107
|
"anomaly_score": float(sc),
|
|
1072
1108
|
"is_anomaly": bool(fl),
|
|
1073
1109
|
}
|
|
1074
1110
|
)
|
|
1075
1111
|
|
|
1076
|
-
|
|
1077
|
-
pl.DataFrame(rows)
|
|
1078
|
-
|
|
1079
|
-
|
|
1112
|
+
if rows:
|
|
1113
|
+
data = pl.DataFrame(rows)
|
|
1114
|
+
else:
|
|
1115
|
+
data = pl.DataFrame(
|
|
1080
1116
|
{
|
|
1081
1117
|
key: pl.Series([], dtype=pl.String),
|
|
1082
|
-
|
|
1118
|
+
output_time_col: pl.Series(
|
|
1119
|
+
[], dtype=pl.Date if date_col else pl.Int32
|
|
1120
|
+
),
|
|
1083
1121
|
"anomaly_score": pl.Series([], dtype=pl.Float64),
|
|
1084
1122
|
"is_anomaly": pl.Series([], dtype=pl.Boolean),
|
|
1085
1123
|
}
|
|
1086
1124
|
)
|
|
1087
|
-
)
|
|
1088
1125
|
|
|
1089
1126
|
global_base_vals = (
|
|
1090
1127
|
df.filter(pl.col(year_col).is_between(baseline[0], baseline[1]))[column]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|