Skill Tracer
Skills › Scientific Agent Skills › geomaster › specialized-topics.md

specialized-topics.md

330 lines · skills/geomaster/references/specialized-topics.md at 5f2bc03

Open on GitHub
1# Specialized Topics
2
3Geostatistics, optimization, privacy and provenance. Reviewed 2026-10-01. Data-dependent fragments remain illustrative; small numerical functions are exercised by the recipe suite. All Euclidean coordinates below require a suitable metric CRS.
4

Specialized Topics

Geostatistics, optimization, privacy and provenance. Reviewed 2026-10-01. Data-dependent fragments remain illustrative; small numerical functions are exercised by the recipe suite. All Euclidean coordinates below require a suitable metric CRS.

Raw lines 1 to 4
5## Geostatistics
6

Geostatistics

Raw lines 5 to 6
7### Variogram Analysis
8
9```python
10import numpy as np
11from scipy.spatial.distance import pdist, squareform
12import matplotlib.pyplot as plt
14def empirical_variogram(points, values, max_lag=None, n_lags=15):
15 """
16 Calculate empirical variogram.
17 """
18 points = np.asarray(points, dtype=float)
19 values = np.asarray(values, dtype=float)
20 if points.ndim != 2 or values.shape != (len(points),) or len(points) < 2:
21 raise ValueError('At least two points with one value each are required')
22 if not np.isfinite(points).all() or not np.isfinite(values).all():
23 raise ValueError('Inputs must be finite')
24 distances = pdist(points) # unique unordered pairs; no self-pairs
25 differences = pdist(values[:, None], metric='sqeuclidean')
26 max_lag = distances.max() / 2 if max_lag is None else max_lag
27 if not np.isfinite(max_lag) or max_lag <= 0 or n_lags < 1:
28 raise ValueError('Positive finite maximum lag and positive lag count required')
29 edges = np.linspace(0, max_lag, n_lags + 1)
30 centers, semivariance = [], []
31 for i, (low, high) in enumerate(zip(edges[:-1], edges[1:])):
32 keep = (distances >= low) & ((distances <= high) if i == n_lags - 1 else (distances < high))
33 if keep.any():
34 centers.append(distances[keep].mean())
35 semivariance.append(0.5 * differences[keep].mean())
36 return np.asarray(centers), np.asarray(semivariance)
38# Fit variogram model
39def fit_variogram_model(lags, gammas, model='spherical'):
40 """
41 Fit theoretical variogram model.
42 """
43 from scipy.optimize import curve_fit
45 def spherical(h, nugget, sill, range_):
46 """Spherical model; sill argument is partial sill, total sill=nugget+sill."""
47 h = np.asarray(h)
48 gamma = np.where(h < range_,
49 nugget + sill * (1.5 * h/range_ - 0.5 * (h/range_)**3),
50 nugget + sill)
51 return gamma
53 def exponential(h, nugget, sill, range_):
54 """Exponential model."""
55 return nugget + sill * (1 - np.exp(-3 * h / range_))
57 def gaussian(h, nugget, sill, range_):
58 """Gaussian model."""
59 return nugget + sill * (1 - np.exp(-3 * (h/range_)**2))
61 models = {
62 'spherical': spherical,
63 'exponential': exponential,
64 'gaussian': gaussian
65 }
67 # Fit model
68 popt, _ = curve_fit(models[model], lags, gammas,
69 p0=[np.min(gammas), np.max(gammas), np.max(lags)/2],
70 bounds=([0, 0, 1e-12], [np.inf, np.inf, np.inf]))
72 return popt, models[model]
73```

Variogram Analysis

import numpy as np
from scipy.spatial.distance import pdist, squareform
import matplotlib.pyplot as plt

