bluegreencity-ki

Age Criterion

Overview

The age criterion evaluates drought risk based on tree age. It uses a two-stage approach: Stage 1 calculates age directly from the planting year; Stage 2 estimates age from stem circumference for trees where planting year is unknown. The criterion uses quantile-based normalization to map age to risk scores.

Implementation

Location

How It Works

  1. Stage 1 — Planting year: If pflanzjahr is available and valid → age = feature_reference_year - pflanzjahr The reference year is determined from project.feature_reference_year (if set) or falls back to run_date.year. This ensures that historical panel snapshots use the correct year instead of today.
  2. Stage 2 — Stem circumference fallback: If pflanzjahr is missing → estimate age from st_umfang using a piecewise linear function derived from the Stadt Hannover classification table (2024)
  3. Quantile Normalization: Maps age distribution to [0, 1] risk scores using quantile_minmax normalizer
  4. Neutral Handling: Assigns neutral score (default 0.5) to trees where neither source is available
  5. Source Tracking: Each tree gets an age_source tag ("pflanzjahr", "circumference", or "missing")

Stage 2: Circumference-to-Age Mapping

Based on the classification table from Stadt Hannover (Straßen- und Grünflächenamt, 2024):

Stem Circumference U [cm] Estimated Age [years] Description
U ≤ 130 40 Midpoint of class 31–50
130 < U ≤ 170 50 → 75 (linear) Interpolation between class boundaries
170 < U ≤ 200 75 → 100 (linear) Interpolation between class boundaries
U > 200 110 Presumably > 100 years
Missing / ≤ 0 NaN Not determinable

Linear regression approximation for U > 130 cm: Â(U) ≈ 0.71 · U − 42.1

Scoring Logic

Stage 1: Valid planting year → Age = current_year - planting_year
Stage 2: Missing planting year + valid circumference → piecewise estimated age
Both missing → Risk Score = 0.5 (neutral)

Age distribution → Quantile-based normalization to [0, 1]
  - Younger trees (age < p05) → Lower risk scores (~0.0-0.2)
  - Middle-aged trees (age between p05-p95) → Linear scaling
  - Older trees (age > p95) → Higher risk scores (~0.8-1.0)

Risk Interpretation:

Note: The current implementation treats older trees as higher risk within the present scoring model.

Configuration

Basic Configuration

criteria:
  - name: "age"
    weight: 0.2  # example; current calibrated weight in config/config.yaml
    params: {}

With Custom Normalizer Parameters

Gotcha: normalizer_params is read from the criterion entry level (next to weight), not from params:. Nesting it under params: is silently ignored and the code defaults apply.

criteria:
  - name: "age"
    weight: 0.2  # example; current calibrated weight in config/config.yaml
    normalizer_params:
      q_low: 0.05   # 5th percentile (younger boundary)
      q_high: 0.95  # 95th percentile (older boundary)
      clip: true    # Clip scores to [0, 1]
      invert: false # false = older → higher risk

Normalizer Parameter Details

The normalizer_params (entry level) are passed to the quantile_minmax() function:

Example: Conservative Scoring

Use wider quantile range to compress scores:

criteria:
  - name: "age"
    normalizer_params:
      q_low: 0.10
      q_high: 0.90

This makes the criterion less sensitive to extreme ages.

Example: Inverted Risk (Older = More Resilient)

criteria:
  - name: "age"
    normalizer_params:
      invert: true

This treats older trees as lower risk, suitable if your inventory primarily contains healthy mature trees.

Data Requirements

Required Fields

Field Format

Example Data

gisid pflanzjahr st_umfang age_value age_source age_risk age_reference_year
tree_001 2020 — 4 years pflanzjahr ~0.1 (low) 2024
tree_002 2000 — 24 years pflanzjahr ~0.5 (med) 2024
tree_003 None 150 62.5 years circumference ~0.8 (high) 2024
tree_004 None None N/A missing 0.5 (neutral) 2024

Output Fields:

Output Metadata

The criterion returns meta={"age_reference_year": …} (the year the age values were computed against). Summary statistics can also be derived from the output fields, for example:

{
    "age_min": 5,          # Youngest tree age
    "age_max": 74,         # Oldest tree age
    "age_mean": 28.3,      # Mean age
    "age_p05": 8,          # 5th percentile age
    "age_p95": 65,         # 95th percentile age
    "n_valid": 48150,      # Trees with valid planting years
    "n_invalid": 69        # Trees with missing/invalid years
}

Integration with Other Criteria

The age criterion complements:

Validation & Testing

Unit Tests

Run tests with:

conda activate bgcki_wesemann
python -m pytest tests/test_age.py -v

Tests cover:

Visual Validation

After running the pipeline, validate results by:

  1. Scatter plot: Plot age vs. risk score to verify linear scaling
  2. Histogram: Check age distribution and score distribution
  3. Outlier detection: Identify trees with unusual age/score combinations

Example Validation Code

import geopandas as gpd
import matplotlib.pyplot as plt
from datetime import date

# Load results
gdf = gpd.read_file("./data/output/<study_area>/trees_at_risk_<area>.gpkg", layer="trees_at_risk_points")

# Compute age for visualization
current_year = date.today().year
gdf['age'] = current_year - gdf['pflanzjahr']

# Plot age vs risk score
fig, ax = plt.subplots(1, 2, figsize=(12, 5))

# Scatter plot
ax[0].scatter(gdf['age_value'], gdf['age_risk'], alpha=0.5)
ax[0].set_xlabel('Tree Age (years)')
ax[0].set_ylabel('Age Risk Score')
ax[0].set_title('Age vs. Risk Score')
ax[0].grid(True, alpha=0.3)

# Histogram
ax[1].hist(gdf['age_value'].dropna(), bins=30, alpha=0.7, label='Age')
ax[1].set_xlabel('Tree Age (years)')
ax[1].set_ylabel('Frequency')
ax[1].set_title('Age Distribution')
ax[1].legend()

plt.tight_layout()
plt.show()

# Print summary statistics
print("Age Statistics:")
print(gdf['age_value'].describe())
print("\nScore Statistics:")
print(gdf['age_risk'].describe())

Known Limitations

  1. Linear risk assumption: Current implementation assumes older = higher risk, which may not capture U-shaped vulnerability (very young and very old)
  2. No species interaction: Age risk is independent of species-specific maturity rates
  3. Circumference estimation is coarse: The piecewise mapping uses city-wide averages; individual growth rates vary by species and site conditions
  4. Trees without both pflanzjahr and st_umfang: Still receive neutral score (0.5)

References

Changelog