Skip to content

Geospatial Data Engineering Best Practices

This document establishes production-ready patterns for geospatial data engineering, covering spatial data management, raster processing, and optimization strategies used in enterprise geospatial systems.

Spatial Data Management

GeoParquet Implementation

import geopandas as gpd
import pyarrow.parquet as pq
from shapely.geometry import Point, Polygon
import pyproj

def create_optimized_geoparquet(data_path, output_path, target_crs='EPSG:3857'):
    """
    Create optimized GeoParquet with spatial indexing and compression
    """
    # Load spatial data
    gdf = gpd.read_file(data_path)

    # Ensure consistent CRS
    if gdf.crs != target_crs:
        gdf = gdf.to_crs(target_crs)

    # Add spatial index
    gdf = gdf.set_index('id') if 'id' not in gdf.columns else gdf

    # Optimize for analytics
    gdf['geometry'] = gdf['geometry'].apply(lambda x: x.wkt)

    # Write with compression and spatial metadata
    gdf.to_parquet(
        output_path,
        engine='pyarrow',
        compression='snappy',
        index=True,
        schema={
            'geometry': 'string',
            'properties': 'struct<...>'  # Define your schema
        }
    )

    return gdf

# Usage example
optimized_data = create_optimized_geoparquet(
    'input.geojson',
    'output.parquet',
    'EPSG:3857'
)

Why: GeoParquet provides columnar storage with spatial metadata, enabling efficient analytics and cross-platform compatibility. Compression reduces storage costs while maintaining query performance.

Spatial Indexing Strategies

import geopandas as gpd
from shapely.geometry import Point
import rtree

def create_spatial_index(gdf, index_name='spatial_idx'):
    """
    Create efficient spatial index for fast spatial queries
    """
    # Create R-tree index
    spatial_index = rtree.index.Index()

    for idx, row in gdf.iterrows():
        bounds = row.geometry.bounds
        spatial_index.insert(idx, bounds)

    return spatial_index

def spatial_join_optimized(left_gdf, right_gdf, predicate='intersects'):
    """
    Optimized spatial join using spatial indexing
    """
    # Create spatial index for right dataframe
    right_index = create_spatial_index(right_gdf)

    results = []
    for idx, left_row in left_gdf.iterrows():
        # Query spatial index
        candidates = list(right_index.intersection(left_row.geometry.bounds))

        # Perform precise geometric operations only on candidates
        for candidate_idx in candidates:
            if getattr(left_row.geometry, predicate)(right_gdf.iloc[candidate_idx].geometry):
                results.append({
                    'left_idx': idx,
                    'right_idx': candidate_idx
                })

    return results

Why: Spatial indexing reduces query complexity from O(n²) to O(n log n) for spatial operations. R-tree indexes enable fast bounding box queries before expensive geometric operations.

Raster Processing Pipelines

Scalable Raster Operations

import rasterio
import numpy as np
from rasterio.warp import calculate_default_transform, reproject, Resampling
import dask.array as da
from dask.distributed import Client

def process_large_raster(input_path, output_path, target_crs='EPSG:3857'):
    """
    Process large raster files using Dask for parallel processing
    """
    with rasterio.open(input_path) as src:
        # Calculate transform for target CRS
        transform, width, height = calculate_default_transform(
            src.crs, target_crs, src.width, src.height, *src.bounds
        )

        # Create output profile
        profile = src.profile.copy()
        profile.update({
            'crs': target_crs,
            'transform': transform,
            'width': width,
            'height': height
        })

        # Process in chunks
        with rasterio.open(output_path, 'w', **profile) as dst:
            for i in range(0, src.height, 1000):  # Process in 1000-pixel chunks
                window = rasterio.windows.Window(0, i, src.width, min(1000, src.height - i))

                # Read chunk
                data = src.read(window=window)

                # Process chunk (example: apply some transformation)
                processed_data = process_raster_chunk(data)

                # Write chunk
                dst.write(processed_data, window=window)

def process_raster_chunk(data):
    """
    Process a chunk of raster data
    """
    # Example: Apply NDVI calculation
    if data.shape[0] >= 2:  # Ensure we have at least 2 bands
        red = data[0].astype(np.float32)
        nir = data[1].astype(np.float32)

        # Avoid division by zero
        ndvi = np.where(
            (nir + red) != 0,
            (nir - red) / (nir + red),
            0
        )

        return np.clip(ndvi, -1, 1)

    return data

Why: Chunked processing enables handling of large raster datasets that exceed memory capacity. Parallel processing with Dask provides linear scalability across multiple cores and nodes.

Spatial Data Quality Assurance

Data Validation Pipeline

import geopandas as gpd
from shapely.geometry import Point, Polygon
import pandas as pd