def empirical_variogram(points, values, max_lag=None, n_lags=15):
    """
    Calculate empirical variogram.
    """
    points = np.asarray(points, dtype=float)
    values = np.asarray(values, dtype=float)
    if points.ndim != 2 or values.shape != (len(points),) or len(points) < 2:
        raise ValueError('At least two points with one value each are required')
    if not np.isfinite(points).all() or not np.isfinite(values).all():
        raise ValueError('Inputs must be finite')
    distances = pdist(points)  # unique unordered pairs; no self-pairs
    differences = pdist(values[:, None], metric='sqeuclidean')
    max_lag = distances.max() / 2 if max_lag is None else max_lag
    if not np.isfinite(max_lag) or max_lag <= 0 or n_lags < 1:
        raise ValueError('Positive finite maximum lag and positive lag count required')
    edges = np.linspace(0, max_lag, n_lags + 1)
    centers, semivariance = [], []
    for i, (low, high) in enumerate(zip(edges[:-1], edges[1:])):
        keep = (distances >= low) & ((distances <= high) if i == n_lags - 1 else (distances < high))
        if keep.any():
            centers.append(distances[keep].mean())
            semivariance.append(0.5 * differences[keep].mean())
    return np.asarray(centers), np.asarray(semivariance)

# Fit variogram model
def fit_variogram_model(lags, gammas, model='spherical'):
    """
    Fit theoretical variogram model.
    """
    from scipy.optimize import curve_fit

    def spherical(h, nugget, sill, range_):
        """Spherical model; sill argument is partial sill, total sill=nugget+sill."""
        h = np.asarray(h)
        gamma = np.where(h < range_,
                        nugget + sill * (1.5 * h/range_ - 0.5 * (h/range_)**3),
                        nugget + sill)
        return gamma

    def exponential(h, nugget, sill, range_):
        """Exponential model."""
        return nugget + sill * (1 - np.exp(-3 * h / range_))

    def gaussian(h, nugget, sill, range_):
        """Gaussian model."""
        return nugget + sill * (1 - np.exp(-3 * (h/range_)**2))

    models = {
        'spherical': spherical,
        'exponential': exponential,
        'gaussian': gaussian
    }

    # Fit model
    popt, _ = curve_fit(models[model], lags, gammas,
                        p0=[np.min(gammas), np.max(gammas), np.max(lags)/2],
                        bounds=([0, 0, 1e-12], [np.inf, np.inf, np.inf]))

    return popt, models[model]
Raw lines 7 to 74
75### Kriging Interpolation
77```python
78from pykrige.ok import OrdinaryKriging
79import numpy as np
81def ordinary_kriging(x, y, z, grid_resolution=100):
82 """
83 Perform ordinary kriging interpolation.
84 """
85 # Create grid
86 gridx = np.linspace(x.min(), x.max(), grid_resolution)
87 gridy = np.linspace(y.min(), y.max(), grid_resolution)
89 # Fit variogram
90 OK = OrdinaryKriging(
91 x, y, z,
92 variogram_model='spherical',
93 verbose=False,
94 enable_plotting=False,
95 coordinates_type='euclidean',
96 )
98 # Interpolate
99 zinterp, sigmasq = OK.execute('grid', gridx, gridy)
101 return zinterp, sigmasq, gridx, gridy
103# Cross-validation
104def kriging_cross_validation(x, y, z, groups, n_folds=5):
105 """
106 Perform k-fold cross-validation for kriging.
107 """
108 from sklearn.model_selection import GroupKFold
110 kf = GroupKFold(n_splits=n_folds)
111 errors = []
113 for train_idx, test_idx in kf.split(z, groups=groups):
114 # Train
115 OK = OrdinaryKriging(
116 x[train_idx], y[train_idx], z[train_idx],
117 variogram_model='spherical',
118 verbose=False
119 )
121 # Predict at test locations
122 predictions, _ = OK.execute('points',
123 x[test_idx], y[test_idx])
125 # Calculate error
126 rmse = np.sqrt(np.mean((predictions - z[test_idx])**2))
127 errors.append(rmse)
129 return np.mean(errors), np.std(errors)
130```

Kriging Interpolation

from pykrige.ok import OrdinaryKriging
import numpy as np

def ordinary_kriging(x, y, z, grid_resolution=100):
    """
    Perform ordinary kriging interpolation.
    """
    # Create grid
    gridx = np.linspace(x.min(), x.max(), grid_resolution)
    gridy = np.linspace(y.min(), y.max(), grid_resolution)

    # Fit variogram
    OK = OrdinaryKriging(
        x, y, z,
        variogram_model='spherical',
        verbose=False,
        enable_plotting=False,
        coordinates_type='euclidean',
    )

    # Interpolate
    zinterp, sigmasq = OK.execute('grid', gridx, gridy)

    return zinterp, sigmasq, gridx, gridy

