core-lens 0.1.dev164__tar.gz → 0.1.dev166__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.dev164 → core_lens-0.1.dev166}/CONTRIBUTING.md +2 -2
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/PKG-INFO +1 -1
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/SKILLS.md +7 -7
- core_lens-0.1.dev166/analyze_summaries.py +90 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/README.md +1 -1
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_result.py +2 -2
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_schema.py +10 -10
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_season.py +13 -13
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_view.py +6 -6
- core_lens-0.1.dev166/district_report.py +127 -0
- core_lens-0.1.dev166/district_reports/Sehore_report.json +193 -0
- core_lens-0.1.dev166/district_summaries/Sehore/annual_summary.csv +38 -0
- core_lens-0.1.dev166/district_summaries/Sehore/static_summary.csv +64 -0
- core_lens-0.1.dev166/district_summaries/Sehore/sub-annual_summary.csv +21 -0
- core_lens-0.1.dev166/docs/source/concepts.md +7 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/logging.md +1 -1
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/queries.md +7 -7
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/stats.md +1 -1
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/examples/demo_mws.py +12 -12
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/examples/demo_tehsil.py +2 -2
- core_lens-0.1.dev166/generate_summaries.py +69 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/aoi.py +3 -3
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/entity.py +13 -13
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/namespaces/plot.py +30 -1
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/namespaces/stats.py +30 -7
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/result.py +10 -10
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/view.py +15 -15
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/entities/mws.py +4 -4
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/entities/tehsil.py +1 -1
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/schema/detection.py +18 -18
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/schema/profile.py +8 -8
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/season.py +2 -2
- core_lens-0.1.dev166/test_districts.txt +1 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/fixtures/generate_fixtures.py +7 -7
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/conftest.py +16 -16
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_aoi.py +3 -3
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_entities.py +2 -2
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_entity.py +2 -2
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_plot.py +1 -1
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_profile.py +1 -1
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_result.py +8 -8
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_schema_detection.py +7 -7
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_schema_profile.py +7 -7
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_spatial.py +3 -3
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_stats.py +6 -6
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_view.py +31 -61
- core_lens-0.1.dev164/docs/source/concepts.md +0 -7
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.gitignore +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.gitmessage +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.python-version +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/LICENSE +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/README.md +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_aoi.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_export.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_polars_utils.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_spatial.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/run_all.sh +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/Makefile +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/make.bat +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/export.md +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/pyproject.toml +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/__init__.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/export/formats.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/paths.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/polars_utils.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/spatial.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev164 → core_lens-0.1.dev166}/uv.lock +0 -0
|
@@ -143,7 +143,7 @@ Examples:
|
|
|
143
143
|
```text
|
|
144
144
|
feature/stats-similarity
|
|
145
145
|
feature/plugin-validation
|
|
146
|
-
fix/version-clash-
|
|
146
|
+
fix/version-clash-sub-annual
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
Workflow:
|
|
@@ -204,7 +204,7 @@ Examples:
|
|
|
204
204
|
|
|
205
205
|
```text
|
|
206
206
|
feat: add stats.similarity()
|
|
207
|
-
fix: resolve VersionClashError on
|
|
207
|
+
fix: resolve VersionClashError on sub-annual
|
|
208
208
|
docs: add plugin guide
|
|
209
209
|
test: add aggregate validation fixtures
|
|
210
210
|
chore: update ruff
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-lens
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.dev166
|
|
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
|
|
@@ -47,7 +47,7 @@ view.between(season="kharif", year=2020)
|
|
|
47
47
|
# Materialise into a Result object
|
|
48
48
|
static_result = view.static # has geometry
|
|
49
49
|
annual_result = view.annual # no geometry
|
|
50
|
-
|
|
50
|
+
sub_annual_result = view.sub_annual # no geometry
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
## 3. Operations on Results (Compute & Aggregation)
|
|
@@ -129,7 +129,7 @@ result.stats.similarity(
|
|
|
129
129
|
target="13_551",
|
|
130
130
|
columns={
|
|
131
131
|
"rainfall": ("annual", {"year": 2018}),
|
|
132
|
-
"ndvi": ("
|
|
132
|
+
"ndvi": ("sub_annual", {"season": "kharif", "year": 2020})
|
|
133
133
|
},
|
|
134
134
|
method="euclidean",
|
|
135
135
|
top_n=10
|
|
@@ -189,8 +189,8 @@ AoI.register(ForestEntity)
|
|
|
189
189
|
|
|
190
190
|
## 9. Execution Environment Context
|
|
191
191
|
|
|
192
|
-
- **Loading Data**: There is no explicit `AoI.load_data()` method. The `AoI(data_root="...")` constructor acts as the main entry point and defines the directory scope. Data reading happens lazily using Polars `scan_parquet()` when you explicitly call materialization methods like `.static`, `.annual`, or `.
|
|
193
|
-
- **Spatial and Temporal Relationship**: To conserve memory, temporal views (`.annual` and `.
|
|
192
|
+
- **Loading Data**: There is no explicit `AoI.load_data()` method. The `AoI(data_root="...")` constructor acts as the main entry point and defines the directory scope. Data reading happens lazily using Polars `scan_parquet()` when you explicitly call materialization methods like `.static`, `.annual`, or `.sub_annual` on a `View`.
|
|
193
|
+
- **Spatial and Temporal Relationship**: To conserve memory, temporal views (`.annual` and `.sub_annual`) drop geometry columns by default. To establish the spatial relationship between the static boundaries (`mws.parquet`) and the temporal time-series (`annual.parquet`), you use the `Result.with_geometry()` method. This triggers an internal join on the entity's primary key (e.g., `mws_id`) bringing the geometry back into the DataFrame for spatial operations and plotting.
|
|
194
194
|
|
|
195
195
|
- **Expected Directory Structure**: When initializing `AoI(data_root="data/")`, the library expects a specific folder structure inside the `data_root` for each registered entity. For example, the `MWSEntity` expects:
|
|
196
196
|
```text
|
|
@@ -199,7 +199,7 @@ AoI.register(ForestEntity)
|
|
|
199
199
|
├── static/
|
|
200
200
|
│ └── mws.parquet ← GeoParquet with boundaries
|
|
201
201
|
├── annual/ ← Directory of Parquets (often partitioned by basin)
|
|
202
|
-
└──
|
|
202
|
+
└── sub-annual/ ← Directory of Parquets (partitioned by year/basin)
|
|
203
203
|
```
|
|
204
204
|
*(Note: The exact paths for these files/directories are defined within the `BaseEntity` subclasses, e.g., `MWSEntity.static_path`)*
|
|
205
205
|
|
|
@@ -229,9 +229,9 @@ view = aoi.mws.where(ba_name="Barmer")
|
|
|
229
229
|
temporal_view = view.between(season="kharif", year=2021)
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
-
**5. Materialization:** Trigger the actual Parquet I/O to pull the data into memory by accessing `.static`, `.annual`, or `.
|
|
232
|
+
**5. Materialization:** Trigger the actual Parquet I/O to pull the data into memory by accessing `.static`, `.annual`, or `.sub_annual`.
|
|
233
233
|
```python
|
|
234
|
-
result = temporal_view.
|
|
234
|
+
result = temporal_view.sub_annual
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
**6. Compute & Analysis:** Chain computations, derive metrics, or run statistical tests on the returned `Result`.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
description="Analyze summary CSVs for missing data."
|
|
9
|
+
)
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"district", type=str, help="Name of the district (e.g. 'Sehore')"
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"--dir",
|
|
15
|
+
type=str,
|
|
16
|
+
default="/home/akashyap54/Projects/CoreStack/core-lens/district_summaries",
|
|
17
|
+
help="Directory with summaries",
|
|
18
|
+
)
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
safe_dist = str(args.district).replace("/", "_").replace(" ", "_").replace(",", "")
|
|
22
|
+
dist_dir = os.path.join(args.dir, safe_dist)
|
|
23
|
+
|
|
24
|
+
if not os.path.exists(dist_dir):
|
|
25
|
+
print(f"Error: No summaries found for district '{args.district}' at {dist_dir}")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
print(f"--- Data Completeness Report for {args.district} ---")
|
|
29
|
+
|
|
30
|
+
for cat in ["static", "annual", "sub-annual"]:
|
|
31
|
+
csv_path = os.path.join(dist_dir, f"{cat}_summary.csv")
|
|
32
|
+
print(f"\n[{cat.upper()}]")
|
|
33
|
+
|
|
34
|
+
if not os.path.exists(csv_path):
|
|
35
|
+
print(" Status: No Data (Missing File)")
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
df = pd.read_csv(csv_path)
|
|
40
|
+
|
|
41
|
+
# Check for completely empty table
|
|
42
|
+
if len(df) == 0:
|
|
43
|
+
print(" Status: Empty summary table")
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
# Depending on DuckDB version, null_percentage could be string '100.0%' or float
|
|
47
|
+
if "null_percentage" in df.columns:
|
|
48
|
+
if df["null_percentage"].dtype == object:
|
|
49
|
+
df["null_pct_float"] = (
|
|
50
|
+
df["null_percentage"]
|
|
51
|
+
.astype(str)
|
|
52
|
+
.str.rstrip("%")
|
|
53
|
+
.astype("float")
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
df["null_pct_float"] = df["null_percentage"]
|
|
57
|
+
else:
|
|
58
|
+
print(" Status: Unknown schema (no null_percentage column)")
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
total_cols = len(df)
|
|
62
|
+
fully_missing = df[df["null_pct_float"] >= 100.0]["column_name"].tolist()
|
|
63
|
+
partially_missing = df[
|
|
64
|
+
(df["null_pct_float"] > 0.0) & (df["null_pct_float"] < 100.0)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
print(f" Total Columns: {total_cols}")
|
|
68
|
+
|
|
69
|
+
if fully_missing:
|
|
70
|
+
print(f" Completely Null Columns ({len(fully_missing)}):")
|
|
71
|
+
print(" " + ", ".join(fully_missing))
|
|
72
|
+
else:
|
|
73
|
+
print(" Completely Null Columns: None")
|
|
74
|
+
|
|
75
|
+
if not partially_missing.empty:
|
|
76
|
+
print(f" Partially Null Columns ({len(partially_missing)}):")
|
|
77
|
+
for _, row in partially_missing.iterrows():
|
|
78
|
+
print(f" - {row['column_name']}: {row['null_percentage']} null")
|
|
79
|
+
else:
|
|
80
|
+
print(" Partially Null Columns: None")
|
|
81
|
+
|
|
82
|
+
if not fully_missing and partially_missing.empty:
|
|
83
|
+
print(" Status: COMPLETE (No missing values)")
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f" Error reading {cat}: {e}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|
|
@@ -10,7 +10,7 @@ benchmarks/
|
|
|
10
10
|
├── run_all.sh # Run all benchmarks in sequence
|
|
11
11
|
├── bench_aoi.py # AoI init, register, named-boundary resolution
|
|
12
12
|
├── bench_spatial.py # build_bbox_index, bbox_intersects_geometry, exact_spatial_filter
|
|
13
|
-
├── bench_view.py # View.where, spatial_filter, between, materialise (static/annual/
|
|
13
|
+
├── bench_view.py # View.where, spatial_filter, between, materialise (static/annual/sub-annual)
|
|
14
14
|
├── bench_result.py # Result.derive, aggregate, with_geometry, df/gdf
|
|
15
15
|
├── bench_season.py # resolve_time_filter, add_temporal_columns, SeasonConfig.season_for
|
|
16
16
|
├── bench_schema.py # schema detection (detect, _infer_* helpers)
|
|
@@ -7,7 +7,7 @@ Targets:
|
|
|
7
7
|
- Result.with_geometry() static-join to attach geometry to non-static result
|
|
8
8
|
- Result.derive() with_columns expression append
|
|
9
9
|
- Result.aggregate() group_by + agg (no temporal grouping)
|
|
10
|
-
- Result.aggregate(by=...) temporal grouping (annual /
|
|
10
|
+
- Result.aggregate(by=...) temporal grouping (annual / sub_annual)
|
|
11
11
|
|
|
12
12
|
Scalene focuses on:
|
|
13
13
|
- shapely.wkb.loads() in gdf() — Python loop over WKB bytes
|
|
@@ -130,7 +130,7 @@ print(
|
|
|
130
130
|
)
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
# ── Note: meaningful with_geometry() test requires annual/
|
|
133
|
+
# ── Note: meaningful with_geometry() test requires annual/sub_annual result ──
|
|
134
134
|
# Uncomment with an MWS entity that has annual_path set:
|
|
135
135
|
|
|
136
136
|
result_annual = aoi.mws.annual # no geometry
|
|
@@ -36,7 +36,7 @@ DATA_ROOT = "data/"
|
|
|
36
36
|
entity = MWSEntity(data_root=DATA_ROOT)
|
|
37
37
|
static_path = entity._resolve(entity.static_path)
|
|
38
38
|
annual_path = entity._resolve(entity.annual_path) if entity.annual_path else None
|
|
39
|
-
fn_path = entity._resolve(entity.
|
|
39
|
+
fn_path = entity._resolve(entity.sub_annual_path) if entity.sub_annual_path else None
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def _section(title: str) -> None:
|
|
@@ -104,7 +104,7 @@ print(
|
|
|
104
104
|
print(f"Detected time col: {_infer_time_col(schema, static_path)!r}")
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
# ── 5. detect() — cold (no annual/
|
|
107
|
+
# ── 5. detect() — cold (no annual/sub_annual) ────────────────────────────────
|
|
108
108
|
_section("5. detect(static only) [full detection — cold]")
|
|
109
109
|
REPS = 10
|
|
110
110
|
profile = detect( # ensure bound before loop
|
|
@@ -112,7 +112,7 @@ profile = detect( # ensure bound before loop
|
|
|
112
112
|
key_cols=entity.key_cols,
|
|
113
113
|
geometry_col=entity.geometry_col,
|
|
114
114
|
annual_path=None,
|
|
115
|
-
|
|
115
|
+
sub_annual_path=None,
|
|
116
116
|
)
|
|
117
117
|
t0 = time.perf_counter()
|
|
118
118
|
for _ in range(REPS):
|
|
@@ -121,7 +121,7 @@ for _ in range(REPS):
|
|
|
121
121
|
key_cols=entity.key_cols,
|
|
122
122
|
geometry_col=entity.geometry_col,
|
|
123
123
|
annual_path=None,
|
|
124
|
-
|
|
124
|
+
sub_annual_path=None,
|
|
125
125
|
)
|
|
126
126
|
t1 = time.perf_counter()
|
|
127
127
|
print(
|
|
@@ -133,15 +133,15 @@ print(f"Profile bbox_cols : {profile.bbox_cols!r}")
|
|
|
133
133
|
print(f"Profile extra_static : {profile.extra_static_cols}")
|
|
134
134
|
|
|
135
135
|
|
|
136
|
-
# ── 6. detect() — with annual +
|
|
136
|
+
# ── 6. detect() — with annual + sub_annual ───────────────────────────────────
|
|
137
137
|
if annual_path or fn_path:
|
|
138
|
-
_section("6. detect(static + annual +
|
|
138
|
+
_section("6. detect(static + annual + sub_annual) [3× schema reads]")
|
|
139
139
|
profile_full = detect( # ensure bound before loop
|
|
140
140
|
static_path=static_path,
|
|
141
141
|
key_cols=entity.key_cols,
|
|
142
142
|
geometry_col=entity.geometry_col,
|
|
143
143
|
annual_path=annual_path,
|
|
144
|
-
|
|
144
|
+
sub_annual_path=fn_path,
|
|
145
145
|
)
|
|
146
146
|
t0 = time.perf_counter()
|
|
147
147
|
for _ in range(REPS):
|
|
@@ -150,7 +150,7 @@ if annual_path or fn_path:
|
|
|
150
150
|
key_cols=entity.key_cols,
|
|
151
151
|
geometry_col=entity.geometry_col,
|
|
152
152
|
annual_path=annual_path,
|
|
153
|
-
|
|
153
|
+
sub_annual_path=fn_path,
|
|
154
154
|
)
|
|
155
155
|
t1 = time.perf_counter()
|
|
156
156
|
print(
|
|
@@ -158,10 +158,10 @@ if annual_path or fn_path:
|
|
|
158
158
|
f"({(t1 - t0) / REPS * 1000:.2f} ms/call)"
|
|
159
159
|
)
|
|
160
160
|
print(f"annual_time_col : {profile_full.annual_time_col!r}")
|
|
161
|
-
print(f"
|
|
161
|
+
print(f"sub_annual_time_col : {profile_full.sub_annual_time_col!r}")
|
|
162
162
|
else:
|
|
163
163
|
print(
|
|
164
|
-
"\n[skip] No annual/
|
|
164
|
+
"\n[skip] No annual/sub_annual paths on TehsilEntity — add an entity with those paths"
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
|
|
@@ -6,7 +6,7 @@ Targets:
|
|
|
6
6
|
- resolve_time_filter() dict → Polars Expr build (date-range + season modes)
|
|
7
7
|
- _date_range_expr() expression build for date ranges
|
|
8
8
|
- _season_expr() expression build for seasons (year-crossing vs not)
|
|
9
|
-
- add_temporal_columns()
|
|
9
|
+
- add_temporal_columns() sub_annual vectorised temporal derivation
|
|
10
10
|
|
|
11
11
|
Scalene focuses on:
|
|
12
12
|
- Python datetime.strptime calls in __post_init__
|
|
@@ -35,22 +35,22 @@ from core_lens.utils.season import add_temporal_columns, resolve_time_filter
|
|
|
35
35
|
START_DATE = date(2010, 1, 1)
|
|
36
36
|
END_DATE = date(2023, 12, 31)
|
|
37
37
|
|
|
38
|
-
# Number of
|
|
39
|
-
|
|
38
|
+
# Number of sub_annual periods between START_DATE and END_DATE (roughly).
|
|
39
|
+
N_SUB_ANNUAL = (END_DATE - START_DATE).days // 14
|
|
40
40
|
|
|
41
|
-
# Representative synthetic
|
|
41
|
+
# Representative synthetic sub_annual dataframe for add_temporal_columns.
|
|
42
42
|
N_MWS = 500 # number of entity instances (e.g. MWS)
|
|
43
|
-
DATES = [START_DATE + timedelta(days=14 * i) for i in range(
|
|
44
|
-
# Long format: N_MWS ×
|
|
43
|
+
DATES = [START_DATE + timedelta(days=14 * i) for i in range(N_SUB_ANNUAL)]
|
|
44
|
+
# Long format: N_MWS × N_SUB_ANNUAL rows
|
|
45
45
|
_all_dates = DATES * N_MWS
|
|
46
|
-
|
|
46
|
+
SUB_ANNUAL_DF = pl.DataFrame(
|
|
47
47
|
{
|
|
48
48
|
"mws_id": [str(mid) for mid in range(N_MWS) for _ in DATES],
|
|
49
49
|
"date": _all_dates,
|
|
50
|
-
"ndvi": [0.5] * (N_MWS *
|
|
50
|
+
"ndvi": [0.5] * (N_MWS * N_SUB_ANNUAL),
|
|
51
51
|
}
|
|
52
52
|
)
|
|
53
|
-
print(f"Synthetic
|
|
53
|
+
print(f"Synthetic sub_annual frame: {SUB_ANNUAL_DF.shape}")
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def _section(title: str) -> None:
|
|
@@ -162,15 +162,15 @@ print(
|
|
|
162
162
|
)
|
|
163
163
|
|
|
164
164
|
|
|
165
|
-
# ── 8. add_temporal_columns — large
|
|
166
|
-
_section(f"8. add_temporal_columns() [{
|
|
165
|
+
# ── 8. add_temporal_columns — large sub_annual DataFrame ─────────────────────
|
|
166
|
+
_section(f"8. add_temporal_columns() [{SUB_ANNUAL_DF.shape[0]:,} rows]")
|
|
167
167
|
t0 = time.perf_counter()
|
|
168
|
-
result_df = add_temporal_columns(
|
|
168
|
+
result_df = add_temporal_columns(SUB_ANNUAL_DF, "date", cfg)
|
|
169
169
|
t1 = time.perf_counter()
|
|
170
170
|
print(f"add_temporal_columns: {(t1 - t0) * 1000:.2f} ms")
|
|
171
171
|
print(f"Output shape : {cast(pl.DataFrame, result_df).shape}")
|
|
172
172
|
print(
|
|
173
|
-
f"New columns added : {[c for c in cast(pl.DataFrame, result_df).columns if c not in
|
|
173
|
+
f"New columns added : {[c for c in cast(pl.DataFrame, result_df).columns if c not in SUB_ANNUAL_DF.columns]}"
|
|
174
174
|
)
|
|
175
175
|
|
|
176
176
|
# Second call — all columns already present, should be near-zero.
|
|
@@ -4,14 +4,14 @@ Targets:
|
|
|
4
4
|
- View.where() attribute filter (Polars semi-join on static file)
|
|
5
5
|
- View.spatial_filter() STRtree filter narrowing existing keys
|
|
6
6
|
- View.between() time-filter dict construction (no I/O)
|
|
7
|
-
- View._materialise() static / annual /
|
|
7
|
+
- View._materialise() static / annual / sub_annual Parquet reads
|
|
8
8
|
- View chaining where → spatial_filter → between → static
|
|
9
9
|
|
|
10
10
|
Scalene focuses on:
|
|
11
11
|
- scan_with_key_filter() predicate-pushdown LazyFrame
|
|
12
12
|
- collect_lf() streaming collect (CPU or GPU)
|
|
13
13
|
- resolve_time_filter() expression build for time predicates
|
|
14
|
-
- add_temporal_columns()
|
|
14
|
+
- add_temporal_columns() sub_annual temporal column derivation
|
|
15
15
|
|
|
16
16
|
Usage:
|
|
17
17
|
uv run scalene run --html --outfile benchmarks/profiles/bench_view.html benchmarks/bench_view.py
|
|
@@ -140,12 +140,12 @@ print(f"annual : {(t1 - t0) * 1000:.2f} ms")
|
|
|
140
140
|
print(f"Shape : {result_annual.df().shape}")
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
# ── 11.
|
|
144
|
-
_section("11. aoi_small.mws.
|
|
143
|
+
# ── 11. sub_annual materialisation ───────────────────────────────────────────
|
|
144
|
+
_section("11. aoi_small.mws.sub_annual [sub_annual materialisation]")
|
|
145
145
|
t0 = time.perf_counter()
|
|
146
|
-
result_fn = aoi_small.mws.between(season=Season.KHARIF, year=2022).
|
|
146
|
+
result_fn = aoi_small.mws.between(season=Season.KHARIF, year=2022).sub_annual
|
|
147
147
|
t1 = time.perf_counter()
|
|
148
|
-
print(f"
|
|
148
|
+
print(f"sub_annual : {(t1 - t0) * 1000:.2f} ms")
|
|
149
149
|
print(f"Shape : {result_fn.df().shape}")
|
|
150
150
|
|
|
151
151
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import os
|
|
4
|
+
import duckdb
|
|
5
|
+
import json
|
|
6
|
+
import glob
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
def analyze_district(district, base_dir, out_dir):
|
|
10
|
+
safe_dist = str(district).strip().replace("/", "_").replace(" ", "_").replace(",", "")
|
|
11
|
+
if not safe_dist:
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
print(f"Processing district: {district}")
|
|
15
|
+
|
|
16
|
+
con = duckdb.connect()
|
|
17
|
+
|
|
18
|
+
# Check if district exists
|
|
19
|
+
found_any = False
|
|
20
|
+
for cat in ["static", "annual", "sub-annual"]:
|
|
21
|
+
if not glob.glob(f"{base_dir}/{cat}/**/*.parquet", recursive=True):
|
|
22
|
+
continue
|
|
23
|
+
try:
|
|
24
|
+
q = f"SELECT count(*) FROM read_parquet('{base_dir}/{cat}/**/*.parquet', union_by_name=true) WHERE list_contains(district, '{district.replace("'", "''")}')"
|
|
25
|
+
if con.execute(q).fetchone()[0] > 0:
|
|
26
|
+
found_any = True
|
|
27
|
+
break
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
if not found_any:
|
|
32
|
+
print(f" -> Error: District '{district}' not found. Skipping.")
|
|
33
|
+
con.close()
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
report = {
|
|
37
|
+
"district": district.strip(),
|
|
38
|
+
"categories": defaultdict(lambda: {
|
|
39
|
+
"null_percentages": {},
|
|
40
|
+
"completely_null_columns": [],
|
|
41
|
+
"most_empty_tehsils": {}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for cat in ["static", "annual", "sub-annual"]:
|
|
46
|
+
if not glob.glob(f"{base_dir}/{cat}/**/*.parquet", recursive=True):
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
query = f"""
|
|
50
|
+
SELECT * FROM read_parquet('{base_dir}/{cat}/**/*.parquet', union_by_name=true)
|
|
51
|
+
WHERE list_contains(district, '{district.replace("'", "''")}')
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
df = con.execute(query).df()
|
|
55
|
+
if len(df) == 0:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
df_exploded = df.explode('tehsil') if 'tehsil' in df.columns else df
|
|
59
|
+
|
|
60
|
+
# Identify data columns
|
|
61
|
+
exclude = ['district', 'tehsil', 'state', 'geometry', 'id', 'key']
|
|
62
|
+
data_cols = [c for c in df.columns if c not in exclude]
|
|
63
|
+
|
|
64
|
+
total_rows = len(df)
|
|
65
|
+
cat_dict = report["categories"][cat]
|
|
66
|
+
|
|
67
|
+
# Column null percentages
|
|
68
|
+
for c in data_cols:
|
|
69
|
+
nulls = int(df[c].isna().sum())
|
|
70
|
+
pct = float((nulls / total_rows) * 100) if total_rows > 0 else 100.0
|
|
71
|
+
cat_dict["null_percentages"][c] = round(pct, 2)
|
|
72
|
+
if pct == 100.0:
|
|
73
|
+
cat_dict["completely_null_columns"].append(c)
|
|
74
|
+
|
|
75
|
+
# Tehsil missingness
|
|
76
|
+
if 'tehsil' in df_exploded.columns and len(data_cols) > 0:
|
|
77
|
+
missing_by_tehsil = df_exploded[data_cols].isna().sum(axis=1).groupby(df_exploded['tehsil']).sum()
|
|
78
|
+
total_by_tehsil = df_exploded.groupby('tehsil').size() * len(data_cols)
|
|
79
|
+
pct_series = (missing_by_tehsil / total_by_tehsil) * 100
|
|
80
|
+
|
|
81
|
+
# Top 3 most empty
|
|
82
|
+
top_empty = pct_series.sort_values(ascending=False).head(3)
|
|
83
|
+
|
|
84
|
+
empty_dict = {}
|
|
85
|
+
for t, v in top_empty.items():
|
|
86
|
+
if pd.notna(v) and t is not None:
|
|
87
|
+
empty_dict[str(t)] = round(float(v), 2)
|
|
88
|
+
|
|
89
|
+
cat_dict["most_empty_tehsils"] = empty_dict
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f" -> Error processing {cat} for {district}: {e}")
|
|
93
|
+
|
|
94
|
+
con.close()
|
|
95
|
+
|
|
96
|
+
# Convert defaultdict to standard dict for JSON serialization
|
|
97
|
+
report["categories"] = dict(report["categories"])
|
|
98
|
+
|
|
99
|
+
out_file = os.path.join(out_dir, f"{safe_dist}_report.json")
|
|
100
|
+
with open(out_file, 'w') as f:
|
|
101
|
+
json.dump(report, f, indent=2)
|
|
102
|
+
|
|
103
|
+
print(f" -> Saved report to {out_file}")
|
|
104
|
+
|
|
105
|
+
def main():
|
|
106
|
+
parser = argparse.ArgumentParser(description="Generate district data completeness report in JSON.")
|
|
107
|
+
parser.add_argument("districts_file", type=str, help="File containing one district name per line")
|
|
108
|
+
parser.add_argument("--base-dir", type=str, default="/home/akashyap54/workstation/CoREStack-Data/mws", help="Base directory of raw data")
|
|
109
|
+
parser.add_argument("--out-dir", type=str, default="/home/akashyap54/Projects/CoreStack/core-lens/district_reports", help="Output directory")
|
|
110
|
+
args = parser.parse_args()
|
|
111
|
+
|
|
112
|
+
os.makedirs(args.out_dir, exist_ok=True)
|
|
113
|
+
|
|
114
|
+
if not os.path.exists(args.districts_file):
|
|
115
|
+
print(f"Error: Input file {args.districts_file} does not exist.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
with open(args.districts_file, 'r') as f:
|
|
119
|
+
districts = [line.strip() for line in f if line.strip()]
|
|
120
|
+
|
|
121
|
+
print(f"Found {len(districts)} districts to process.")
|
|
122
|
+
|
|
123
|
+
for d in districts:
|
|
124
|
+
analyze_district(d, args.base_dir, args.out_dir)
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
main()
|