bluegreencity-ki

Vitality Criterion

Overview

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.

Implementation

Location

How It Works

  1. Stage Validation: Parses vitality field and converts to numeric (handling invalid values)
  2. Stage Mapping: Maps vitality stages to risk scores based on classification system
  3. Exclusion Logic: Applies watering exclusion for dead/degenerating trees (stages 4-5)
  4. Neutral Handling: Assigns configurable score for stage 0 or missing data

Scoring Logic

Vitality 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)

Vitality Stage System

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:

Watering Exclusion Semantics

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.

Configuration

Basic Configuration

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
}

Integration with Other Criteria

The vitality criterion complements:

Interaction with Final Risk Score

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.

Validation & Testing

Unit Tests

Run tests with:

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

Tests cover:

Visual Validation

After running the pipeline, validate results by:

  1. Stage distribution: Plot histogram of vitality stages
  2. Score distribution: Verify scores match expected mapping
  3. Exclusion check: Confirm stages 4-5 have risk score 0.0
  4. Spatial patterns: Map vitality scores to identify stressed areas

Example Validation Code

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}")

Known Limitations

  1. Subjective assessment: Vitality depends on field assessor training and consistency
  2. Temporal lag: Vitality assessments may be outdated if inventory is not regularly updated
  3. Binary exclusion: Stage 4 trees receive same treatment as dead trees (stage 5), which may be too aggressive
  4. No watering exclusion reporting: Excluded trees are silently set to 0.0; no separate output flag indicates exclusion

References

Changelog