# Cross-validation
def kriging_cross_validation(x, y, z, groups, n_folds=5):
    """
    Perform k-fold cross-validation for kriging.
    """
    from sklearn.model_selection import GroupKFold

    kf = GroupKFold(n_splits=n_folds)
    errors = []

    for train_idx, test_idx in kf.split(z, groups=groups):
        # Train
        OK = OrdinaryKriging(
            x[train_idx], y[train_idx], z[train_idx],
            variogram_model='spherical',
            verbose=False
        )

        # Predict at test locations
        predictions, _ = OK.execute('points',
                                    x[test_idx], y[test_idx])

        # Calculate error
        rmse = np.sqrt(np.mean((predictions - z[test_idx])**2))
        errors.append(rmse)

    return np.mean(errors), np.std(errors)
Raw lines 75 to 131
132## Spatial Optimization

Spatial Optimization

Raw lines 132 to 133
134### Location-allocation: explicit p-median formulation
136SLSQP over thresholded fractional variables is not a binary facility solver. For a
137small candidate set, use binary open-site variables `y_j` and assignment variables
138`x_ij`, minimize `sum(demand_i * cost_ij * x_ij)`, and constrain:
140- `sum_j x_ij = 1` for every demand point;
141- `x_ij <= y_j`;
142- `sum_j y_j = p`;
143- all variables in {0,1}.
145Use `scipy.optimize.milp` with `integrality=1`, `Bounds(0, 1)` and explicit
146`LinearConstraint` objects, then require solver success and verify feasibility,
147objective and chosen site count. Costs may be network travel times, not straight-line
148distance; unreachable pairs must be prohibited. Weighted demand needs explicit units.
149KMeans/snapping is a heuristic and can select duplicate sites.

Location-allocation: explicit p-median formulation

SLSQP over thresholded fractional variables is not a binary facility solver. For a small candidate set, use binary open-site variables y_j and assignment variables x_ij, minimize sum(demand_i * cost_ij * x_ij), and constrain:

  • sum_j x_ij = 1 for every demand point;
  • x_ij <= y_j;
  • sum_j y_j = p;
  • all variables in {0,1}.

Use scipy.optimize.milp with integrality=1, Bounds(0, 1) and explicit LinearConstraint objects, then require solver success and verify feasibility, objective and chosen site count. Costs may be network travel times, not straight-line distance; unreachable pairs must be prohibited. Weighted demand needs explicit units. KMeans/snapping is a heuristic and can select duplicate sites.

Raw lines 134 to 150
151### Routing optimization
153For a small connected undirected road graph, build a shortest-path metric closure
154before a TSP heuristic. `G[current][candidate]` assumes a direct edge and fails on
155ordinary sparse road graphs. NetworkX `traveling_salesman_problem(G, nodes=stops,
156weight='length', cycle=True)` computes a closure and expands the tour back onto
157original graph paths. Report it as approximate; repeated intermediate nodes are
158normal. Directed routing needs strong reachability and a compatible heuristic.
160Vehicle routing additionally constrains capacity, depot starts/ends, customer demand,
161service time, time windows and fleet. Cluster-first routes that never inspect a
162`capacity` argument are not a capacity-constrained VRP solution. Use an appropriate
163routing optimizer and verify each route against the constraints afterward.

Routing optimization

For a small connected undirected road graph, build a shortest-path metric closure before a TSP heuristic. G[current][candidate] assumes a direct edge and fails on ordinary sparse road graphs. NetworkX traveling_salesman_problem(G, nodes=stops, weight='length', cycle=True) computes a closure and expands the tour back onto original graph paths. Report it as approximate; repeated intermediate nodes are normal. Directed routing needs strong reachability and a compatible heuristic.

Vehicle routing additionally constrains capacity, depot starts/ends, customer demand, service time, time windows and fleet. Cluster-first routes that never inspect a capacity argument are not a capacity-constrained VRP solution. Use an appropriate routing optimizer and verify each route against the constraints afterward.

Raw lines 151 to 164
165## Ethics and Privacy

Ethics and Privacy

