Skip to content

Quick Start

savana ships two independently-usable modules today. Pick the section for the one you need, nothing here requires the other.

Land-system classification

One-call classification

import savana

clf = savana.classify_landscape(
    aoi="path/to/my_area.geojson",   # or an EE asset ID, ee.Geometry, or geopandas GeoDataFrame
    epochs=[2019, 2021, 2024],
    park_name="My Study Area",
)

clf.show()                  # interactive map in Jupyter (geemap)
clf.accuracy_summary()      # pandas.DataFrame, one row per model (A/B/C/D)
clf.class_areas()           # pandas.DataFrame, area (km2) per class per epoch
clf.show_change()           # conservative + RUE-validated change map

clf.export(drive_folder="MyProject")   # push results to Google Drive

Using an Earth Engine parks database, filtered by name

clf = savana.classify_landscape(
    aoi="projects/ee-desmond/assets/NewParkMerged",
    name_filter="Kogyae",
    park_name="Kogyae",
    epochs=[2017, 2019, 2021, 2024],
)

Step-by-step control

If you want to inspect intermediate results (masks, thresholds, GCPs) instead of running the full pipeline in one call:

clf = savana.SavanaClassifier(
    aoi="path/to/my_area.geojson",
    epochs=[2019, 2021, 2024],
    park_name="My Study Area",
)

clf.build_features()          # composites, indices, RUE, thresholds, masks
clf.sample_training_points()  # unsupervised clustering + rule-based labels
clf.train()                   # 4-model ablation
clf.classify()                # multi-epoch classification
clf.analyse_change()          # conservative + RUE-validated change detection

clf.masks       # dict of ee.Image masks
clf.T           # dict of ee.Number adaptive thresholds
clf.gcps        # ee.FeatureCollection of ground control points
clf.models      # trained classifiers + confusion matrices

Precipitation product assessment

One-call validation

from savana.rainfall import validate_against_gpcc

# Reproduces the published West Africa study exactly: 16 GPCC stations,
# all 6 default products (CHIRPS, ERA5-Land, GPM IMERG, MERRA-2,
# PERSIANN-CDR, TerraClimate), 2001-2020.
result = validate_against_gpcc(ee_project="your-gcp-project-id")

print(result.summarize())
result.export_workbook("decision_tool.xlsx")

Your own stations, your own products, your own years, same call:

result = validate_against_gpcc(
    stations=[(-1.5, 12.4), (2.1, 6.5)],   # a single (lon, lat), a list of them,
                                             # a DataFrame, or a .geojson/.csv path
    products=["CHIRPS", "GPM_IMERG"],       # any subset of the default 6
    start_year=2015,
    end_year=2023,
    ee_project="your-gcp-project-id",
)
print(result.validation_overall_df)
print(result.answer("which product is best for drought early warning?"))

Step-by-step control, with previews before you commit

Every stage can be inspected before moving to the next, useful before running a full multi-year, multi-product assessment.

from savana.rainfall import RainfallAssessment

ra = RainfallAssessment(stations=(-1.5, 12.4), ee_project="your-gcp-project-id")

ra.preview_stations()                        # is this actually where you think it is?

ra.ingest(start="2020-01-01", end="2020-12-31")
ra.preview_map("CHIRPS")                      # one product's spatial pattern
ra.preview_map("CHIRPS", reference="GPM_IMERG")  # inter-product bias (never vs GPCC directly)

ra.get_observations(source="download")        # or source="ee_asset" for the built-in 16 stations
ra.preview_observations()                     # raw GPCC time series, sanity check
ra.preview_map("CHIRPS", show_gpcc=True)      # product raster + real GPCC points, same color scale

ra.extract().merge()
ra.compare_table()                            # GPCC vs every product, side by side, per station-month
ra.preview_comparison()                       # obs-vs-sim scatter, before any formal metric
ra.preview_station_bias("CHIRPS")             # per-station bias against real GPCC, on the map

ra.assign_zones()                             # optional, pooled validation if skipped
ra.validate().analyze_thresholds().score()

print(ra.summarize())
ra.show("recommendation_heatmap")
ra.export_workbook("decision_tool.xlsx")

Using your own ecological/climatic zones

from savana.rainfall import zones

# Option A: one custom AOI, no stratification needed
one_zone = zones.single_region_zone((-2.0, 5.0, 1.0, 8.0), zone_name="My Study Area")
ra.assign_zones(zones_gdf=one_zone)

# Option B: build real zones from your own base regions + latitude-band splits
zones_fc = zones.build_zones_from_bands(
    base_zones={"my_region": "projects/your-project/assets/your_region"},
    zone_defs=[
        {"zone_name": "North", "source_zone": "my_region", "lat_min": 5, "lat_max": 15},
        {"zone_name": "South", "source_zone": "my_region", "lat_min": -5, "lat_max": 5},
    ],
)
ra.assign_zones(zones_fc=zones_fc)

# Option C: do nothing -- .validate() runs pooled, using a documented
# latitude-band fallback for the zone column

Ask your results questions

One agent class, either module (or both at once):

from savana.agents import SavanaGeoAgent

agent = SavanaGeoAgent(clf, model="anthropic", model_id="claude-sonnet-4-6")
agent.ask("How much core woodland is there in 2024?")
agent.show_ui()   # live map + chat, inline in the notebook
agent = SavanaGeoAgent(rainfall=ra, model="anthropic")
agent.ask("Which product would you recommend for fire risk monitoring, and why?")
# both loaded at once -- one agent, one conversation, covers both result sets
agent = SavanaGeoAgent(clf=clf, rainfall=ra, model="anthropic")