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.
Files changed (100) hide show
  1. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/CONTRIBUTING.md +2 -2
  2. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/PKG-INFO +1 -1
  3. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/SKILLS.md +7 -7
  4. core_lens-0.1.dev166/analyze_summaries.py +90 -0
  5. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/README.md +1 -1
  6. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_result.py +2 -2
  7. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_schema.py +10 -10
  8. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_season.py +13 -13
  9. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_view.py +6 -6
  10. core_lens-0.1.dev166/district_report.py +127 -0
  11. core_lens-0.1.dev166/district_reports/Sehore_report.json +193 -0
  12. core_lens-0.1.dev166/district_summaries/Sehore/annual_summary.csv +38 -0
  13. core_lens-0.1.dev166/district_summaries/Sehore/static_summary.csv +64 -0
  14. core_lens-0.1.dev166/district_summaries/Sehore/sub-annual_summary.csv +21 -0
  15. core_lens-0.1.dev166/docs/source/concepts.md +7 -0
  16. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/logging.md +1 -1
  17. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/queries.md +7 -7
  18. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/stats.md +1 -1
  19. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/examples/demo_mws.py +12 -12
  20. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/examples/demo_tehsil.py +2 -2
  21. core_lens-0.1.dev166/generate_summaries.py +69 -0
  22. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/_version.py +2 -2
  23. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/aoi.py +3 -3
  24. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/entity.py +13 -13
  25. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/namespaces/plot.py +30 -1
  26. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/namespaces/stats.py +30 -7
  27. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/result.py +10 -10
  28. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/view.py +15 -15
  29. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/entities/mws.py +4 -4
  30. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/entities/tehsil.py +1 -1
  31. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/schema/detection.py +18 -18
  32. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/schema/profile.py +8 -8
  33. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/season.py +2 -2
  34. core_lens-0.1.dev166/test_districts.txt +1 -0
  35. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/fixtures/generate_fixtures.py +7 -7
  36. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/conftest.py +16 -16
  37. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_aoi.py +3 -3
  38. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_entities.py +2 -2
  39. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_entity.py +2 -2
  40. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_plot.py +1 -1
  41. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_profile.py +1 -1
  42. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_result.py +8 -8
  43. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_schema_detection.py +7 -7
  44. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_schema_profile.py +7 -7
  45. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_spatial.py +3 -3
  46. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_stats.py +6 -6
  47. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_view.py +31 -61
  48. core_lens-0.1.dev164/docs/source/concepts.md +0 -7
  49. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  50. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  51. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  52. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/pull_request_template.md +0 -0
  53. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/workflows/ci.yml +0 -0
  54. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/workflows/gh-pages.yml +0 -0
  55. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/workflows/pre-release.yml +0 -0
  56. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.github/workflows/release.yml +0 -0
  57. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.gitignore +0 -0
  58. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.gitmessage +0 -0
  59. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.pre-commit-config.yaml +0 -0
  60. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/.python-version +0 -0
  61. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/LICENSE +0 -0
  62. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/README.md +0 -0
  63. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_aoi.py +0 -0
  64. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_export.py +0 -0
  65. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_polars_utils.py +0 -0
  66. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/bench_spatial.py +0 -0
  67. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/benchmarks/run_all.sh +0 -0
  68. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/Makefile +0 -0
  69. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/make.bat +0 -0
  70. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/conf.py +0 -0
  71. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/export.md +0 -0
  72. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/index.rst +0 -0
  73. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/intro.md +0 -0
  74. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/plots.md +0 -0
  75. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/plugins.md +0 -0
  76. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/docs/source/quickstart.md +0 -0
  77. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/hooks/mypy.sh +0 -0
  78. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/hooks/no-parquet-outside-fixtures.sh +0 -0
  79. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/hooks/pytest.sh +0 -0
  80. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/pyproject.toml +0 -0
  81. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/__init__.py +0 -0
  82. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/__main__.py +0 -0
  83. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/__init__.py +0 -0
  84. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/base/namespaces/__init__.py +0 -0
  85. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/entities/__init__.py +0 -0
  86. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/entities/waterbody.py +0 -0
  87. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/export/__init__.py +0 -0
  88. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/export/formats.py +0 -0
  89. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/py.typed +0 -0
  90. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/schema/__init__.py +0 -0
  91. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/__init__.py +0 -0
  92. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/paths.py +0 -0
  93. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/polars_utils.py +0 -0
  94. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/src/core_lens/utils/spatial.py +0 -0
  95. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_export.py +0 -0
  96. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_main.py +0 -0
  97. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_polars_utils.py +0 -0
  98. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_season.py +0 -0
  99. {core_lens-0.1.dev164 → core_lens-0.1.dev166}/tests/unit/test_season_config.py +0 -0
  100. {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-fortnightly
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 fortnightly
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.dev164
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
- fortnightly_result = view.fortnightly # no geometry
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": ("fortnightly", {"season": "kharif", "year": 2020})
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 `.fortnightly` on a `View`.
193
- - **Spatial and Temporal Relationship**: To conserve memory, temporal views (`.annual` and `.fortnightly`) 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.
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
- └── fortnightly/ ← Directory of Parquets (partitioned by year/basin)
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 `.fortnightly`.
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.fortnightly
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/fortnightly)
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 / fortnightly)
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/fortnightly result ──
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.fortnightly_path) if entity.fortnightly_path else None
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/fortnightly) ────────────────────────────────
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
- fortnightly_path=None,
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
- fortnightly_path=None,
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 + fortnightly ───────────────────────────────────
136
+ # ── 6. detect() — with annual + sub_annual ───────────────────────────────────
137
137
  if annual_path or fn_path:
138
- _section("6. detect(static + annual + fortnightly) [3× schema reads]")
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
- fortnightly_path=fn_path,
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
- fortnightly_path=fn_path,
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"fortnightly_time_col : {profile_full.fortnightly_time_col!r}")
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/fortnightly paths on TehsilEntity — add an entity with those paths"
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() fortnightly vectorised temporal derivation
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 fortnightly periods between START_DATE and END_DATE (roughly).
39
- N_FORTNIGHTLY = (END_DATE - START_DATE).days // 14
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 fortnightly dataframe for add_temporal_columns.
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(N_FORTNIGHTLY)]
44
- # Long format: N_MWS × N_FORTNIGHTLY rows
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
- FORTNIGHTLY_DF = pl.DataFrame(
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 * N_FORTNIGHTLY),
50
+ "ndvi": [0.5] * (N_MWS * N_SUB_ANNUAL),
51
51
  }
52
52
  )
53
- print(f"Synthetic fortnightly frame: {FORTNIGHTLY_DF.shape}")
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 fortnightly DataFrame ─────────────────────
166
- _section(f"8. add_temporal_columns() [{FORTNIGHTLY_DF.shape[0]:,} rows]")
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(FORTNIGHTLY_DF, "date", cfg)
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 FORTNIGHTLY_DF.columns]}"
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 / fortnightly Parquet reads
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() fortnightly temporal column derivation
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. fortnightly materialisation ───────────────────────────────────────────
144
- _section("11. aoi_small.mws.fortnightly [fortnightly materialisation]")
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).fortnightly
146
+ result_fn = aoi_small.mws.between(season=Season.KHARIF, year=2022).sub_annual
147
147
  t1 = time.perf_counter()
148
- print(f"fortnightly : {(t1 - t0) * 1000:.2f} ms")
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()