Raw lines 165 to 166
167### Privacy-preserving geospatial analysis
169Independent metre-scale noise added directly to longitude/latitude is dimensionally
170wrong and does not establish a privacy guarantee. Define protected units, adjacency,
171release mechanism, sensitivity and composition before claiming differential privacy.
172Use a reviewed mechanism appropriate to location data and a threat model; geographic
173projection/extent and side information matter. For descriptive maps, aggregate to
174justified cells and suppress sparse groups, while reporting that aggregation alone
175does not prove anonymity.
177Line simplification is not k-anonymity: it does not create k indistinguishable people
178or protect sensitive places. Audit re-identification risk and consent/access controls;
179preserve sensitive raw trajectories separately from permitted outputs.

Privacy-preserving geospatial analysis

Independent metre-scale noise added directly to longitude/latitude is dimensionally wrong and does not establish a privacy guarantee. Define protected units, adjacency, release mechanism, sensitivity and composition before claiming differential privacy. Use a reviewed mechanism appropriate to location data and a threat model; geographic projection/extent and side information matter. For descriptive maps, aggregate to justified cells and suppress sparse groups, while reporting that aggregation alone does not prove anonymity.

Line simplification is not k-anonymity: it does not create k indistinguishable people or protect sensitive places. Audit re-identification risk and consent/access controls; preserve sensitive raw trajectories separately from permitted outputs.

Raw lines 167 to 180
181### Data Provenance
183```python
184# Track geospatial data lineage
185import pandas as pd
187class DataLineage:
188 def __init__(self):
189 self.history = []
191 def record_transformation(self, input_data, operation, output_data, params):
192 """Record data transformation."""
193 record = {
194 'timestamp': pd.Timestamp.now(),
195 'input': input_data,
196 'operation': operation,
197 'output': output_data,
198 'parameters': params
199 }
200 self.history.append(record)
202 def get_lineage(self, data_id):
203 """Traverse recorded edges without infinite recursion on cycles."""
204 lineage, seen, pending = [], set(), [data_id]
205 while pending:
206 current = pending.pop()
207 if current in seen:
208 continue
209 seen.add(current)
210 for record in self.history:
211 if record['output'] == current:
212 lineage.append(record)
213 pending.append(record['input'])
214 return lineage
215```

Data Provenance

# Track geospatial data lineage
import pandas as pd

class DataLineage:
    def __init__(self):
        self.history = []

    def record_transformation(self, input_data, operation, output_data, params):
        """Record data transformation."""
        record = {
            'timestamp': pd.Timestamp.now(),
            'input': input_data,
            'operation': operation,
            'output': output_data,
            'parameters': params
        }
        self.history.append(record)

    def get_lineage(self, data_id):
        """Traverse recorded edges without infinite recursion on cycles."""
        lineage, seen, pending = [], set(), [data_id]
        while pending:
            current = pending.pop()
            if current in seen:
                continue
            seen.add(current)
            for record in self.history:
                if record['output'] == current:
                    lineage.append(record)
                    pending.append(record['input'])
        return lineage
Raw lines 181 to 216
217## Best Practices

Best Practices

Raw lines 217 to 218
219### Reproducible Research
221```python
222# Use environment.yml for dependencies
223# environment.yml:
224"""
225name: geomaster
226dependencies:
227 - python=3.13
228 - geopandas
229 - rasterio
230 - scikit-learn
231 - pip
232 - pip:
233 - torchgeo
234"""
236# Capture session info
237def capture_environment():
238 """Capture software and data versions."""
239 import platform
240 import geopandas as gpd
241 import rasterio
242 import numpy as np
243 import pandas as pd
245 info = {
246 'os': platform.platform(),
247 'python': platform.python_version(),
248 'geopandas': gpd.__version__,
249 'rasterio': rasterio.__version__,
250 'numpy': np.__version__,
251 'pandas': pd.__version__,
252 'timestamp': pd.Timestamp.now()
253 }
255 return info
257# Save with output
258import json
259with open('processing_info.json', 'w') as f:
260 json.dump(capture_environment(), f, indent=2, default=str)
261```

Reproducible Research

# Use environment.yml for dependencies
# environment.yml:
"""
name: geomaster
dependencies:
  - python=3.13
  - geopandas
  - rasterio
  - scikit-learn
  - pip
  - pip:
    - torchgeo
"""