def validate_spatial_data(gdf, validation_rules=None):
    """
    Comprehensive spatial data validation
    """
    if validation_rules is None:
        validation_rules = {
            'check_geometry_validity': True,
            'check_crs_consistency': True,
            'check_duplicate_geometries': True,
            'check_spatial_bounds': True
        }

    validation_results = {}

    # Check geometry validity
    if validation_rules['check_geometry_validity']:
        invalid_geometries = gdf[~gdf.geometry.is_valid]
        validation_results['invalid_geometries'] = len(invalid_geometries)

    # Check CRS consistency
    if validation_rules['check_crs_consistency']:
        validation_results['crs'] = str(gdf.crs)
        validation_results['crs_defined'] = gdf.crs is not None

    # Check for duplicate geometries
    if validation_rules['check_duplicate_geometries']:
        duplicate_count = gdf.geometry.duplicated().sum()
        validation_results['duplicate_geometries'] = duplicate_count

    # Check spatial bounds
    if validation_rules['check_spatial_bounds']:
        bounds = gdf.total_bounds
        validation_results['spatial_bounds'] = {
            'minx': bounds[0], 'miny': bounds[1],
            'maxx': bounds[2], 'maxy': bounds[3]
        }

    return validation_results

def clean_spatial_data(gdf):
    """
    Clean and standardize spatial data
    """
    # Remove invalid geometries
    gdf = gdf[gdf.geometry.is_valid]

    # Remove empty geometries
    gdf = gdf[~gdf.geometry.is_empty]

    # Fix invalid geometries
    gdf.geometry = gdf.geometry.buffer(0)

    # Remove duplicate geometries
    gdf = gdf.drop_duplicates(subset=['geometry'])

    return gdf

Why: Data validation ensures spatial data integrity before processing. Automated cleaning prevents downstream errors and improves analysis accuracy.

Performance Optimization

Memory-Efficient Spatial Operations

import geopandas as gpd
from shapely.geometry import Point
import numpy as np

def memory_efficient_spatial_join(left_gdf, right_gdf, how='inner', predicate='intersects'):
    """
    Memory-efficient spatial join for large datasets
    """
    # Process in chunks to manage memory
    chunk_size = 10000
    results = []

    for i in range(0, len(left_gdf), chunk_size):
        left_chunk = left_gdf.iloc[i:i+chunk_size]

        # Perform spatial join on chunk
        chunk_result = gpd.sjoin(left_chunk, right_gdf, how=how, predicate=predicate)
        results.append(chunk_result)

    # Combine results
    if results:
        return gpd.pd.concat(results, ignore_index=True)
    else:
        return gpd.GeoDataFrame()

def optimize_spatial_queries(gdf, common_filters=None):
    """
    Optimize spatial queries with pre-computed indices
    """
    if common_filters is None:
        common_filters = ['active', 'public']

    # Create partial indexes for common filters
    for filter_value in common_filters:
        if filter_value in gdf.columns:
            filtered_gdf = gdf[gdf[filter_value] == True]
            # Create spatial index for filtered data
            create_spatial_index(filtered_gdf, f'spatial_idx_{filter_value}')

    return gdf

Why: Memory-efficient operations enable processing of large spatial datasets without memory exhaustion. Pre-computed indices accelerate common queries.

Data Pipeline Integration

ETL Pipeline for Spatial Data

import geopandas as gpd
from sqlalchemy import create_engine
import pandas as pd

def spatial_etl_pipeline(source_config, target_config):
    """
    Complete ETL pipeline for spatial data
    """
    # Extract
    spatial_data = extract_spatial_data(source_config)

    # Transform
    transformed_data = transform_spatial_data(spatial_data)

    # Load
    load_spatial_data(transformed_data, target_config)

    return transformed_data

def extract_spatial_data(config):
    """
    Extract spatial data from various sources
    """
    if config['type'] == 'file':
        return gpd.read_file(config['path'])
    elif config['type'] == 'database':
        engine = create_engine(config['connection_string'])
        query = config['query']
        return gpd.read_postgis(query, engine, geom_col='geometry')
    elif config['type'] == 'api':
        # Implementation for API extraction
        pass

def transform_spatial_data(gdf):
    """
    Transform spatial data
    """
    # Standardize CRS
    gdf = gdf.to_crs('EPSG:4326')

    # Clean geometries
    gdf = clean_spatial_data(gdf)

    # Add computed fields
    gdf['area'] = gdf.geometry.area
    gdf['centroid'] = gdf.geometry.centroid

    return gdf

def load_spatial_data(gdf, config):
    """
    Load spatial data to target
    """
    if config['type'] == 'database':
        engine = create_engine(config['connection_string'])
        gdf.to_postgis(
            config['table_name'],
            engine,
            if_exists='replace',
            index=False
        )
    elif config['type'] == 'file':
        gdf.to_file(config['path'], driver=config['driver'])

Why: Standardized ETL pipelines ensure consistent data processing across different sources and targets. Modular design enables reuse and testing of individual components.