The vitality criterion evaluates drought risk based on tree vitality stages assessed during field surveys. Lower vitality indicates reduced physiological capacity to cope with water stress. The criterion implements a watering exclusion logic where dead or severely declining trees (stages 4-5) receive zero risk, as they should not be prioritized for irrigation.
bluegreencity_ki/criteria/vitality.pytests/test_vitality.pyVitality Stage 1 (excellent) → Risk Score = 1/3 ≈ 0.33 (low-medium risk)
Vitality Stage 2 (good) → Risk Score = 2/3 ≈ 0.67 (medium-high risk)
Vitality Stage 3 (moderate) → Risk Score = 3/3 = 1.00 (high risk)
Vitality Stage 4 (poor/declining) → Risk Score = 0.0 (watering exclusion)
Vitality Stage 5 (dead) → Risk Score = 0.0 (watering exclusion)
Vitality Stage 0 (no data) → Risk Score = 0.5 (neutral, configurable)
Missing/invalid data → Risk Score = 0.5 (neutral)
The criterion uses a standard 1-5 vitality classification:
| Stage | Description | Tree Condition | vitality_risk | Watering Priority |
|---|---|---|---|---|
| 1 | Excellent vitality | Healthy, vigorous growth | 0.33 | Medium |
| 2 | Good vitality | Healthy, normal growth | 0.67 | High |
| 3 | Moderate/Fair | Signs of stress, reduced vigor | 1.00 | Highest |
| 4 | Poor/Declining | Severe stress, dieback | 0.00 | Exclude |
| 5 | Dead/Dying | Dead or near-dead | 0.00 | Exclude |
| 0 | Not assessed | No vitality data | 0.50 | Neutral |
Output Fields:
vitality_risk: Normalized risk score (0-1)vitality_value: Raw vitality stage (0-5)Key Design Decision: Stages 4 and 5 receive risk score 0.0, meaning they are excluded from watering prioritization. This reflects practical irrigation management:
The exclusion ensures irrigation resources are directed to trees that can benefit (stages 1-3).
Note: Risk score 0.0 does NOT mean “low risk” in this context—it means “excluded from consideration.” The final aggregated risk score will also be low, effectively removing these trees from priority lists.
criteria:
- name: "vitality"
weight: 0.3 # example; current calibrated weight in config/config.yaml
{
"stage_counts": {1: 12500, 2: 18200, 3: 14300, 4: 2800, 5: 420},
"n_excluded": 3220, # Trees in stages 4-5
"n_neutral": 69, # Trees with stage 0 or missing
"n_scored": 45000, # Trees with stages 1-3
"exclude_stages": [4, 5], # Configuration used
"stage0_score": 0.5 # Configuration used
}
The vitality criterion complements:
When vitality = 0.0 (stages 4-5), the final weighted risk score will be reduced:
final_risk = w1*age + w2*vitality + w3*street_type + ...
= w1*age + w2*0.0 + w3*street_type + ...
This effectively downweights excluded trees in the final prioritization.
Run tests with:
conda activate bgcki_wesemann
python -m pytest tests/test_vitality.py -v
Tests cover:
After running the pipeline, validate results by:
import geopandas as gpd
import matplotlib.pyplot as plt
import pandas as pd
# Load results
gdf = gpd.read_file("./data/output/<study_area>/trees_at_risk_<area>.gpkg", layer="trees_at_risk_points")
# Vitality stage distribution
fig, ax = plt.subplots(1, 3, figsize=(15, 5))
# Stage distribution
stage_counts = gdf['vitalitaet'].value_counts().sort_index()
ax[0].bar(stage_counts.index, stage_counts.values)
ax[0].set_xlabel('Vitality Stage')
ax[0].set_ylabel('Count')
ax[0].set_title('Vitality Stage Distribution')
ax[0].set_xticks([0, 1, 2, 3, 4, 5])
# Score distribution
ax[1].hist(gdf['vitality_risk'], bins=20, alpha=0.7)
ax[1].set_xlabel('Vitality Risk Score')
ax[1].set_ylabel('Frequency')
ax[1].set_title('Vitality Score Distribution')
# Stage vs Score scatter
ax[2].scatter(gdf['vitality_value'], gdf['vitality_risk'], alpha=0.3)
ax[2].set_xlabel('Vitality Stage')
ax[2].set_ylabel('Risk Score')
ax[2].set_title('Stage vs. Risk Score')
ax[2].set_xticks([0, 1, 2, 3, 4, 5])
ax[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Verify exclusion logic
excluded = gdf[gdf['vitality_value'].isin([4, 5])]
print(f"Excluded trees (stages 4-5): {len(excluded)}")
print(f"Mean score for excluded trees: {excluded['vitality_risk'].mean():.3f} (should be 0.0)")
# Verify stage mapping
for stage in [1, 2, 3]:
stage_trees = gdf[gdf['vitality_value'] == stage]
expected_score = stage / 3.0
actual_mean = stage_trees['vitality_risk'].mean()
print(f"Stage {stage}: expected {expected_score:.3f}, actual {actual_mean:.3f}")