# Capture session info
def capture_environment():
    """Capture software and data versions."""
    import platform
    import geopandas as gpd
    import rasterio
    import numpy as np
    import pandas as pd

    info = {
        'os': platform.platform(),
        'python': platform.python_version(),
        'geopandas': gpd.__version__,
        'rasterio': rasterio.__version__,
        'numpy': np.__version__,
        'pandas': pd.__version__,
        'timestamp': pd.Timestamp.now()
    }

    return info

# Save with output
import json
with open('processing_info.json', 'w') as f:
    json.dump(capture_environment(), f, indent=2, default=str)
Raw lines 219 to 262
263### Code Organization
265```python
266# Project structure
267"""
268project/
269├── data/
270│ ├── raw/
271│ ├── processed/
272│ └── external/
273├── notebooks/
274├── src/
275│ ├── __init__.py
276│ ├── data_loading.py
277│ ├── preprocessing.py
278│ ├── analysis.py
279│ └── visualization.py
280├── tests/
281├── config.yaml
282└── README.md
283"""
285# Configuration management
286import yaml
288with open('config.yaml') as f:
289 config = yaml.safe_load(f)
291# Access parameters
292crs = config['projection']['output_crs']
293resolution = config['data']['resolution']
294```

Code Organization

# Project structure
"""
project/
├── data/
│   ├── raw/
│   ├── processed/
│   └── external/
├── notebooks/
├── src/
│   ├── __init__.py
│   ├── data_loading.py
│   ├── preprocessing.py
│   ├── analysis.py
│   └── visualization.py
├── tests/
├── config.yaml
└── README.md
"""

# Configuration management
import yaml

with open('config.yaml') as f:
    config = yaml.safe_load(f)

# Access parameters
crs = config['projection']['output_crs']
resolution = config['data']['resolution']
Raw lines 263 to 295
296### Performance Optimization
298```python
299# Memory profiling
300import memory_profiler
302@memory_profiler.profile
303def process_large_dataset(data_path):
304 """Profile memory usage."""
305 data = load_data(data_path)
306 result = process(data)
307 return result
309# Vectorization vs loops
310# BAD: Iterating rows
311for idx, row in gdf.iterrows():
312 gdf.loc[idx, 'buffer'] = row.geometry.buffer(100)
314# GOOD: Vectorized
315gdf['buffer'] = gdf.geometry.buffer(100)
317# Chunked processing
318def process_in_chunks(gdf, func, chunk_size=1000):
319 """Process GeoDataFrame in chunks."""
320 results = []
321 for i in range(0, len(gdf), chunk_size):
322 chunk = gdf.iloc[i:i+chunk_size]
323 result = func(chunk)
324 results.append(result)
325 return pd.concat(results)
326```
328For more code examples, see [code-examples.md](code-examples.md).
330Sources: [SciPy MILP](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.milp.html), [PyKrige](https://geostat-framework.readthedocs.io/projects/pykrige/en/stable/generated/pykrige.ok.OrdinaryKriging.html), [NetworkX TSP](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.approximation.traveling_salesman.traveling_salesman_problem.html), [GroupKFold](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GroupKFold.html).

Performance Optimization

# Memory profiling
import memory_profiler

@memory_profiler.profile
def process_large_dataset(data_path):
    """Profile memory usage."""
    data = load_data(data_path)
    result = process(data)
    return result

# Vectorization vs loops
# BAD: Iterating rows
for idx, row in gdf.iterrows():
    gdf.loc[idx, 'buffer'] = row.geometry.buffer(100)

# GOOD: Vectorized
gdf['buffer'] = gdf.geometry.buffer(100)

# Chunked processing
def process_in_chunks(gdf, func, chunk_size=1000):
    """Process GeoDataFrame in chunks."""
    results = []
    for i in range(0, len(gdf), chunk_size):
        chunk = gdf.iloc[i:i+chunk_size]
        result = func(chunk)
        results.append(result)
    return pd.concat(results)

For more code examples, see code-examples.md.

Sources: SciPy MILP, PyKrige, NetworkX TSP, GroupKFold.

Raw lines 296 to 330