spatial-data-foundation 0.1.0__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.
- spatial_data_foundation-0.1.0/PKG-INFO +29 -0
- spatial_data_foundation-0.1.0/README.md +56 -0
- spatial_data_foundation-0.1.0/pyproject.toml +41 -0
- spatial_data_foundation-0.1.0/setup.cfg +4 -0
- spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/PKG-INFO +29 -0
- spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/SOURCES.txt +29 -0
- spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/dependency_links.txt +1 -0
- spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/requires.txt +29 -0
- spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/top_level.txt +1 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/__init__.py +20 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/catalog.py +52 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/geography/__init__.py +19 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/geography/gadm.py +92 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/geography/materialize.py +450 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/geography/membership.py +74 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/geography/models.py +37 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/geography/overlap.py +178 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/periods.py +48 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/presentation/__init__.py +10 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/presentation/basemaps.py +85 -0
- spatial_data_foundation-0.1.0/src/spatial_foundation/presentation/plotting.py +32 -0
- spatial_data_foundation-0.1.0/tests/test_adversarial_spatial.py +165 -0
- spatial_data_foundation-0.1.0/tests/test_areal_overlap.py +262 -0
- spatial_data_foundation-0.1.0/tests/test_catalog.py +29 -0
- spatial_data_foundation-0.1.0/tests/test_distribution.py +112 -0
- spatial_data_foundation-0.1.0/tests/test_external_consumer.py +28 -0
- spatial_data_foundation-0.1.0/tests/test_gadm_materialization.py +138 -0
- spatial_data_foundation-0.1.0/tests/test_geography.py +63 -0
- spatial_data_foundation-0.1.0/tests/test_membership_kernel.py +123 -0
- spatial_data_foundation-0.1.0/tests/test_periods.py +26 -0
- spatial_data_foundation-0.1.0/tests/test_presentation.py +144 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spatial-data-foundation
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable geography, time, provenance and spatial-membership infrastructure for empirical research
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: empirical-data-contracts<0.2,>=0.1
|
|
7
|
+
Requires-Dist: geopandas>=0.14
|
|
8
|
+
Requires-Dist: shapely>=2
|
|
9
|
+
Requires-Dist: pyproj>=3.5
|
|
10
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
11
|
+
Provides-Extra: io
|
|
12
|
+
Requires-Dist: pyarrow>=15; extra == "io"
|
|
13
|
+
Requires-Dist: duckdb>=1.0; extra == "io"
|
|
14
|
+
Provides-Extra: raster
|
|
15
|
+
Requires-Dist: rasterio>=1.3; extra == "raster"
|
|
16
|
+
Provides-Extra: cli
|
|
17
|
+
Requires-Dist: typer>=0.12; extra == "cli"
|
|
18
|
+
Provides-Extra: presentation
|
|
19
|
+
Requires-Dist: matplotlib>=3.8; extra == "presentation"
|
|
20
|
+
Requires-Dist: contextily<2,>=1.7.1; extra == "presentation"
|
|
21
|
+
Requires-Dist: xyzservices<2027,>=2026.3; extra == "presentation"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
25
|
+
Requires-Dist: pyarrow>=15; extra == "dev"
|
|
26
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
27
|
+
Requires-Dist: matplotlib>=3.8; extra == "dev"
|
|
28
|
+
Requires-Dist: contextily<2,>=1.7.1; extra == "dev"
|
|
29
|
+
Requires-Dist: xyzservices<2027,>=2026.3; extra == "dev"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# spatial-data-foundation
|
|
2
|
+
|
|
3
|
+
Reusable spatial/time infrastructure for research systems.
|
|
4
|
+
|
|
5
|
+
This repository owns geography authority, period indexing, source registration, auditable spatial membership, and materialization provenance. It does not own FCV treatments, outcomes, matching, survey harmonization, or regressions.
|
|
6
|
+
|
|
7
|
+
Initial provider: GADM. Initial clients in the next build pack: GHSL and ACLED.
|
|
8
|
+
|
|
9
|
+
## Operational GADM materialization
|
|
10
|
+
|
|
11
|
+
After registering local GADM files as an immutable source snapshot, install the IO extra and materialize the native levels needed by downstream work:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from spatial_foundation import materialize_gadm
|
|
15
|
+
|
|
16
|
+
materialize_gadm(
|
|
17
|
+
snapshot=gadm_4_1_snapshot,
|
|
18
|
+
levels=[0, 1, 2, 3],
|
|
19
|
+
output_root=data_root,
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This publishes full-geometry GeoParquet under `silver/geography/gadm/<version>/` and writes `run_manifest.json` plus `geography_qa.json` under the run directory. Registered source hashes are rechecked before publication; source drift produces a RED run instead of silently accepting changed bytes.
|
|
24
|
+
|
|
25
|
+
## Optional contextual presentation
|
|
26
|
+
|
|
27
|
+
Presentation helpers are an optional, non-analytical surface. Core imports do not require plotting or tile dependencies:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install "spatial-data-foundation[presentation]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Add contextual imagery beneath an existing analytical plot:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from spatial_foundation.presentation import add_basemap
|
|
37
|
+
|
|
38
|
+
ax = gdf.plot(column="value", alpha=0.65)
|
|
39
|
+
add_basemap(ax, crs=gdf.crs, kind="imagery", alpha=0.55)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or use the compact GeoDataFrame helper:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from spatial_foundation.presentation import plot_context
|
|
46
|
+
|
|
47
|
+
ax = plot_context(
|
|
48
|
+
gdf,
|
|
49
|
+
basemap="neutral",
|
|
50
|
+
column="value",
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The governed convenience aliases are `neutral`, `imagery`, and `terrain`. A concrete tile source or local raster path can also be supplied. Local rasters allow reproducible/offline presentation; web providers may perform network requests when explicitly selected by the caller.
|
|
55
|
+
|
|
56
|
+
Presentation never changes analytical geometry, membership, identifiers, or domain interpretation. Provider attribution remains enabled, and no provider credentials are bundled with this package.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spatial-data-foundation"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reusable geography, time, provenance and spatial-membership infrastructure for empirical research"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"empirical-data-contracts>=0.1,<0.2",
|
|
12
|
+
"geopandas>=0.14",
|
|
13
|
+
"shapely>=2",
|
|
14
|
+
"pyproj>=3.5",
|
|
15
|
+
"pydantic>=2.7,<3",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
io = ["pyarrow>=15", "duckdb>=1.0"]
|
|
20
|
+
raster = ["rasterio>=1.3"]
|
|
21
|
+
cli = ["typer>=0.12"]
|
|
22
|
+
presentation = [
|
|
23
|
+
"matplotlib>=3.8",
|
|
24
|
+
"contextily>=1.7.1,<2",
|
|
25
|
+
"xyzservices>=2026.3,<2027",
|
|
26
|
+
]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=8",
|
|
29
|
+
"ruff>=0.6",
|
|
30
|
+
"pyarrow>=15",
|
|
31
|
+
"build>=1.2",
|
|
32
|
+
"matplotlib>=3.8",
|
|
33
|
+
"contextily>=1.7.1,<2",
|
|
34
|
+
"xyzservices>=2026.3,<2027",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spatial-data-foundation
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable geography, time, provenance and spatial-membership infrastructure for empirical research
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: empirical-data-contracts<0.2,>=0.1
|
|
7
|
+
Requires-Dist: geopandas>=0.14
|
|
8
|
+
Requires-Dist: shapely>=2
|
|
9
|
+
Requires-Dist: pyproj>=3.5
|
|
10
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
11
|
+
Provides-Extra: io
|
|
12
|
+
Requires-Dist: pyarrow>=15; extra == "io"
|
|
13
|
+
Requires-Dist: duckdb>=1.0; extra == "io"
|
|
14
|
+
Provides-Extra: raster
|
|
15
|
+
Requires-Dist: rasterio>=1.3; extra == "raster"
|
|
16
|
+
Provides-Extra: cli
|
|
17
|
+
Requires-Dist: typer>=0.12; extra == "cli"
|
|
18
|
+
Provides-Extra: presentation
|
|
19
|
+
Requires-Dist: matplotlib>=3.8; extra == "presentation"
|
|
20
|
+
Requires-Dist: contextily<2,>=1.7.1; extra == "presentation"
|
|
21
|
+
Requires-Dist: xyzservices<2027,>=2026.3; extra == "presentation"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
25
|
+
Requires-Dist: pyarrow>=15; extra == "dev"
|
|
26
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
27
|
+
Requires-Dist: matplotlib>=3.8; extra == "dev"
|
|
28
|
+
Requires-Dist: contextily<2,>=1.7.1; extra == "dev"
|
|
29
|
+
Requires-Dist: xyzservices<2027,>=2026.3; extra == "dev"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/spatial_data_foundation.egg-info/PKG-INFO
|
|
4
|
+
src/spatial_data_foundation.egg-info/SOURCES.txt
|
|
5
|
+
src/spatial_data_foundation.egg-info/dependency_links.txt
|
|
6
|
+
src/spatial_data_foundation.egg-info/requires.txt
|
|
7
|
+
src/spatial_data_foundation.egg-info/top_level.txt
|
|
8
|
+
src/spatial_foundation/__init__.py
|
|
9
|
+
src/spatial_foundation/catalog.py
|
|
10
|
+
src/spatial_foundation/periods.py
|
|
11
|
+
src/spatial_foundation/geography/__init__.py
|
|
12
|
+
src/spatial_foundation/geography/gadm.py
|
|
13
|
+
src/spatial_foundation/geography/materialize.py
|
|
14
|
+
src/spatial_foundation/geography/membership.py
|
|
15
|
+
src/spatial_foundation/geography/models.py
|
|
16
|
+
src/spatial_foundation/geography/overlap.py
|
|
17
|
+
src/spatial_foundation/presentation/__init__.py
|
|
18
|
+
src/spatial_foundation/presentation/basemaps.py
|
|
19
|
+
src/spatial_foundation/presentation/plotting.py
|
|
20
|
+
tests/test_adversarial_spatial.py
|
|
21
|
+
tests/test_areal_overlap.py
|
|
22
|
+
tests/test_catalog.py
|
|
23
|
+
tests/test_distribution.py
|
|
24
|
+
tests/test_external_consumer.py
|
|
25
|
+
tests/test_gadm_materialization.py
|
|
26
|
+
tests/test_geography.py
|
|
27
|
+
tests/test_membership_kernel.py
|
|
28
|
+
tests/test_periods.py
|
|
29
|
+
tests/test_presentation.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
empirical-data-contracts<0.2,>=0.1
|
|
2
|
+
geopandas>=0.14
|
|
3
|
+
shapely>=2
|
|
4
|
+
pyproj>=3.5
|
|
5
|
+
pydantic<3,>=2.7
|
|
6
|
+
|
|
7
|
+
[cli]
|
|
8
|
+
typer>=0.12
|
|
9
|
+
|
|
10
|
+
[dev]
|
|
11
|
+
pytest>=8
|
|
12
|
+
ruff>=0.6
|
|
13
|
+
pyarrow>=15
|
|
14
|
+
build>=1.2
|
|
15
|
+
matplotlib>=3.8
|
|
16
|
+
contextily<2,>=1.7.1
|
|
17
|
+
xyzservices<2027,>=2026.3
|
|
18
|
+
|
|
19
|
+
[io]
|
|
20
|
+
pyarrow>=15
|
|
21
|
+
duckdb>=1.0
|
|
22
|
+
|
|
23
|
+
[presentation]
|
|
24
|
+
matplotlib>=3.8
|
|
25
|
+
contextily<2,>=1.7.1
|
|
26
|
+
xyzservices<2027,>=2026.3
|
|
27
|
+
|
|
28
|
+
[raster]
|
|
29
|
+
rasterio>=1.3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spatial_foundation
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .catalog import DataRoot, register_external_snapshot, sha256_file
|
|
2
|
+
from .geography import (
|
|
3
|
+
ArealOverlapAudit,
|
|
4
|
+
GADMMaterialization,
|
|
5
|
+
materialize_gadm,
|
|
6
|
+
relate_areal_objects,
|
|
7
|
+
)
|
|
8
|
+
from .periods import Period, PeriodIndex
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ArealOverlapAudit",
|
|
12
|
+
"DataRoot",
|
|
13
|
+
"GADMMaterialization",
|
|
14
|
+
"Period",
|
|
15
|
+
"PeriodIndex",
|
|
16
|
+
"materialize_gadm",
|
|
17
|
+
"register_external_snapshot",
|
|
18
|
+
"relate_areal_objects",
|
|
19
|
+
"sha256_file",
|
|
20
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from empirical_contracts import SourceFileRef, SourceSnapshotRef
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class DataRoot:
|
|
12
|
+
root: Path
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_path(cls, value: str | Path) -> DataRoot:
|
|
16
|
+
return cls(Path(value).expanduser().resolve())
|
|
17
|
+
|
|
18
|
+
def bronze(self, source: str, release: str, snapshot_id: str) -> Path:
|
|
19
|
+
return self.root / "bronze" / source / release / snapshot_id
|
|
20
|
+
|
|
21
|
+
def silver(self, domain: str, dataset: str, version: str) -> Path:
|
|
22
|
+
return self.root / "silver" / domain / dataset / version
|
|
23
|
+
|
|
24
|
+
def gold(self, domain: str, dataset: str, version: str) -> Path:
|
|
25
|
+
return self.root / "gold" / domain / dataset / version
|
|
26
|
+
|
|
27
|
+
def run(self, package: str, run_id: str) -> Path:
|
|
28
|
+
return self.root / "runs" / package / run_id
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sha256_file(path: str | Path, chunk_size: int = 1024 * 1024) -> str:
|
|
32
|
+
digest = sha256()
|
|
33
|
+
with Path(path).open("rb") as handle:
|
|
34
|
+
while chunk := handle.read(chunk_size):
|
|
35
|
+
digest.update(chunk)
|
|
36
|
+
return digest.hexdigest()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def register_external_snapshot(source: str, release: str, paths: list[str | Path]) -> SourceSnapshotRef:
|
|
40
|
+
resolved_paths = sorted(Path(raw).expanduser().resolve() for raw in paths)
|
|
41
|
+
refs = []
|
|
42
|
+
for path in resolved_paths:
|
|
43
|
+
stat = path.stat()
|
|
44
|
+
refs.append(SourceFileRef(path=str(path), sha256=sha256_file(path), size_bytes=stat.st_size))
|
|
45
|
+
short = sha256("".join(ref.sha256 for ref in refs).encode()).hexdigest()[:12]
|
|
46
|
+
return SourceSnapshotRef(
|
|
47
|
+
source=source,
|
|
48
|
+
release=release,
|
|
49
|
+
snapshot_id=f"{source}-{release}-{short}",
|
|
50
|
+
storage_mode="external_immutable",
|
|
51
|
+
files=tuple(refs),
|
|
52
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .gadm import normalize_gadm_frame
|
|
2
|
+
from .materialize import GADMMaterialization, materialize_gadm
|
|
3
|
+
from .membership import MembershipAudit, assign_points
|
|
4
|
+
from .models import GeographyUnit, GeometryRole, MembershipStatus, geography_uid
|
|
5
|
+
from .overlap import ArealOverlapAudit, relate_areal_objects
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ArealOverlapAudit",
|
|
9
|
+
"GADMMaterialization",
|
|
10
|
+
"GeographyUnit",
|
|
11
|
+
"GeometryRole",
|
|
12
|
+
"MembershipAudit",
|
|
13
|
+
"MembershipStatus",
|
|
14
|
+
"assign_points",
|
|
15
|
+
"geography_uid",
|
|
16
|
+
"materialize_gadm",
|
|
17
|
+
"normalize_gadm_frame",
|
|
18
|
+
"relate_areal_objects",
|
|
19
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
|
|
5
|
+
from .models import GeometryRole, geography_uid
|
|
6
|
+
|
|
7
|
+
REQUIRED_BASE = {"GID_0"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _require_identity(series, *, label: str):
|
|
11
|
+
missing = series.isna() | series.astype("string").str.strip().eq("")
|
|
12
|
+
if missing.any():
|
|
13
|
+
raise ValueError(f"GADM {label} is not resolvable for {int(missing.sum())} row(s)")
|
|
14
|
+
return series.astype(str)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalize_gadm_frame(
|
|
18
|
+
frame: gpd.GeoDataFrame,
|
|
19
|
+
*,
|
|
20
|
+
version: str,
|
|
21
|
+
level: int,
|
|
22
|
+
area_crs: str = "EPSG:6933",
|
|
23
|
+
) -> gpd.GeoDataFrame:
|
|
24
|
+
"""Normalize one native GADM level without simplifying analytical geometry.
|
|
25
|
+
|
|
26
|
+
Expected source fields are GID_0 .. GID_{level}. Parent identity is retained when
|
|
27
|
+
available. Geometry is stored in EPSG:4326; area is calculated in the declared
|
|
28
|
+
equal-area CRS. The returned frame records the area CRS and geometry validity
|
|
29
|
+
profile in ``GeoDataFrame.attrs`` for run-manifest/provenance capture.
|
|
30
|
+
"""
|
|
31
|
+
if level < 0:
|
|
32
|
+
raise ValueError("GADM admin level must be >= 0")
|
|
33
|
+
if not version.strip():
|
|
34
|
+
raise ValueError("GADM version must be non-empty")
|
|
35
|
+
|
|
36
|
+
gid_col = f"GID_{level}"
|
|
37
|
+
parent_col = f"GID_{level - 1}" if level > 0 else None
|
|
38
|
+
required = REQUIRED_BASE | {gid_col}
|
|
39
|
+
missing = required - set(frame.columns)
|
|
40
|
+
if missing:
|
|
41
|
+
raise ValueError(f"missing required GADM columns: {sorted(missing)}")
|
|
42
|
+
if frame.crs is None:
|
|
43
|
+
raise ValueError("GADM source must have a declared CRS")
|
|
44
|
+
|
|
45
|
+
source_ids = _require_identity(frame[gid_col], label="source geography identity")
|
|
46
|
+
country_ids = _require_identity(frame["GID_0"], label="country identity")
|
|
47
|
+
|
|
48
|
+
data = frame.to_crs("EPSG:4326").copy()
|
|
49
|
+
metric = data.to_crs(area_crs)
|
|
50
|
+
data["area_km2"] = metric.geometry.area / 1_000_000.0
|
|
51
|
+
data["provider"] = "gadm"
|
|
52
|
+
data["version"] = version
|
|
53
|
+
data["source_geo_id"] = source_ids.to_numpy()
|
|
54
|
+
data["country_iso3"] = country_ids.str.slice(0, 3).to_numpy()
|
|
55
|
+
data["native_admin_level"] = level
|
|
56
|
+
data["geo_uid"] = data["source_geo_id"].map(lambda x: geography_uid("gadm", version, level, x))
|
|
57
|
+
if parent_col and parent_col in data.columns:
|
|
58
|
+
parent_present = data[parent_col].notna() & data[parent_col].astype("string").str.strip().ne("")
|
|
59
|
+
data["parent_geo_uid"] = [
|
|
60
|
+
geography_uid("gadm", version, level - 1, str(value)) if present else None
|
|
61
|
+
for value, present in zip(data[parent_col], parent_present)
|
|
62
|
+
]
|
|
63
|
+
else:
|
|
64
|
+
data["parent_geo_uid"] = None
|
|
65
|
+
data["geometry_role"] = GeometryRole.ANALYTICAL.value
|
|
66
|
+
|
|
67
|
+
keep = [
|
|
68
|
+
"geo_uid",
|
|
69
|
+
"provider",
|
|
70
|
+
"version",
|
|
71
|
+
"source_geo_id",
|
|
72
|
+
"country_iso3",
|
|
73
|
+
"native_admin_level",
|
|
74
|
+
"parent_geo_uid",
|
|
75
|
+
"area_km2",
|
|
76
|
+
"geometry_role",
|
|
77
|
+
"geometry",
|
|
78
|
+
]
|
|
79
|
+
out = data[keep].copy()
|
|
80
|
+
if out["geo_uid"].duplicated().any():
|
|
81
|
+
dupes = out.loc[out["geo_uid"].duplicated(keep=False), "geo_uid"].unique().tolist()[:10]
|
|
82
|
+
raise ValueError(f"duplicate geography IDs after normalization: {dupes}")
|
|
83
|
+
|
|
84
|
+
null_geometry = out.geometry.isna()
|
|
85
|
+
invalid_geometry = ~null_geometry & ~out.geometry.is_valid
|
|
86
|
+
out.attrs["area_crs"] = area_crs
|
|
87
|
+
out.attrs["geometry_profile"] = {
|
|
88
|
+
"rows": len(out),
|
|
89
|
+
"null": int(null_geometry.sum()),
|
|
90
|
+
"invalid": int(invalid_geometry.sum()),
|
|
91
|
+
}
|
|
92
|
+
return out
|