core-lens 0.1.dev180__tar.gz → 0.1.dev183__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.dev180 → core_lens-0.1.dev183}/PKG-INFO +1 -1
- core_lens-0.1.dev183/SKILLS.md +242 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/export.md +1 -6
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/plugins.md +2 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/queries.md +2 -7
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/quickstart.md +2 -6
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/stats.md +10 -18
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/schema/detection.py +19 -1
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/schema/profile.py +11 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/usage.md +12 -13
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/uv.lock +485 -392
- core_lens-0.1.dev180/SKILLS.md +0 -259
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.gitignore +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.gitmessage +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.python-version +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/LICENSE +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/README.md +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/README.md +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_aoi.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_entity.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_export.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_polars_utils.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_result.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_schema.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_season.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_spatial.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_view.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/run_all.sh +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/Makefile +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/make.bat +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/logging.md +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/pyproject.toml +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/__init__.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/aoi.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/entity.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/namespaces/plot.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/namespaces/stats.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/result.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/view.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/entities/mws.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/entities/tehsil.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/export/formats.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/paths.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/polars_utils.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/season.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/spatial.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_entity.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_plot.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_view.py +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.dev183
|
|
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
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: core-lens
|
|
3
|
+
description: >
|
|
4
|
+
A Python library for querying, analysing, and visualising geospatial Earth science data.
|
|
5
|
+
Provides a unified interface over microwatersheds (MWS), administrative boundaries, and pluggable domain entities.
|
|
6
|
+
Use this skill to understand how to interact with AoI (Area of Interest), Views, Results, and Statistical analysis.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Agent Instructions: Core-Lens Library
|
|
10
|
+
|
|
11
|
+
You are an AI Agent tasked with writing or debugging code using the `core-lens` library. `core-lens` is a high-performance Python library designed for querying, filtering, and manipulating large-scale geospatial datasets using deferred execution with **Polars**.
|
|
12
|
+
|
|
13
|
+
**Read these instructions carefully before writing any code.**
|
|
14
|
+
|
|
15
|
+
## 1. Core Architectural Constraints
|
|
16
|
+
|
|
17
|
+
- **Area of Interest (AoI) is the Primary Entry Point**: Never load parquet files directly. Always create an `AoI` object to define the geographical scope and directory context.
|
|
18
|
+
- **Lazy Evaluation**: `core-lens` is lazy by default. Accessing an entity like `aoi.mws` creates a `View`. No data is loaded into memory until you call a materialisation method (`.static`, `.annual`, or `.sub_annual`).
|
|
19
|
+
- **Polars Over Pandas**: `core-lens` uses Polars for zero-copy operations. Only convert to GeoPandas (`.gdf()`) when strictly necessary for heavy geometric processing.
|
|
20
|
+
- **Separation of Concerns**:
|
|
21
|
+
- `AoI`: Filters boundaries, resolves geography, scopes entities, and handles time constraints.
|
|
22
|
+
- `Result`: Produced by materializing a view. Handles all computation, spatial joins, aggregations, stats, and plotting.
|
|
23
|
+
- `Export`: Separate namespace (`from core_lens.export import geoparquet`).
|
|
24
|
+
|
|
25
|
+
## 1.5 Logging and Tracing
|
|
26
|
+
|
|
27
|
+
`core_lens` uses Loguru and is disabled by default. If you need to debug spatial resolution issues or trace validation errors, enable the internal logger:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import sys
|
|
31
|
+
from loguru import logger
|
|
32
|
+
|
|
33
|
+
logger.enable("core_lens")
|
|
34
|
+
logger.add(sys.stdout, level="DEBUG")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 2. Initialization and Setup
|
|
38
|
+
|
|
39
|
+
Before querying, you MUST register the specific entities you plan to use.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from core_lens import AoI, SeasonConfig
|
|
43
|
+
from core_lens.entities import MWSEntity, TehsilEntity, DistrictEntity
|
|
44
|
+
|
|
45
|
+
# 1. Register entities FIRST
|
|
46
|
+
AoI.register(MWSEntity)
|
|
47
|
+
AoI.register(DistrictEntity)
|
|
48
|
+
# ... register others as needed
|
|
49
|
+
|
|
50
|
+
# 2. Scoping the AoI (Choose ONE mode)
|
|
51
|
+
DATA_ROOT = "data/"
|
|
52
|
+
# Mode A: By Administrative Hierarchy
|
|
53
|
+
aoi = AoI(DATA_ROOT, tehsil="Pangi", district="Chamba")
|
|
54
|
+
# Mode B: By Bounding Box
|
|
55
|
+
aoi = AoI(DATA_ROOT, bbox=(76.0, 31.0, 78.0, 33.0))
|
|
56
|
+
# Mode C: By specific Entity ID
|
|
57
|
+
aoi = AoI(DATA_ROOT, mws_id="13_551")
|
|
58
|
+
|
|
59
|
+
# Optional: Override default seasons (kharif, rabi, zaid)
|
|
60
|
+
custom_seasons = SeasonConfig(kharif=("06-01", "10-15"), rabi=("10-16", "02-28"), zaid=("03-01", "05-31"))
|
|
61
|
+
aoi_custom = AoI(DATA_ROOT, tehsil="Pangi", seasons=custom_seasons)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 3. Querying & Filtering (Views)
|
|
65
|
+
|
|
66
|
+
Entities on the `AoI` object are accessed as **Views**. Views are lazy and can be chained.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Access the view
|
|
70
|
+
mws_view = aoi.mws
|
|
71
|
+
|
|
72
|
+
# Attribute Filtering
|
|
73
|
+
filtered = mws_view.where(state="Himachal Pradesh")
|
|
74
|
+
|
|
75
|
+
# Spatial Filtering (inside the AoI)
|
|
76
|
+
spatial_filtered = mws_view.spatial_filter(bbox=(76.5, 31.5, 77.5, 32.5))
|
|
77
|
+
|
|
78
|
+
# Temporal Filtering
|
|
79
|
+
# Note: You can filter by exact years, a range of years, or predefined seasons.
|
|
80
|
+
from core_lens.base.view import Season
|
|
81
|
+
temporal_view = mws_view.between(year=(2020, 2022), season=Season.KHARIF)
|
|
82
|
+
current_view = mws_view.between(season=Season.CURRENT)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 4. Materialization (Loading Data)
|
|
86
|
+
|
|
87
|
+
You must materialize the view into a `Result` to perform computations.
|
|
88
|
+
Choose the resolution based on what you need:
|
|
89
|
+
- `.static`: One row per entity, includes `geometry`.
|
|
90
|
+
- `.annual`: Time-series, aggregated annually. (NO geometry by default).
|
|
91
|
+
- `.sub_annual`: High-frequency time-series. (NO geometry by default).
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
static_result = filtered.static.materialise()
|
|
95
|
+
annual_result = temporal_view.annual.materialise()
|
|
96
|
+
```
|
|
97
|
+
*Note: Depending on library version, you may only need `.static` instead of `.static.materialise()`. If `.static` returns a Result directly, use that.*
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# To access underlying data structures:
|
|
101
|
+
df = static_result.df() # Polars DataFrame (Zero-copy)
|
|
102
|
+
lazy_df = static_result.lazy() # Polars LazyFrame
|
|
103
|
+
gdf = static_result.gdf() # GeoPandas GeoDataFrame (Heavy!)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 5. Computation and Data Manipulation (Result API)
|
|
107
|
+
|
|
108
|
+
All data manipulation happens on the `Result` object.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import polars as pl
|
|
112
|
+
|
|
113
|
+
# Derive new columns
|
|
114
|
+
res = annual_result.derive("water_shortage", pl.col("rainfall") - pl.col("storage"))
|
|
115
|
+
|
|
116
|
+
# Aggregation (Supported 'by' keys: "year", "month", "season", "season_year")
|
|
117
|
+
agg_res = res.aggregate(pl.mean("ndvi").alias("avg_ndvi"), by="year")
|
|
118
|
+
|
|
119
|
+
# Reattaching Geometry
|
|
120
|
+
# Time-series results drop geometry to save memory. Reattach it if you need to map them:
|
|
121
|
+
geo_result = agg_res.with_geometry()
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 6. Spatial Joins
|
|
125
|
+
|
|
126
|
+
Spatial joins are lazy and enrich a primary entity with aggregated data from a secondary entity.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# Calculate the total forest tree_cover inside each MWS
|
|
130
|
+
joined_view = aoi.mws.spatial_join(
|
|
131
|
+
aoi.forest, agg={"tree_cover": "sum", "canopy_density": "mean"}
|
|
132
|
+
)
|
|
133
|
+
# Resulting columns are prefixed, e.g., 'forest_tree_cover', 'forest_canopy_density'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 7. Statistical Analysis (`Result.stats`)
|
|
137
|
+
|
|
138
|
+
All stats methods are available under `result.stats` and return a NEW `Result`.
|
|
139
|
+
- Computed data is in `result.data` (which is a Polars DataFrame)
|
|
140
|
+
- Parameters and testing metrics are stored in `result.metadata`
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# Descriptive stats
|
|
144
|
+
result.stats.describe(columns=["ndvi", "rainfall"])
|
|
145
|
+
|
|
146
|
+
# Correlation (pearson, spearman, kendall)
|
|
147
|
+
from core_lens.base.namespaces.stats import CorrelateMethod
|
|
148
|
+
result.stats.correlate(columns=["ndvi", "rainfall"], method=CorrelateMethod.PEARSON, across="entity")
|
|
149
|
+
|
|
150
|
+
# Hypothesis Testing (t-test, mann-whitney, wilcoxon, ks, chi-square)
|
|
151
|
+
from core_lens.base.namespaces.stats import TestMethod
|
|
152
|
+
result.stats.test(column="cropping_intensity", groups="temperature_zone", method=TestMethod.MANN_WHITNEY)
|
|
153
|
+
|
|
154
|
+
# Change Detection (absolute, percentage, trend)
|
|
155
|
+
from core_lens.base.namespaces.stats import ChangeMethod
|
|
156
|
+
result.stats.change(column="tree_cover", from_period=2018, to_period=2023, method=ChangeMethod.PERCENTAGE)
|
|
157
|
+
|
|
158
|
+
# Anomaly Detection
|
|
159
|
+
from core_lens.base.namespaces.stats import AnomalyTsMethod, AnomalyCrossMethod
|
|
160
|
+
# Mode 1: Cross-sectional (zscore, iqr, percentile, threshold)
|
|
161
|
+
result.stats.anomaly(column="ndvi", mode="cross_sectional", method=AnomalyCrossMethod.ZSCORE, baseline=(2010, 2020))
|
|
162
|
+
# Mode 2: Time-series (stl, cusum, mad)
|
|
163
|
+
result.stats.anomaly(column="ndvi", mode="timeseries", method=AnomalyTsMethod.STL, baseline=(2010, 2018))
|
|
164
|
+
|
|
165
|
+
# Similarity Matching (euclidean, cosine, mahalanobis, manhattan)
|
|
166
|
+
from core_lens.base.namespaces.stats import SimilarityMethod
|
|
167
|
+
result.stats.similarity(
|
|
168
|
+
target="13_551",
|
|
169
|
+
columns={"rainfall": ("annual", {"year": 2018})},
|
|
170
|
+
method=SimilarityMethod.EUCLIDEAN,
|
|
171
|
+
top_n=10
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 8. Plotting and Visualization (`Result.plot`)
|
|
176
|
+
|
|
177
|
+
Plots are explicitly accessed via the `result.plot` namespace. Maps use Lonboard (requires geometry); charts use Plotly. By default, charts render the top 10 entities.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# Map plotting (Requires result.with_geometry() to have been called for time-series)
|
|
181
|
+
result.plot.choropleth("ndvi", subplot_on="season_year")
|
|
182
|
+
|
|
183
|
+
# Charts
|
|
184
|
+
result.plot.timeseries(x="year", y="ndvi", top_n=10)
|
|
185
|
+
result.plot.scatter(x="rainfall", y="ndvi", top_n=10)
|
|
186
|
+
result.plot.distribution(x="ndvi", top_n=10)
|
|
187
|
+
result.plot.correlation(columns=["ndvi", "rainfall"], top_n=10)
|
|
188
|
+
result.plot.heatmap(x="year", y="month", value="ndvi", top_n=10)
|
|
189
|
+
result.plot.matrix()
|
|
190
|
+
|
|
191
|
+
# Plotting the AoI boundary itself
|
|
192
|
+
aoi.plot()
|
|
193
|
+
aoi.plot(overlay=result)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## 9. Exporting Results
|
|
197
|
+
|
|
198
|
+
Exporting uses standalone functions. Do not export massive datasets (like country-wide `mws`) to GeoJSON as it will bloat the file size. Use GeoParquet instead.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from core_lens.export import geoparquet, parquet, geojson
|
|
202
|
+
|
|
203
|
+
# For results with geometry
|
|
204
|
+
geoparquet(result, "output.geoparquet")
|
|
205
|
+
|
|
206
|
+
# For tabular results without geometry
|
|
207
|
+
parquet(result, "output.parquet")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 10. Plugin System (Custom Entities)
|
|
211
|
+
|
|
212
|
+
You can extend core-lens with custom entities by subclassing `BaseEntity`.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from core_lens.base import BaseEntity
|
|
216
|
+
|
|
217
|
+
class CustomEntity(BaseEntity):
|
|
218
|
+
@property
|
|
219
|
+
def key_cols(self) -> list[str]:
|
|
220
|
+
return ["custom_id"]
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def geometry_col(self) -> str:
|
|
224
|
+
return "geometry"
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def static_path(self) -> str:
|
|
228
|
+
return "custom/static.geoparquet"
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def annual_path(self) -> str | None:
|
|
232
|
+
return "custom/annual.parquet"
|
|
233
|
+
|
|
234
|
+
AoI.register(CustomEntity)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Agent Reminders & Pitfalls to Avoid
|
|
238
|
+
- **DO NOT** attempt to read parquet files directly using `pandas.read_parquet` or `polars.read_parquet`. Always construct an `AoI` and use the library's materialized `Result` output.
|
|
239
|
+
- **DO NOT** assume `annual` or `sub_annual` results contain geometry. You must call `.with_geometry()` to reattach it.
|
|
240
|
+
- **DO NOT** forget to import and register entities before initializing the `AoI`.
|
|
241
|
+
- **DO** use Polars expressions (`import polars as pl`) inside `.derive()` and `.aggregate()` methods for maximum performance.
|
|
242
|
+
- **DO** always read parameters/testing metrics from `result.metadata` and underlying stats from `result.data` after running any methods in `result.stats`.
|
|
@@ -32,12 +32,7 @@ from core_lens.export import geoparquet, geojson
|
|
|
32
32
|
spatial_res = aoi.mws.annual.with_geometry()
|
|
33
33
|
|
|
34
34
|
# Export to GeoParquet with DuckDB COPY options (e.g., partitioning)
|
|
35
|
-
geoparquet(
|
|
36
|
-
spatial_res,
|
|
37
|
-
"output.geoparquet",
|
|
38
|
-
compression="ZSTD",
|
|
39
|
-
partition_by="year"
|
|
40
|
-
)
|
|
35
|
+
geoparquet(spatial_res, "output.geoparquet", compression="ZSTD", partition_by="year")
|
|
41
36
|
|
|
42
37
|
# Export to GeoJSON
|
|
43
38
|
geojson(spatial_res, "output.json")
|
|
@@ -5,6 +5,7 @@ You can extend CoreLens by defining your own entities. Simply subclass `BaseEnti
|
|
|
5
5
|
```python
|
|
6
6
|
from core_lens.base import BaseEntity
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
class CustomEntity(BaseEntity):
|
|
9
10
|
@property
|
|
10
11
|
def key_cols(self) -> list[str]:
|
|
@@ -22,6 +23,7 @@ class CustomEntity(BaseEntity):
|
|
|
22
23
|
def annual_path(self) -> str | None:
|
|
23
24
|
return "custom/annual.parquet"
|
|
24
25
|
|
|
26
|
+
|
|
25
27
|
# Register before use
|
|
26
28
|
AoI.register(CustomEntity)
|
|
27
29
|
```
|
|
@@ -27,8 +27,7 @@ import polars as pl
|
|
|
27
27
|
# 1. Derive new columns (works on any Result)
|
|
28
28
|
res_annual = aoi.mws.annual
|
|
29
29
|
derived_res = res_annual.derive(
|
|
30
|
-
"drought_flag",
|
|
31
|
-
pl.when(pl.col("rainfall") < 500).then(1).otherwise(0)
|
|
30
|
+
"drought_flag", pl.when(pl.col("rainfall") < 500).then(1).otherwise(0)
|
|
32
31
|
)
|
|
33
32
|
|
|
34
33
|
# 2. Aggregate (temporal grouping like "month" requires sub-annual, but "year" works on annual too)
|
|
@@ -47,11 +46,7 @@ Spatially join data from two different entities (e.g. Microwatersheds and Forest
|
|
|
47
46
|
|
|
48
47
|
# Join forest metrics onto MWS geometries
|
|
49
48
|
joined_view = aoi.mws.spatial_join(
|
|
50
|
-
aoi.forest,
|
|
51
|
-
agg={
|
|
52
|
-
"tree_cover": "area",
|
|
53
|
-
"canopy_density": "mean"
|
|
54
|
-
}
|
|
49
|
+
aoi.forest, agg={"tree_cover": "area", "canopy_density": "mean"}
|
|
55
50
|
)
|
|
56
51
|
|
|
57
52
|
# Materialise the joined view
|
|
@@ -29,15 +29,11 @@ Create an `AoI` (Area of Interest) by specifying the `data_root` (which can be a
|
|
|
29
29
|
|
|
30
30
|
```python
|
|
31
31
|
# Create an AoI scoped to a specific Tehsil using a local path
|
|
32
|
-
aoi = AoI(
|
|
33
|
-
data_root="/path/to/data",
|
|
34
|
-
tehsil="Pangi"
|
|
35
|
-
)
|
|
32
|
+
aoi = AoI(data_root="/path/to/data", tehsil="Pangi")
|
|
36
33
|
|
|
37
34
|
# Or scope it directly to a specific list of entity IDs using an S3 bucket
|
|
38
35
|
aoi_specific = AoI(
|
|
39
|
-
data_root="s3://my-cloud-bucket/core-lens-data",
|
|
40
|
-
mws_id=["13_001", "13_002"]
|
|
36
|
+
data_root="s3://my-cloud-bucket/core-lens-data", mws_id=["13_001", "13_002"]
|
|
41
37
|
)
|
|
42
38
|
|
|
43
39
|
# You can now access entities scoped to this AoI:
|
|
@@ -21,8 +21,8 @@ from core_lens.base.namespaces.stats import CorrelateMethod
|
|
|
21
21
|
|
|
22
22
|
corr = res.stats.correlate(
|
|
23
23
|
columns=["ndvi", "rainfall", "temperature"],
|
|
24
|
-
method=CorrelateMethod.PEARSON,
|
|
25
|
-
across="entity"
|
|
24
|
+
method=CorrelateMethod.PEARSON, # or SPEARMAN, KENDALL
|
|
25
|
+
across="entity", # correlate across entities or time
|
|
26
26
|
)
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -37,14 +37,12 @@ from core_lens.base.namespaces.stats import TestMethod
|
|
|
37
37
|
test_res = res.stats.test(
|
|
38
38
|
column="cropping_intensity",
|
|
39
39
|
groups="temperature_zone",
|
|
40
|
-
method=TestMethod.MANN_WHITNEY
|
|
40
|
+
method=TestMethod.MANN_WHITNEY,
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
# Period-based testing
|
|
44
44
|
test_period = res.stats.test(
|
|
45
|
-
column="ndvi",
|
|
46
|
-
periods=[(2010, 2015), (2016, 2023)],
|
|
47
|
-
method=TestMethod.T_TEST
|
|
45
|
+
column="ndvi", periods=[(2010, 2015), (2016, 2023)], method=TestMethod.T_TEST
|
|
48
46
|
)
|
|
49
47
|
```
|
|
50
48
|
|
|
@@ -57,10 +55,7 @@ from core_lens.base.namespaces.stats import ChangeMethod
|
|
|
57
55
|
|
|
58
56
|
# Trend over time
|
|
59
57
|
trend = res.stats.change(
|
|
60
|
-
column="ndvi",
|
|
61
|
-
from_period=2010,
|
|
62
|
-
to_period=2023,
|
|
63
|
-
method=ChangeMethod.TREND
|
|
58
|
+
column="ndvi", from_period=2010, to_period=2023, method=ChangeMethod.TREND
|
|
64
59
|
)
|
|
65
60
|
|
|
66
61
|
# Absolute or percentage change
|
|
@@ -68,7 +63,7 @@ pct_change = res.stats.change(
|
|
|
68
63
|
column="tree_cover",
|
|
69
64
|
from_period=2018,
|
|
70
65
|
to_period=2023,
|
|
71
|
-
method=ChangeMethod.PERCENTAGE
|
|
66
|
+
method=ChangeMethod.PERCENTAGE,
|
|
72
67
|
)
|
|
73
68
|
```
|
|
74
69
|
|
|
@@ -81,10 +76,7 @@ from core_lens.base.namespaces.stats import AnomalyTsMethod, AnomalyCrossMethod
|
|
|
81
76
|
|
|
82
77
|
# Timeseries anomaly against its own history
|
|
83
78
|
ts_anomalies = res.stats.anomaly(
|
|
84
|
-
column="ndvi",
|
|
85
|
-
mode="timeseries",
|
|
86
|
-
method=AnomalyTsMethod.STL,
|
|
87
|
-
baseline=(2010, 2018)
|
|
79
|
+
column="ndvi", mode="timeseries", method=AnomalyTsMethod.STL, baseline=(2010, 2018)
|
|
88
80
|
)
|
|
89
81
|
|
|
90
82
|
# Cross-sectional anomaly against other entities
|
|
@@ -92,7 +84,7 @@ cross_anomalies = res.stats.anomaly(
|
|
|
92
84
|
column="ndvi",
|
|
93
85
|
mode="cross_sectional",
|
|
94
86
|
method=AnomalyCrossMethod.ZSCORE,
|
|
95
|
-
baseline=(2010, 2020)
|
|
87
|
+
baseline=(2010, 2020),
|
|
96
88
|
)
|
|
97
89
|
```
|
|
98
90
|
|
|
@@ -107,9 +99,9 @@ similar = res.stats.similarity(
|
|
|
107
99
|
target="13_551",
|
|
108
100
|
columns={
|
|
109
101
|
"rainfall": ("annual", {"year": 2018}),
|
|
110
|
-
"ndvi": ("sub_annual", {"season": "kharif", "year": 2020})
|
|
102
|
+
"ndvi": ("sub_annual", {"season": "kharif", "year": 2020}),
|
|
111
103
|
},
|
|
112
104
|
method=SimilarityMethod.EUCLIDEAN,
|
|
113
|
-
top_n=10
|
|
105
|
+
top_n=10,
|
|
114
106
|
)
|
|
115
107
|
```
|
|
@@ -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.dev183'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev183')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -102,10 +102,12 @@ def detect(
|
|
|
102
102
|
if lon_col:
|
|
103
103
|
reserved_static.add(lon_col)
|
|
104
104
|
extra_static_cols = [c for c in static_schema if c not in reserved_static]
|
|
105
|
+
col_types_static = {col: str(dtype) for col, dtype in static_schema.items()}
|
|
105
106
|
|
|
106
107
|
annual_time_col: str | None = None
|
|
107
108
|
extra_annual_cols: list[str] = []
|
|
108
109
|
annual_is_year_col: bool | None = None
|
|
110
|
+
col_types_annual: dict[str, str] = {}
|
|
109
111
|
if annual_path:
|
|
110
112
|
annual_schema = _read_schema(
|
|
111
113
|
annual_path, label="annual", storage_options=_so or None
|
|
@@ -116,10 +118,12 @@ def detect(
|
|
|
116
118
|
)
|
|
117
119
|
extra_annual_cols = [c for c in annual_schema if c not in reserved_annual]
|
|
118
120
|
annual_is_year_col = _is_year_col_from_schema(annual_schema, annual_time_col)
|
|
121
|
+
col_types_annual = {col: str(dtype) for col, dtype in annual_schema.items()}
|
|
119
122
|
|
|
120
123
|
sub_annual_time_col: str | None = None
|
|
121
124
|
extra_sub_annual_cols: list[str] = []
|
|
122
125
|
sub_annual_is_year_col: bool | None = None
|
|
126
|
+
col_types_sub_annual: dict[str, str] = {}
|
|
123
127
|
if sub_annual_path:
|
|
124
128
|
sub_annual_schema = _read_schema(
|
|
125
129
|
sub_annual_path, label="sub_annual", storage_options=_so or None
|
|
@@ -132,6 +136,9 @@ def detect(
|
|
|
132
136
|
sub_annual_is_year_col = _is_year_col_from_schema(
|
|
133
137
|
sub_annual_schema, sub_annual_time_col
|
|
134
138
|
)
|
|
139
|
+
col_types_sub_annual = {
|
|
140
|
+
col: str(dtype) for col, dtype in sub_annual_schema.items()
|
|
141
|
+
}
|
|
135
142
|
|
|
136
143
|
return SchemaProfile(
|
|
137
144
|
key_cols=key_cols,
|
|
@@ -143,6 +150,9 @@ def detect(
|
|
|
143
150
|
extra_static_cols=extra_static_cols,
|
|
144
151
|
extra_annual_cols=extra_annual_cols,
|
|
145
152
|
extra_sub_annual_cols=extra_sub_annual_cols,
|
|
153
|
+
col_types_static=col_types_static,
|
|
154
|
+
col_types_annual=col_types_annual,
|
|
155
|
+
col_types_sub_annual=col_types_sub_annual,
|
|
146
156
|
annual_is_year_col=annual_is_year_col,
|
|
147
157
|
sub_annual_is_year_col=sub_annual_is_year_col,
|
|
148
158
|
)
|
|
@@ -153,10 +163,18 @@ def _read_schema(
|
|
|
153
163
|
label: str,
|
|
154
164
|
storage_options: dict[str, Any] | None = None,
|
|
155
165
|
) -> pl.Schema:
|
|
166
|
+
import os
|
|
167
|
+
|
|
156
168
|
_so = storage_options or {}
|
|
169
|
+
# When path is a directory, use a glob to read only .parquet files.
|
|
170
|
+
# This prevents polars from raising InvalidOperationError when the directory
|
|
171
|
+
# contains mixed-extension files (e.g. data_dictionary.csv alongside .parquet).
|
|
172
|
+
scan_path = path
|
|
173
|
+
if os.path.isdir(path):
|
|
174
|
+
scan_path = os.path.join(path, "**", "*.parquet")
|
|
157
175
|
try:
|
|
158
176
|
return pl.scan_parquet(
|
|
159
|
-
|
|
177
|
+
scan_path,
|
|
160
178
|
hive_partitioning=True,
|
|
161
179
|
storage_options=_so or None,
|
|
162
180
|
).collect_schema()
|
|
@@ -56,6 +56,12 @@ class SchemaProfile(BaseModel):
|
|
|
56
56
|
is used for validation and documentation only.
|
|
57
57
|
extra_annual_cols: Additional attribute columns in the annual file.
|
|
58
58
|
extra_sub_annual_cols: Additional attribute columns in the sub_annual file.
|
|
59
|
+
col_types_static: Mapping of **all** column names in the static file to
|
|
60
|
+
their Polars dtype string (e.g. ``{"basin_id": "Int32", "geometry": "Binary"}``).
|
|
61
|
+
Populated by the auto-detection layer; ``{}`` when the profile is
|
|
62
|
+
constructed manually.
|
|
63
|
+
col_types_annual: Same as ``col_types_static`` but for the annual file.
|
|
64
|
+
col_types_sub_annual: Same as ``col_types_static`` but for the sub_annual file.
|
|
59
65
|
|
|
60
66
|
"""
|
|
61
67
|
|
|
@@ -70,6 +76,11 @@ class SchemaProfile(BaseModel):
|
|
|
70
76
|
extra_static_cols: list[str] = []
|
|
71
77
|
extra_annual_cols: list[str] = []
|
|
72
78
|
extra_sub_annual_cols: list[str] = []
|
|
79
|
+
# Column name → Polars dtype string for every column in each resolution.
|
|
80
|
+
# Populated by the auto-detection layer; empty dict when built manually.
|
|
81
|
+
col_types_static: dict[str, str] = {}
|
|
82
|
+
col_types_annual: dict[str, str] = {}
|
|
83
|
+
col_types_sub_annual: dict[str, str] = {}
|
|
73
84
|
# Whether the time column is an integer-year column (True), a Date/Datetime
|
|
74
85
|
# column (False), or absent/unknown (None). Determined at detect() time from
|
|
75
86
|
# the Parquet schema — eliminates a redundant collect_schema() call in
|
|
@@ -89,8 +89,8 @@ spatial_view = mws_view.spatial_filter(bbox=(76.5, 31.5, 77.5, 32.5))
|
|
|
89
89
|
from core_lens.base.view import Season
|
|
90
90
|
|
|
91
91
|
temporal_view = mws_view.between(
|
|
92
|
-
year=(2020, 2022),
|
|
93
|
-
season=Season.KHARIF
|
|
92
|
+
year=(2020, 2022), # Inclusive year range
|
|
93
|
+
season=Season.KHARIF, # Filter to specific season
|
|
94
94
|
)
|
|
95
95
|
```
|
|
96
96
|
|
|
@@ -101,8 +101,7 @@ You can define a spatial join to enrich one entity with aggregated data from ano
|
|
|
101
101
|
```python
|
|
102
102
|
# Join MWS with Tehsil entity to calculate the sum of tehsil areas per MWS
|
|
103
103
|
joined_view = mws_view.spatial_join(
|
|
104
|
-
other=TehsilEntity(data_root=DATA_ROOT),
|
|
105
|
-
agg={"Shape_Area": "sum"}
|
|
104
|
+
other=TehsilEntity(data_root=DATA_ROOT), agg={"Shape_Area": "sum"}
|
|
106
105
|
)
|
|
107
106
|
```
|
|
108
107
|
|
|
@@ -127,22 +126,24 @@ A `Result` object provides access to the materialized data. `core-lens` uses zer
|
|
|
127
126
|
|
|
128
127
|
```python
|
|
129
128
|
# 1. Zero-copy access (Instantaneous)
|
|
130
|
-
pl_df = result_static.df()
|
|
131
|
-
pl_lazy = result_static.lazy()
|
|
129
|
+
pl_df = result_static.df() # Polars DataFrame
|
|
130
|
+
pl_lazy = result_static.lazy() # Polars LazyFrame
|
|
132
131
|
|
|
133
132
|
# 2. Decode WKB geometries to Shapely (CPU intensive)
|
|
134
|
-
gdf = result_static.gdf()
|
|
133
|
+
gdf = result_static.gdf() # GeoPandas GeoDataFrame
|
|
135
134
|
|
|
136
135
|
# 3. Data Transformations (Returns a new Result)
|
|
137
136
|
# Derive new columns using Polars expressions
|
|
138
137
|
import polars as pl
|
|
139
|
-
|
|
138
|
+
|
|
139
|
+
result_derived = result_annual.derive(
|
|
140
|
+
"yield_per_ha", pl.col("yield") / pl.col("area_in_ha")
|
|
141
|
+
)
|
|
140
142
|
|
|
141
143
|
# 4. Fast aggregations (Returns a new Result)
|
|
142
144
|
# Aggregate by a key column using Polars expressions
|
|
143
145
|
result_agg = result_derived.aggregate(
|
|
144
|
-
pl.col("yield_per_ha").mean().alias("avg_yield_per_ha"),
|
|
145
|
-
by="year"
|
|
146
|
+
pl.col("yield_per_ha").mean().alias("avg_yield_per_ha"), by="year"
|
|
146
147
|
)
|
|
147
148
|
```
|
|
148
149
|
|
|
@@ -173,9 +174,7 @@ By default, `core-lens` uses Indian agricultural seasons. You can inject a custo
|
|
|
173
174
|
from core_lens.aoi import SeasonConfig
|
|
174
175
|
|
|
175
176
|
custom_seasons = SeasonConfig(
|
|
176
|
-
kharif=("06-01", "10-15"),
|
|
177
|
-
rabi=("10-16", "02-28"),
|
|
178
|
-
zaid=("03-01", "05-31")
|
|
177
|
+
kharif=("06-01", "10-15"), rabi=("10-16", "02-28"), zaid=("03-01", "05-31")
|
|
179
178
|
)
|
|
180
179
|
|
|
181
180
|
aoi = AoI(DATA_ROOT, bbox=(...), seasons=custom_seasons)
|