core-lens 0.1.dev142__tar.gz → 0.1.dev147__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.dev142 → core_lens-0.1.dev147}/PKG-INFO +1 -1
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/bench_aoi.py +1 -1
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/bench_export.py +5 -5
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/bench_result.py +3 -3
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/bench_season.py +6 -3
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/bench_spatial.py +1 -1
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/bench_view.py +9 -9
- core_lens-0.1.dev147/docs/source/concepts.md +7 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/quickstart.md +2 -1
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/stats.md +1 -1
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/aoi.py +24 -44
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/base/entity.py +95 -61
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/base/namespaces/plot.py +1 -1
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/base/namespaces/stats.py +12 -10
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/base/result.py +14 -19
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/base/view.py +43 -26
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/export/formats.py +1 -1
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/utils/polars_utils.py +24 -3
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/utils/season.py +4 -4
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_aoi.py +4 -2
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_entity.py +6 -6
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_plot.py +15 -15
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_result.py +3 -1
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_view.py +10 -10
- core_lens-0.1.dev142/docs/source/concepts.md +0 -7
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.gitignore +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.gitmessage +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/.python-version +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/LICENSE +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/README.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/SKILLS.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/README.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/bench_polars_utils.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/bench_schema.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/benchmarks/run_all.sh +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/Makefile +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/make.bat +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/export.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/logging.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/docs/source/queries.md +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/pyproject.toml +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/__init__.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/entities/mws.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/entities/tehsil.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/schema/detection.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/schema/profile.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/utils/paths.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/src/core_lens/utils/spatial.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev142 → core_lens-0.1.dev147}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-lens
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.dev147
|
|
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
|
|
@@ -116,7 +116,7 @@ view = aoi_bbox.mws
|
|
|
116
116
|
|
|
117
117
|
t1 = time.perf_counter()
|
|
118
118
|
print(f"First access : {(t1 - t0) * 1000:.2f} ms")
|
|
119
|
-
print(f"Keys shape : {view.keys.shape}")
|
|
119
|
+
print(f"Keys shape : {view.keys.collect().shape}")
|
|
120
120
|
|
|
121
121
|
# Second access — should hit _scoped cache (near-zero cost).
|
|
122
122
|
t0 = time.perf_counter()
|
|
@@ -45,8 +45,8 @@ aoi_small = AoI(DATA_ROOT, bbox=SMALL_BBOX)
|
|
|
45
45
|
result_all = aoi_all.mws.static # full dataset — geometry present
|
|
46
46
|
result_small = aoi_small.mws.static # small subset
|
|
47
47
|
|
|
48
|
-
print(f"All result : {result_all.
|
|
49
|
-
print(f"Small result : {result_small.
|
|
48
|
+
print(f"All result : {result_all.df().shape}")
|
|
49
|
+
print(f"Small result : {result_small.df().shape}")
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def _section(title: str) -> None:
|
|
@@ -81,9 +81,9 @@ with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
81
81
|
# Strip geometry (and nested bbox) to pass has_geometry=False and allow CSV export.
|
|
82
82
|
geom_col = result_small.entity.geometry_col
|
|
83
83
|
drop_cols = [geom_col]
|
|
84
|
-
if "bbox" in result_small.
|
|
84
|
+
if "bbox" in result_small.df().columns:
|
|
85
85
|
drop_cols.append("bbox")
|
|
86
|
-
df_no_geom = result_small.
|
|
86
|
+
df_no_geom = result_small.df().drop(drop_cols)
|
|
87
87
|
result_no_geom = result_small._replace(data=df_no_geom, has_geometry=False)
|
|
88
88
|
|
|
89
89
|
REPS_IO = 5
|
|
@@ -99,7 +99,7 @@ with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
# All dataset.
|
|
102
|
-
df_all_no_geom = result_all.
|
|
102
|
+
df_all_no_geom = result_all.df().drop(geom_col)
|
|
103
103
|
result_all_no_geom = result_all._replace(data=df_all_no_geom, has_geometry=False)
|
|
104
104
|
out_all = tmp / "bench_all.parquet"
|
|
105
105
|
t0 = time.perf_counter()
|
|
@@ -41,7 +41,7 @@ aoi_small = AoI(DATA_ROOT, bbox=SMALL_BBOX)
|
|
|
41
41
|
# Materialise once — all benchmarks below operate on this result.
|
|
42
42
|
result = aoi.mws.static
|
|
43
43
|
result_small = aoi_small.mws.static
|
|
44
|
-
print(f"Dataset: {result.
|
|
44
|
+
print(f"Dataset: {result.df().shape[0]} MWS, {result.df().shape[1]} columns")
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def _section(title: str) -> None:
|
|
@@ -151,7 +151,7 @@ agg_result = result_annual.aggregate(
|
|
|
151
151
|
)
|
|
152
152
|
t1 = time.perf_counter()
|
|
153
153
|
print(f"aggregate(no by) : {(t1 - t0) * 1000:.2f} ms")
|
|
154
|
-
print(f"Shape : {agg_result.
|
|
154
|
+
print(f"Shape : {agg_result.df().shape}")
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
# ── 8. Result.aggregate(by='year') ──────────────────────────────────────────────
|
|
@@ -165,7 +165,7 @@ agg_year = result_annual.aggregate(
|
|
|
165
165
|
)
|
|
166
166
|
t1 = time.perf_counter()
|
|
167
167
|
print(f"aggregate(year) : {(t1 - t0) * 1000:.2f} ms")
|
|
168
|
-
print(f"Shape : {agg_year.
|
|
168
|
+
print(f"Shape : {agg_year.df().shape}")
|
|
169
169
|
|
|
170
170
|
|
|
171
171
|
# ── Cleanup ───────────────────────────────────────────────────────────────
|
|
@@ -25,6 +25,7 @@ import time
|
|
|
25
25
|
from datetime import date, timedelta
|
|
26
26
|
|
|
27
27
|
import polars as pl
|
|
28
|
+
from typing import cast
|
|
28
29
|
|
|
29
30
|
from core_lens.aoi import SeasonConfig
|
|
30
31
|
from core_lens.utils.season import add_temporal_columns, resolve_time_filter
|
|
@@ -167,9 +168,9 @@ t0 = time.perf_counter()
|
|
|
167
168
|
result_df = add_temporal_columns(FORTNIGHTLY_DF, "date", cfg)
|
|
168
169
|
t1 = time.perf_counter()
|
|
169
170
|
print(f"add_temporal_columns: {(t1 - t0) * 1000:.2f} ms")
|
|
170
|
-
print(f"Output shape : {result_df.shape}")
|
|
171
|
+
print(f"Output shape : {cast(pl.DataFrame, result_df).shape}")
|
|
171
172
|
print(
|
|
172
|
-
f"New columns added : {[c for c in result_df.columns if c not in FORTNIGHTLY_DF.columns]}"
|
|
173
|
+
f"New columns added : {[c for c in cast(pl.DataFrame, result_df).columns if c not in FORTNIGHTLY_DF.columns]}"
|
|
173
174
|
)
|
|
174
175
|
|
|
175
176
|
# Second call — all columns already present, should be near-zero.
|
|
@@ -186,6 +187,8 @@ t0 = time.perf_counter()
|
|
|
186
187
|
filtered = result_df.filter(expr)
|
|
187
188
|
t1 = time.perf_counter()
|
|
188
189
|
print(f"filter(kharif 2022) : {(t1 - t0) * 1000:.2f} ms")
|
|
189
|
-
print(
|
|
190
|
+
print(
|
|
191
|
+
f"Rows matched : {len(cast(pl.DataFrame, filtered)):,} / {len(cast(pl.DataFrame, result_df)):,}"
|
|
192
|
+
)
|
|
190
193
|
|
|
191
194
|
print("\n✓ bench_season.py complete")
|
|
@@ -180,7 +180,7 @@ view = entity.spatial_filter(bbox=SMALL_BBOX)
|
|
|
180
180
|
|
|
181
181
|
t1 = time.perf_counter()
|
|
182
182
|
print(f"spatial_filter (bbox) : {(t1 - t0) * 1000:.2f} ms")
|
|
183
|
-
print(f"View keys : {view.keys.shape}")
|
|
183
|
+
print(f"View keys : {view.keys.collect().shape}")
|
|
184
184
|
|
|
185
185
|
t0 = time.perf_counter()
|
|
186
186
|
view2 = entity.spatial_filter(geometry=small_poly)
|
|
@@ -50,7 +50,7 @@ t0 = time.perf_counter()
|
|
|
50
50
|
view_all = aoi.mws
|
|
51
51
|
t1 = time.perf_counter()
|
|
52
52
|
print(f"aoi.mws : {(t1 - t0) * 1000:.2f} ms")
|
|
53
|
-
print(f"Keys : {view_all.keys.shape}")
|
|
53
|
+
print(f"Keys : {view_all.keys.collect().shape}")
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
# ── 2. View.where() — single filter ──────────────────────────────────────────
|
|
@@ -59,7 +59,7 @@ t0 = time.perf_counter()
|
|
|
59
59
|
view_bihar = view_all.where(state="Bihar")
|
|
60
60
|
t1 = time.perf_counter()
|
|
61
61
|
print(f"where(state) : {(t1 - t0) * 1000:.2f} ms")
|
|
62
|
-
print(f"Bihar keys : {view_bihar.keys.shape}")
|
|
62
|
+
print(f"Bihar keys : {view_bihar.keys.collect().shape}")
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
# ── 3. View.where() — composite AND filter ────────────────────────────────────
|
|
@@ -68,7 +68,7 @@ t0 = time.perf_counter()
|
|
|
68
68
|
view_banka = view_all.where(state="Bihar", district="Banka", tehsil="Banka")
|
|
69
69
|
t1 = time.perf_counter()
|
|
70
70
|
print(f"where(3 cols) : {(t1 - t0) * 1000:.2f} ms")
|
|
71
|
-
print(f"Banka keys : {view_banka.keys.shape}")
|
|
71
|
+
print(f"Banka keys : {view_banka.keys.collect().shape}")
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
# ── 4. View.spatial_filter() — further narrow ─────────────────────────────────
|
|
@@ -78,7 +78,7 @@ t0 = time.perf_counter()
|
|
|
78
78
|
view_spatial = view_all.spatial_filter(bbox=SMALL_BBOX)
|
|
79
79
|
t1 = time.perf_counter()
|
|
80
80
|
print(f"spatial_filter : {(t1 - t0) * 1000:.2f} ms")
|
|
81
|
-
print(f"Spatial keys : {view_spatial.keys.shape}")
|
|
81
|
+
print(f"Spatial keys : {view_spatial.keys.collect().shape}")
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
# ── 5. View.between() — date range (no I/O) ───────────────────────────────────
|
|
@@ -112,14 +112,14 @@ t0 = time.perf_counter()
|
|
|
112
112
|
result_static = aoi.mws.static
|
|
113
113
|
t1 = time.perf_counter()
|
|
114
114
|
print(f"static (all) : {(t1 - t0) * 1000:.2f} ms")
|
|
115
|
-
print(f"Shape : {result_static.
|
|
115
|
+
print(f"Shape : {result_static.df().shape}")
|
|
116
116
|
|
|
117
117
|
_section("8. aoi_small.mws.static [small-bbox materialisation]")
|
|
118
118
|
t0 = time.perf_counter()
|
|
119
119
|
result_small = aoi_small.mws.static
|
|
120
120
|
t1 = time.perf_counter()
|
|
121
121
|
print(f"static (small) : {(t1 - t0) * 1000:.2f} ms")
|
|
122
|
-
print(f"Shape : {result_small.
|
|
122
|
+
print(f"Shape : {result_small.df().shape}")
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
# ── 9. Chained pipeline: where → spatial_filter → static ─────────────────────
|
|
@@ -128,7 +128,7 @@ t0 = time.perf_counter()
|
|
|
128
128
|
result_chain = aoi.mws.spatial_filter(bbox=SMALL_BBOX).static
|
|
129
129
|
t1 = time.perf_counter()
|
|
130
130
|
print(f"Chain : {(t1 - t0) * 1000:.2f} ms")
|
|
131
|
-
print(f"Shape : {result_chain.
|
|
131
|
+
print(f"Shape : {result_chain.df().shape}")
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
# ── 10. annual materialisation ──────────────────────────────────────────────────
|
|
@@ -137,7 +137,7 @@ t0 = time.perf_counter()
|
|
|
137
137
|
result_annual = aoi_small.mws.between("2018-01-01", "2023-12-31").annual
|
|
138
138
|
t1 = time.perf_counter()
|
|
139
139
|
print(f"annual : {(t1 - t0) * 1000:.2f} ms")
|
|
140
|
-
print(f"Shape : {result_annual.
|
|
140
|
+
print(f"Shape : {result_annual.df().shape}")
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
# ── 11. fortnightly materialisation ───────────────────────────────────────────
|
|
@@ -146,7 +146,7 @@ t0 = time.perf_counter()
|
|
|
146
146
|
result_fn = aoi_small.mws.between(season=Season.KHARIF, year=2022).fortnightly
|
|
147
147
|
t1 = time.perf_counter()
|
|
148
148
|
print(f"fortnightly : {(t1 - t0) * 1000:.2f} ms")
|
|
149
|
-
print(f"Shape : {result_fn.
|
|
149
|
+
print(f"Shape : {result_fn.df().shape}")
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
# ── Cleanup ───────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Core Concepts
|
|
2
|
+
|
|
3
|
+
CoreLens is built on three main layers:
|
|
4
|
+
|
|
5
|
+
1. **AoI (Area of Interest)**: The primary entry point. It represents a spatial boundary and acts as a gateway to all registered entities that intersect that boundary.
|
|
6
|
+
2. **View**: A lazy, immutable query definition. Filtering by attributes (`where`), space (`spatial_filter`), or time (`between`) returns a new `View` without reading any Parquet data.
|
|
7
|
+
3. **Result**: The lazy computation graph. Accessing `.static`, `.annual`, or `.fortnightly` on a `View` resolves the query plan and returns a `Result` object wrapping a Polars `LazyFrame`. Data is not actually read until you materialise it by calling `.df()` or `.gdf()`. This delays execution and reduces memory footprint, allowing the Polars streaming engine (automatically routed to the GPU if RAPIDS `cudf-polars` is installed) to optimize the query globally. All statistical operations, aggregations, and plotting are done on `Result` objects.
|
|
@@ -43,8 +43,9 @@ aoi_specific = AoI(
|
|
|
43
43
|
# You can now access entities scoped to this AoI:
|
|
44
44
|
mws_view = aoi.mws
|
|
45
45
|
|
|
46
|
-
#
|
|
46
|
+
# Resolve the static data query
|
|
47
47
|
static_result = mws_view.static
|
|
48
|
+
# Materialise as a GeoDataFrame (or use .df() for a Polars DataFrame)
|
|
48
49
|
gdf = static_result.gdf()
|
|
49
50
|
|
|
50
51
|
# Plot the AoI and its microwatersheds on an interactive map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Statistical Analysis
|
|
2
2
|
|
|
3
|
-
The `result.stats` namespace provides powerful analytical tools for geospatial and timeseries data. All statistical methods return a new `Result` object with computed `data` and populated `metadata`.
|
|
3
|
+
The `result.stats` namespace provides powerful analytical tools for geospatial and timeseries data. All statistical methods return a new `Result` object with the computed lazy `data` and populated `metadata`.
|
|
4
4
|
|
|
5
5
|
## Descriptive Statistics
|
|
6
6
|
|
|
@@ -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.dev147'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev147')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -20,6 +20,7 @@ from loguru import logger
|
|
|
20
20
|
|
|
21
21
|
from core_lens.base.entity import BaseEntity, EntityValidationError
|
|
22
22
|
from core_lens.utils.paths import is_cloud_uri
|
|
23
|
+
from core_lens.utils.polars_utils import cached_read_schema
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
26
|
from core_lens.base.view import View
|
|
@@ -219,7 +220,7 @@ def _cached_resolve_boundary(
|
|
|
219
220
|
|
|
220
221
|
lf = pl.scan_parquet(static_path, storage_options=storage_options or None)
|
|
221
222
|
filter_expr = pl.lit(True)
|
|
222
|
-
schema_types =
|
|
223
|
+
schema_types = cached_read_schema(static_path, storage_options)
|
|
223
224
|
for col, val in entity_kwargs.items():
|
|
224
225
|
if col in schema.key_cols or col in schema.extra_static_cols:
|
|
225
226
|
is_list_col = isinstance(schema_types.get(col), pl.List)
|
|
@@ -383,19 +384,16 @@ class AoI:
|
|
|
383
384
|
# Entity instances are created lazily on first access via _get_entity().
|
|
384
385
|
self._entity_instances: dict[str, BaseEntity] = {}
|
|
385
386
|
|
|
386
|
-
# Initialise boundary
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
# to the normal spatial_filter path for every entity.
|
|
390
|
-
self._boundary_entity_name: str | None = None
|
|
391
|
-
self._boundary_keys: pl.DataFrame | None = None
|
|
387
|
+
# Initialise boundary kwargs and lazily computed geometry.
|
|
388
|
+
self._boundary_kwargs: dict[str, str | list[str]] | None = None
|
|
389
|
+
self._geometry: shapely.Geometry | None = None
|
|
392
390
|
|
|
393
391
|
if geometry is not None:
|
|
394
|
-
self.
|
|
392
|
+
self._geometry = geometry
|
|
395
393
|
elif bbox is not None:
|
|
396
|
-
self.
|
|
394
|
+
self._geometry = _bbox_to_polygon(bbox)
|
|
397
395
|
else:
|
|
398
|
-
self.
|
|
396
|
+
self._boundary_kwargs = entity_kwargs
|
|
399
397
|
|
|
400
398
|
# Entity views are created lazily on demand in __getattr__.
|
|
401
399
|
self._scoped: dict[str, "View"] = {}
|
|
@@ -406,6 +404,17 @@ class AoI:
|
|
|
406
404
|
if validate_all:
|
|
407
405
|
self.validate()
|
|
408
406
|
|
|
407
|
+
@property
|
|
408
|
+
def geometry(self) -> "shapely.Geometry":
|
|
409
|
+
"""The resolved boundary of this AoI as a Shapely geometry."""
|
|
410
|
+
if self._geometry is None:
|
|
411
|
+
if self._boundary_kwargs is not None:
|
|
412
|
+
self._geometry = self._resolve_named_boundary(self._boundary_kwargs)
|
|
413
|
+
else:
|
|
414
|
+
# Should not happen if initialization was correct
|
|
415
|
+
raise ValueError("No boundary defined.")
|
|
416
|
+
return self._geometry
|
|
417
|
+
|
|
409
418
|
@property
|
|
410
419
|
def current_season(self) -> str:
|
|
411
420
|
"""The season name for today's date under the AoI's SeasonConfig.
|
|
@@ -466,7 +475,7 @@ class AoI:
|
|
|
466
475
|
list(overlay.key_cols) if hasattr(overlay, "key_cols") else []
|
|
467
476
|
)
|
|
468
477
|
arrow_table = _wkb_to_arrow_table(
|
|
469
|
-
overlay.
|
|
478
|
+
overlay.df(), geom_col, extra_cols=key_cols
|
|
470
479
|
)
|
|
471
480
|
overlay_layer = lonboard.PolygonLayer(
|
|
472
481
|
arrow_table,
|
|
@@ -538,23 +547,12 @@ class AoI:
|
|
|
538
547
|
if name in _REGISTRY:
|
|
539
548
|
if name not in self._scoped:
|
|
540
549
|
entity = self._get_entity(name)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
# _resolve_named_boundary. Skip the bbox-index build
|
|
545
|
-
# and geometric predicate entirely.
|
|
546
|
-
from core_lens.base.view import View
|
|
547
|
-
|
|
548
|
-
assert (
|
|
549
|
-
self._boundary_keys is not None
|
|
550
|
-
) # set by _resolve_named_boundary
|
|
551
|
-
view = View(
|
|
552
|
-
keys=self._boundary_keys,
|
|
553
|
-
entity=entity,
|
|
554
|
-
entity_name=name,
|
|
555
|
-
)
|
|
550
|
+
|
|
551
|
+
if self._boundary_kwargs is not None:
|
|
552
|
+
view = entity.where(**self._boundary_kwargs)
|
|
556
553
|
else:
|
|
557
554
|
view = entity.spatial_filter(geometry=self.geometry)
|
|
555
|
+
|
|
558
556
|
view._season_config = self.seasons
|
|
559
557
|
self._scoped[name] = view
|
|
560
558
|
return self._scoped[name]
|
|
@@ -608,24 +606,6 @@ class AoI:
|
|
|
608
606
|
# Reconstruct the boundary geometry from cached WKB bytes (fast).
|
|
609
607
|
geometry: shapely.Geometry = shapely.from_wkb(geom_wkb)
|
|
610
608
|
|
|
611
|
-
# Reconstruct _boundary_keys DataFrame from cached key_rows.
|
|
612
|
-
# We need the key column names — get them from the boundary entity.
|
|
613
|
-
boundary_entity_cls = _REGISTRY.get(boundary_entity_name)
|
|
614
|
-
if boundary_entity_cls is not None:
|
|
615
|
-
key_cols = boundary_entity_cls().key_cols
|
|
616
|
-
self._boundary_entity_name = boundary_entity_name
|
|
617
|
-
self._boundary_keys = pl.DataFrame(
|
|
618
|
-
{col: [row[i] for row in key_rows] for i, col in enumerate(key_cols)}
|
|
619
|
-
)
|
|
620
|
-
else:
|
|
621
|
-
# Registry changed since cache was populated — defensive fallback.
|
|
622
|
-
logger.warning(
|
|
623
|
-
"Boundary cache returned entity name '{}' not found in current registry.",
|
|
624
|
-
boundary_entity_name,
|
|
625
|
-
)
|
|
626
|
-
self._boundary_entity_name = None
|
|
627
|
-
self._boundary_keys = None
|
|
628
|
-
|
|
629
609
|
return geometry
|
|
630
610
|
|
|
631
611
|
@classmethod
|
|
@@ -29,6 +29,7 @@ from core_lens.utils.spatial import (
|
|
|
29
29
|
build_bbox_index,
|
|
30
30
|
exact_spatial_filter,
|
|
31
31
|
)
|
|
32
|
+
from core_lens.utils.polars_utils import cached_read_schema
|
|
32
33
|
|
|
33
34
|
if TYPE_CHECKING:
|
|
34
35
|
import shapely
|
|
@@ -371,6 +372,20 @@ class BaseEntity(ABC):
|
|
|
371
372
|
)
|
|
372
373
|
return self._schema_profile
|
|
373
374
|
|
|
375
|
+
@property
|
|
376
|
+
def geometry_lazy(self) -> pl.LazyFrame:
|
|
377
|
+
"""A cached LazyFrame representing the geometry of this entity.
|
|
378
|
+
|
|
379
|
+
Contains only the key columns and the geometry column. Used to avoid
|
|
380
|
+
repeatedly scanning and selecting geometries in downstream operations.
|
|
381
|
+
"""
|
|
382
|
+
if not hasattr(self, "_geometry_lazy"):
|
|
383
|
+
self._geometry_lazy = pl.scan_parquet(
|
|
384
|
+
self._resolve(self.static_path),
|
|
385
|
+
storage_options=self._storage_options or None,
|
|
386
|
+
).select(self.key_cols + [self.schema_profile.geometry_col])
|
|
387
|
+
return self._geometry_lazy
|
|
388
|
+
|
|
374
389
|
@property
|
|
375
390
|
def _index(self) -> pl.DataFrame:
|
|
376
391
|
"""In-memory bounding-box index for spatial pre-filtering.
|
|
@@ -434,7 +449,7 @@ class BaseEntity(ABC):
|
|
|
434
449
|
)
|
|
435
450
|
|
|
436
451
|
static = self._resolve(self.static_path)
|
|
437
|
-
schema =
|
|
452
|
+
schema = cached_read_schema(static, self._storage_options)
|
|
438
453
|
|
|
439
454
|
attr_kwargs = {k: v for k, v in kwargs.items() if k in schema}
|
|
440
455
|
entity_kwargs = {k: v for k, v in kwargs.items() if k not in schema}
|
|
@@ -453,77 +468,96 @@ class BaseEntity(ABC):
|
|
|
453
468
|
f"Available columns: {sorted(schema)}."
|
|
454
469
|
)
|
|
455
470
|
|
|
456
|
-
# ---
|
|
471
|
+
# --- Combine attribute and spatial filters into a single lazy pipeline ---
|
|
472
|
+
lf = pl.scan_parquet(static)
|
|
473
|
+
|
|
457
474
|
if attr_kwargs:
|
|
458
475
|
filter_expr = pl.lit(True)
|
|
459
476
|
for col, val in attr_kwargs.items():
|
|
460
477
|
filter_expr = filter_expr & (pl.col(col) == val)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
.collect()
|
|
466
|
-
)
|
|
478
|
+
lf = lf.filter(filter_expr)
|
|
479
|
+
|
|
480
|
+
if not entity_kwargs:
|
|
481
|
+
keys = lf.select(self.key_cols).collect()
|
|
467
482
|
else:
|
|
468
|
-
|
|
469
|
-
keys = pl.scan_parquet(static).select(self.key_cols).collect()
|
|
470
|
-
|
|
471
|
-
# --- Spatial entity lookups -----------------------------------------
|
|
472
|
-
for entity_kwarg_name, entity_kwarg_val in entity_kwargs.items():
|
|
473
|
-
other_entity = _REGISTRY[entity_kwarg_name](data_root=self._data_root)
|
|
474
|
-
other_profile = other_entity.schema_profile
|
|
475
|
-
other_static = other_entity._resolve(other_entity.static_path)
|
|
483
|
+
import shapely
|
|
476
484
|
import shapely.wkb as swkb
|
|
485
|
+
import shapely.wkt as swkt
|
|
477
486
|
import shapely.ops as sops
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
for
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"BaseEntity.where failed: No rows matched {}={!r} in {}",
|
|
492
|
-
entity_kwarg_name,
|
|
493
|
-
entity_kwarg_val,
|
|
494
|
-
other_entity.static_path,
|
|
487
|
+
import numpy as np
|
|
488
|
+
from core_lens.utils.spatial import bbox_intersects_geometry
|
|
489
|
+
|
|
490
|
+
lookup_geoms = []
|
|
491
|
+
for entity_kwarg_name, entity_kwarg_val in entity_kwargs.items():
|
|
492
|
+
other_entity = _REGISTRY[entity_kwarg_name](data_root=self._data_root)
|
|
493
|
+
other_profile = other_entity.schema_profile
|
|
494
|
+
other_static = other_entity._resolve(other_entity.static_path)
|
|
495
|
+
|
|
496
|
+
other_lf = pl.scan_parquet(other_static)
|
|
497
|
+
match_expr = pl.lit(False)
|
|
498
|
+
other_schema = cached_read_schema(
|
|
499
|
+
other_static, other_entity._storage_options
|
|
495
500
|
)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
501
|
+
for col in other_entity.key_cols + list(
|
|
502
|
+
other_profile.extra_static_cols
|
|
503
|
+
):
|
|
504
|
+
if col in other_schema:
|
|
505
|
+
match_expr = match_expr | (pl.col(col) == entity_kwarg_val)
|
|
506
|
+
matched = (
|
|
507
|
+
other_lf.filter(match_expr)
|
|
508
|
+
.select([other_profile.geometry_col])
|
|
509
|
+
.collect()
|
|
499
510
|
)
|
|
500
511
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
512
|
+
if matched.is_empty():
|
|
513
|
+
logger.error(
|
|
514
|
+
"BaseEntity.where failed: No rows matched {}={!r} in {}",
|
|
515
|
+
entity_kwarg_name,
|
|
516
|
+
entity_kwarg_val,
|
|
517
|
+
other_entity.static_path,
|
|
518
|
+
)
|
|
519
|
+
raise ValueError(
|
|
520
|
+
f"BaseEntity.where: No rows matched {entity_kwarg_name}={entity_kwarg_val!r} "
|
|
521
|
+
f"in {other_entity.static_path!r}."
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
raw_geoms = matched[other_profile.geometry_col].to_list()
|
|
525
|
+
if other_profile.geometry_type == "wkb":
|
|
526
|
+
geoms = [swkb.loads(v) for v in raw_geoms]
|
|
527
|
+
else:
|
|
528
|
+
geoms = [swkt.loads(v) for v in raw_geoms]
|
|
529
|
+
lookup_geom = sops.unary_union(geoms) if len(geoms) > 1 else geoms[0]
|
|
530
|
+
lookup_geoms.append(lookup_geom)
|
|
531
|
+
|
|
532
|
+
candidates = self._index
|
|
533
|
+
for geom in lookup_geoms:
|
|
534
|
+
candidates = bbox_intersects_geometry(candidates, geom)
|
|
535
|
+
|
|
536
|
+
if candidates.is_empty():
|
|
537
|
+
keys = candidates.select(self.key_cols)
|
|
504
538
|
else:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
539
|
+
lf = lf.select(self.key_cols + [self.schema_profile.geometry_col]).join(
|
|
540
|
+
candidates.select(self.key_cols).lazy(),
|
|
541
|
+
on=self.key_cols,
|
|
542
|
+
how="semi",
|
|
543
|
+
)
|
|
544
|
+
df = lf.collect()
|
|
545
|
+
|
|
546
|
+
if df.is_empty():
|
|
547
|
+
keys = df.select(self.key_cols)
|
|
548
|
+
else:
|
|
549
|
+
geom_array = df[self.schema_profile.geometry_col].to_numpy()
|
|
550
|
+
if self.schema_profile.geometry_type == "wkb":
|
|
551
|
+
decoded_geoms = shapely.from_wkb(geom_array)
|
|
552
|
+
else:
|
|
553
|
+
decoded_geoms = shapely.from_wkt(geom_array)
|
|
554
|
+
|
|
555
|
+
centroids = shapely.centroid(decoded_geoms)
|
|
556
|
+
mask = np.ones(len(centroids), dtype=bool)
|
|
557
|
+
for geom in lookup_geoms:
|
|
558
|
+
mask = mask & shapely.contains(geom, centroids)
|
|
559
|
+
|
|
560
|
+
keys = df.filter(mask).select(self.key_cols)
|
|
527
561
|
|
|
528
562
|
entity_name = _entity_name(type(self))
|
|
529
563
|
return View(keys=keys, entity=self, entity_name=entity_name)
|
|
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
import polars as pl
|
|
12
12
|
|
|
13
|
-
from core_lens.utils.polars_utils import collect_lf
|
|
13
|
+
from core_lens.utils.polars_utils import collect_lf, cached_read_schema
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from core_lens.base.result import Result
|
|
@@ -164,7 +164,7 @@ class StatsNamespace:
|
|
|
164
164
|
return [c for c in df.columns if df[c].dtype.is_numeric()]
|
|
165
165
|
|
|
166
166
|
def _year_col(self) -> str | None:
|
|
167
|
-
if "year" in self._r.
|
|
167
|
+
if "year" in self._r.df().columns:
|
|
168
168
|
return "year"
|
|
169
169
|
profile = self._r.entity.schema_profile
|
|
170
170
|
return profile.annual_time_col
|
|
@@ -190,7 +190,7 @@ class StatsNamespace:
|
|
|
190
190
|
ValueError: If ``by`` is not ``\"column\"`` or ``\"entity\"``.
|
|
191
191
|
|
|
192
192
|
"""
|
|
193
|
-
df = self._r.
|
|
193
|
+
df = self._r.df()
|
|
194
194
|
cols = columns if columns is not None else self._numeric_cols(df)
|
|
195
195
|
|
|
196
196
|
if by == "column":
|
|
@@ -261,7 +261,7 @@ class StatsNamespace:
|
|
|
261
261
|
|
|
262
262
|
import scipy.stats as sp
|
|
263
263
|
|
|
264
|
-
df = self._r.
|
|
264
|
+
df = self._r.df()
|
|
265
265
|
n_obs = len(df)
|
|
266
266
|
rows: list[dict[str, Any]] = []
|
|
267
267
|
|
|
@@ -327,7 +327,7 @@ class StatsNamespace:
|
|
|
327
327
|
"""
|
|
328
328
|
import scipy.stats as sp
|
|
329
329
|
|
|
330
|
-
df = self._r.
|
|
330
|
+
df = self._r.df()
|
|
331
331
|
all_vals = df[column].drop_nulls().to_numpy().astype(float)
|
|
332
332
|
|
|
333
333
|
# auto-select
|
|
@@ -480,7 +480,7 @@ class StatsNamespace:
|
|
|
480
480
|
|
|
481
481
|
import scipy.stats as sp
|
|
482
482
|
|
|
483
|
-
df = self._r.
|
|
483
|
+
df = self._r.df()
|
|
484
484
|
key = self._r.key_cols[0]
|
|
485
485
|
year_col = self._year_col()
|
|
486
486
|
if year_col is None:
|
|
@@ -580,7 +580,7 @@ class StatsNamespace:
|
|
|
580
580
|
ValueError: If ``mode``, ``method``, or observation count invalid.
|
|
581
581
|
|
|
582
582
|
"""
|
|
583
|
-
df = self._r.
|
|
583
|
+
df = self._r.df()
|
|
584
584
|
key = self._r.key_cols[0]
|
|
585
585
|
|
|
586
586
|
if mode == "cross_sectional":
|
|
@@ -856,7 +856,7 @@ class StatsNamespace:
|
|
|
856
856
|
f"Valid options: {[e.name for e in SimilarityMethod]}."
|
|
857
857
|
)
|
|
858
858
|
|
|
859
|
-
df = self._r.
|
|
859
|
+
df = self._r.df()
|
|
860
860
|
key = self._r.key_cols[0]
|
|
861
861
|
|
|
862
862
|
# --- Build feature DataFrame ------------------------------------------
|
|
@@ -903,11 +903,13 @@ class StatsNamespace:
|
|
|
903
903
|
except FileNotFoundError:
|
|
904
904
|
continue
|
|
905
905
|
|
|
906
|
-
schema =
|
|
906
|
+
schema = cached_read_schema(abs_path, entity._storage_options)
|
|
907
907
|
if col_name not in schema:
|
|
908
908
|
continue # column doesn't exist in this file
|
|
909
909
|
|
|
910
|
-
col_lf = pl.scan_parquet(
|
|
910
|
+
col_lf = pl.scan_parquet(
|
|
911
|
+
abs_path, storage_options=entity._storage_options or None
|
|
912
|
+
)
|
|
911
913
|
|
|
912
914
|
# Apply year filter.
|
|
913
915
|
if "year" in filter_dict:
|