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.
bluegreencity_ki/criteria/age.pytests/test_age.pypflanzjahr 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.pflanzjahr is missing → estimate age from st_umfang using a piecewise linear function derived from the Stadt Hannover classification table (2024)quantile_minmax normalizerage_source tag ("pflanzjahr", "circumference", or "missing")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
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.
criteria:
- name: "age"
weight: 0.2 # example; current calibrated weight in config/config.yaml
params: {}
Gotcha:
normalizer_paramsis read from the criterion entry level (next toweight), not fromparams:. Nesting it underparams: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
The normalizer_params (entry level) are passed to the quantile_minmax() function:
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.
criteria:
- name: "age"
normalizer_params:
invert: true
This treats older trees as lower risk, suitable if your inventory primarily contains healthy mature trees.
pflanzjahr in schema): Year when tree was planted (Stage 1)st_umfang in schema): Trunk circumference in cm (Stage 2 fallback)None/NaN/0/negative → triggers Stage 2None/NaN/0/negative → not determinable| 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:
age_risk: Normalized risk score (0-1)age_value: Raw tree age in years (from either stage)age_source: Origin of the age value ("pflanzjahr", "circumference", or "missing")age_reference_year: The year used for the age calculation (equals project.feature_reference_year when set, otherwise run_date.year). Useful for tracing historical panel snapshots.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
}
The age criterion complements:
Run tests with:
conda activate bgcki_wesemann
python -m pytest tests/test_age.py -v
Tests cover:
After running the pipeline, validate results by:
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())
age_source tracking. Removed unused species_factors.csv. Added 12 new tests (total: 21).