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.
Files changed (94) hide show
  1. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/PKG-INFO +1 -1
  2. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/_version.py +2 -2
  3. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/namespaces/stats.py +87 -50
  4. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  5. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  6. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  7. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/pull_request_template.md +0 -0
  8. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/workflows/ci.yml +0 -0
  9. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/workflows/gh-pages.yml +0 -0
  10. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/workflows/pre-release.yml +0 -0
  11. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.github/workflows/release.yml +0 -0
  12. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.gitignore +0 -0
  13. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.gitmessage +0 -0
  14. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.pre-commit-config.yaml +0 -0
  15. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/.python-version +0 -0
  16. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/CONTRIBUTING.md +0 -0
  17. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/LICENSE +0 -0
  18. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/README.md +0 -0
  19. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/SKILLS.md +0 -0
  20. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/README.md +0 -0
  21. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_aoi.py +0 -0
  22. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_entity.py +0 -0
  23. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_export.py +0 -0
  24. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_polars_utils.py +0 -0
  25. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_result.py +0 -0
  26. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_schema.py +0 -0
  27. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_season.py +0 -0
  28. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_spatial.py +0 -0
  29. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/bench_view.py +0 -0
  30. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/benchmarks/run_all.sh +0 -0
  31. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/Makefile +0 -0
  32. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/make.bat +0 -0
  33. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/concepts.md +0 -0
  34. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/conf.py +0 -0
  35. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/export.md +0 -0
  36. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/index.rst +0 -0
  37. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/intro.md +0 -0
  38. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/logging.md +0 -0
  39. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/plots.md +0 -0
  40. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/plugins.md +0 -0
  41. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/queries.md +0 -0
  42. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/quickstart.md +0 -0
  43. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/docs/source/stats.md +0 -0
  44. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/examples/demo_mws.py +0 -0
  45. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/examples/demo_tehsil.py +0 -0
  46. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/hooks/mypy.sh +0 -0
  47. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/hooks/no-parquet-outside-fixtures.sh +0 -0
  48. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/hooks/pytest.sh +0 -0
  49. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/pyproject.toml +0 -0
  50. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/__init__.py +0 -0
  51. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/__main__.py +0 -0
  52. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/aoi.py +0 -0
  53. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/__init__.py +0 -0
  54. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/entity.py +0 -0
  55. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/namespaces/__init__.py +0 -0
  56. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/namespaces/plot.py +0 -0
  57. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/result.py +0 -0
  58. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/base/view.py +0 -0
  59. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/__init__.py +0 -0
  60. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/farms.py +0 -0
  61. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/mws.py +0 -0
  62. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/tehsil.py +0 -0
  63. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/entities/waterbody.py +0 -0
  64. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/export/__init__.py +0 -0
  65. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/export/formats.py +0 -0
  66. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/py.typed +0 -0
  67. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/schema/__init__.py +0 -0
  68. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/schema/detection.py +0 -0
  69. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/schema/profile.py +0 -0
  70. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/__init__.py +0 -0
  71. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/paths.py +0 -0
  72. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/polars_utils.py +0 -0
  73. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/season.py +0 -0
  74. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/src/core_lens/utils/spatial.py +0 -0
  75. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/fixtures/generate_fixtures.py +0 -0
  76. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/conftest.py +0 -0
  77. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_aoi.py +0 -0
  78. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_entities.py +0 -0
  79. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_entity.py +0 -0
  80. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_export.py +0 -0
  81. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_main.py +0 -0
  82. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_plot.py +0 -0
  83. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_polars_utils.py +0 -0
  84. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_profile.py +0 -0
  85. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_result.py +0 -0
  86. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_schema_detection.py +0 -0
  87. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_schema_profile.py +0 -0
  88. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_season.py +0 -0
  89. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_season_config.py +0 -0
  90. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_spatial.py +0 -0
  91. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_stats.py +0 -0
  92. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/tests/unit/test_view.py +0 -0
  93. {core_lens-0.1.dev189 → core_lens-0.1.dev191}/usage.md +0 -0
  94. {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.dev189
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.dev189'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev189')
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
- the years actually scored for anomalies. Timeseries: defaults to "everything
764
- after ``baseline[1]``" if omitted (old behaviour preserved). Cross-sectional:
765
- defaults to all rows if omitted. Must not overlap ``baseline``.
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 | year | anomaly_score | is_anomaly`` (timeseries, baseline excluded,
771
- restricted to ``target_years`` if given). ``metadata`` includes
772
- ``n_entities_dropped`` (entities skipped for insufficient baseline obs).
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, year_col, "anomaly_score", "is_anomaly")
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, keep loop but partition_by ----
987
+ # ---- CUSUM / STL: inherently sequential/per-entity, partition_by instead of filter-loop ----
971
988
  else:
972
989
  rows: list[dict[str, Any]] = []
973
- for sub in df.sort(year_col).partition_by(key, maintain_order=True):
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
- eval_vals = eval_sub[column].drop_nulls().to_numpy().astype(float)
996
- eval_years = eval_sub.filter(pl.col(column).is_not_null())[
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
- # Match residuals to years EXPLICITLY (not positional slicing) —
1038
- # positional slicing silently misaligns if there are gaps between
1039
- # baseline and eval rows.
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
- resid_by_year[y]
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(resid_by_year[y] / std)
1050
- if y in resid_by_year
1085
+ float(resid_by_time[t] / std)
1086
+ if t in resid_by_time
1051
1087
  else float("nan")
1052
- for y in eval_years
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(eval_years)
1064
- ts_flags = [False] * len(eval_years)
1099
+ ts_scores = [float("nan")] * len(eval_years_lookup)
1100
+ ts_flags = [False] * len(eval_years_lookup)
1065
1101
 
1066
- for yr, sc, fl in zip(eval_years, ts_scores, ts_flags):
1102
+ for t, sc, fl in zip(eval_times, ts_scores, ts_flags):
1067
1103
  rows.append(
1068
1104
  {
1069
1105
  key: eid,
1070
- year_col: yr,
1106
+ output_time_col: t,
1071
1107
  "anomaly_score": float(sc),
1072
1108
  "is_anomaly": bool(fl),
1073
1109
  }
1074
1110
  )
1075
1111
 
1076
- data = (
1077
- pl.DataFrame(rows)
1078
- if rows
1079
- else pl.DataFrame(
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
- year_col: pl.Series([], dtype=pl.Int32),
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