LambdaLabTM
Artificial Intelligence · Class 12 Capstone · Subject Code 843
Sample CapstonePredictive analyticsSDG 13 · SDG 11⏱️ 20 min read

A Flood Early-Warning Model

A rare-event capstone: a next-day flood warning from rain, river and dam data, where a model that never warns would score 95% — so it is judged on recall and false alarms, validated monsoon by monsoon, and replayed on a new monsoon as feedback.

1Introduction

Every monsoon, some rivers rise past their danger level. Villages and towns on the banks often get little warning: the water rises overnight, after heavy rain far upstream or a sudden release from a dam.

Rain gauges, river gauges and dam reports already record what is happening upstream. The Central Water Commission issues flood forecasts for major rivers, but many smaller rivers and towns have no forecast of their own.

This capstone builds a next-day flood warning for one river gauge. Its difficulty is not the modelling but the rarity of floods: on most days nothing happens, so a model can look excellent while being useless. Following the Data Science Methodology, it is judged on the measures that matter for a warning — floods caught and false alarms raised.

13SDG 13
Climate Action
11SDG 11
Sustainable Cities and Communities
alignment to the goal

SDG target 13.1 asks every country to strengthen resilience to climate-related hazards, and target 11.5 to cut deaths and losses from disasters, especially for the poor. Floods are India's most frequent disaster, and a warning even a day ahead lets families move to safety, take cattle and documents, and lets the administration open shelters — the difference between a flood that destroys property and one that also takes lives.

Module 1From Problem to Approach

2Step 1 — Business Understanding

What is the problem that you are trying to solve? The handbook uses the 5W1H Problem Canvas for this step.

Who?
  • • Families in villages and towns along the river
  • • District disaster management authorities
  • • Gram panchayats who pass warnings on
What?
  • • Floods arrive with little or no warning
  • • Too many false alarms make people ignore real ones
Where?
  • • The river reach downstream of one gauge station
When?
  • • Each evening of the monsoon, for the next day
Why?
  • • A day's warning saves lives, livestock and documents
  • • Shelters and boats can be ready in time
How?
  • • Learn from ten monsoons of rain, river and dam data
  • • Estimate tomorrow's flood probability each evening, and warn above a threshold

Problem statement

Our the district disaster management authority have a problem of warning villages before the river crosses its danger level when floods are rare, follow heavy rain upstream or dam releases, and arrive overnight. An ideal solution would warn the evening before almost every flood, without so many false alarms that people stop listening.

Who would use it: District disaster management authorities and the gram panchayats of villages along the river, who issue and act on flood warnings.

Talking to the stakeholders

The handbook pairs the 5W1H canvas with Design Thinking, which begins by empathising with the people who have the problem. The questions asked, and what each answer changed:

Question askedWhat we learned, and what it changed
To a disaster management officer: how much warning do you need?One day is enough to move people and cattle and open shelters. That fixed the prediction target: a flood the next day.
To the same officer: which is worse — a missed flood or a false alarm?A missed flood, by far. But too many false alarms and villagers stop moving. Recall comes first, but false alarms must be counted and kept low.
To a river-gauge reader: what tells you the river will rise?Heavy rain upstream for two or three days, wet ground, and dam releases. Those became features — including three-day rain totals.
To the gauge reader: are your records complete?The local rain gauge sometimes fails; the upstream gauge rarely does. So a blank local reading is filled from the upstream gauge, not an average.
To a villager: what happened in the last flood?The river rose at night after rain in the hills where there is no gauge. A warning that part of the cause is never measured — the model cannot be perfect.
To a panchayat member: how should a warning reach people?SMS to the panchayat and an announcement. So deployment means a clear yes/no warning, not a percentage.

How it is handled today

Central Water Commission forecasts

Expert hydrological forecasts for major rivers at hundreds of stations. Many smaller rivers and towns are not covered.

Watching the gauge

A gauge reader raises the alarm when the river is close to danger. Reliable, but often only hours ahead — too late to move people at night.

Rainfall alerts

The meteorological department's heavy-rain warnings are valuable, but heavy rain does not always mean a flood at a given town.

Objectives

  1. To frame next-day flood warning with the 5W1H canvas
  2. To collect and understand ten monsoons of daily rain, river and dam data
  3. To show why accuracy is misleading for a rare event, and choose recall, precision and F1 instead
  4. To engineer features such as three-day rain totals and distance below the danger level
  5. To train Logistic Regression and a Random Forest with class weighting, test them on the two latest monsoons, and cross-validate them one monsoon at a time
  6. To deploy an evening warning, and replay a new monsoon as feedback

3Step 2 — Analytic Approach

DescriptiveDiagnosticPredictivePrescriptive

Whether the river will cross its danger level tomorrow is a yes/no prediction from today's data — predictive analytics with binary classification.

Because floods are rare, the dataset is unbalanced: about one day in twenty. The handbook warns that for unbalanced data accuracy is misleading and precision, recall or F1 should be used instead; this project is built around that warning.

CandidateWhy it was considered
Logistic Regression (balanced)Gives a flood probability and readable coefficients. class_weight="balanced" makes a missed flood day count as much as all the calm days together.
Random Forest (balanced)Captures interactions — heavy rain matters more when the ground is already wet — with the same class weighting.
Never warn (baseline)Not a model to use: the benchmark that shows how meaningless accuracy is here.
Module 2From Requirements to Collection

4Step 3 — Data Requirements

The data must cover many monsoons, because floods are rare: ten monsoons give a few dozen flood days, just enough to learn from. Each row is one monsoon day, with the measurements known that evening and whether the river crossed the danger level the next day.

Because the warning is issued the evening before, every feature must be available by evening: today's rain and river level, not tomorrow's.

Data neededTypeFormatSource
Local and upstream rainfallNumericmm per dayRain gauges (IMD / state)
River level and 24-hour riseNumericmetresRiver gauge station (CWC / state)
Soil wetnessNumeric0 to 1 indexEstimated from recent rain / remote sensing
Dam release upstreamNumericcumecsDam authority
Flood the next day (label)Binary1 if the level exceeds 104 m tomorrowDerived from the gauge record

5Step 4 — Data Collection

All of this is secondary data: river levels from the Central Water Commission or the state water resources department, rainfall from the India Meteorological Department and state rain gauges, and dam releases from the dam authority. Interviews with gauge readers and villagers are primary data, and shaped the features.

The sample file, river_gauge.csv, holds 1211 monsoon days from 2015 to 2024. monsoon_2025.csv holds the next monsoon and is used only for feedback.

As the villager warned, part of what raises the river is rain in hills with no gauge. The sample data includes that unmeasured rain, so the model faces the same limit a real one would.

Watch Out
This dataset is a LambdaLab sample, made by a program to behave like the real thing so the project can be run before any data is collected. It is not real data. Your group's capstone must use data you collected or a credited public dataset.
ColumnTypeWhat it holdsRole
datedateThe monsoon dayused for the year split
rain_mmdecimalRain at the town's gauge, mm; sometimes blankfeature
upstream_rain_mmdecimalRain at the upstream gauge, mmfeature
river_level_mdecimalRiver level today, m (danger level 104 m)feature
rise_24h_mdecimalChange in level over the last 24 hours, mfeature
soil_wetnessdecimalHow saturated the ground is, 0 to 1feature
dam_release_cumecsdecimalWater released by the upstream damfeature
rain_3day_mm, upstream_3day_mm, metres_below_dangerdecimalMade by the programengineered features
flood_next_day0 / 11 if the river crosses 104 m tomorrowtarget

river_gauge.csv — Ten monsoons of daily rain, river and dam data, with next-day flood labels. 1,211 rows, 50.6 KB. Download the file

monsoon_2025.csv — The 2025 monsoon, used only for feedback. 121 rows, 5.1 KB. Download the file

The first eight rows of river_gauge.csv

daterain_mmupstream_rain_mmriver_level_msoil_wetnessdam_release_cumecsrise_24h_mflood_next_day
2015-06-010.013.997.140.270.00.00
2015-06-020.00.097.710.2415.00.570
2015-06-030.00.096.60.220.0-1.110
2015-06-043.90.396.830.220.00.230
2015-06-0511.13.997.180.260.00.350
2015-06-060.04.798.380.230.01.20
2015-06-070.02.697.250.210.0-1.130
2015-06-0822.70.397.650.312.00.40

The first four rows of monsoon_2025.csv

daterain_mmupstream_rain_mmriver_level_msoil_wetnessdam_release_cumecsrise_24h_mflood_next_day
2025-06-010.019.597.770.2742.00.00
2025-06-0256.03.897.550.550.0-0.220
2025-06-030.034.798.160.50.00.610
2025-06-040.00.397.280.450.0-0.880
Module 3From Understanding to Preparation

6Step 5 — Data Understanding

Is the data collected representative of the problem to be solved?

The first finding is the imbalance: 60 of 1211 days were followed by a flood — 5.0 %. A model that never warned would be right on every calm day: 95.0 % accurate, and of no use to anyone (chart 2).

Chart 1 shows a single monsoon: the river spends most of the season well below danger and crosses it only in a few short spells. Chart 3 shows the flood days clustered where upstream rain is heavy and the river is already high — but also some floods after only moderate measured rain, where the unmeasured hill rain must have played a part.

1River level through the 2022 monsoon
Line chart
River level through the 2022 monsoon
how to read it

The river level each day of one monsoon, with the danger level dashed.

what it shows

Most of the season the river is metres below danger; it crosses only in short spells after heavy rain. A warning system is about catching those few days, not the many calm ones.

2An unbalanced dataset
Bar chart
An unbalanced dataset
how to read it

How many days were followed by no flood and by a flood.

what it shows

Calm days outnumber flood days about nineteen to one. This chart is the reason the project cannot be judged on accuracy.

3Upstream rain and today's river level
Scatter plot
Upstream rain and today's river level
how to read it

Each dot is a day: upstream rain across, today's river level up. Red dots were followed by a flood.

what it shows

Floods follow days with heavy upstream rain and a river that is already high. But a few red dots sit at moderate rain — floods driven partly by rain no gauge recorded. Those are the ones any model will struggle with.

7Step 6 — Data Preparation

A day entered twice
1

Removed by date, and the series sorted, so each day appears once and in order — essential for rolling totals.

Blank local rain readings
8 (from the upstream gauge)

Filled with the upstream gauge's reading for the same day, as the gauge reader suggested. On a stormy day, a seasonal average would badly understate the rain; the nearest gauge is a far better estimate.

Feature engineering

rain_3day_mm and upstream_3day_mm add up the last three days of rain, within each monsoon, because the gauge reader said floods follow two or three wet days, not one. Chart 4 confirms that three-day upstream rain separates flood and calm days well.

metres_below_danger is the gap between today's level and the 104 m danger mark — the same information as the level, but expressed as "how far there is to go", which is how the officer thinks about it.

With nine features, both models are trained.

4Upstream rain over the last three days
Box plot
Upstream rain over the last three days
how to read it

Three-day upstream rain for days followed by no flood and by a flood.

what it shows

Before floods, three-day upstream rain is several times higher. The engineered feature separates the two classes far better than one day's rain alone.

Module 4From Modelling to Evaluation

8Step 7 — Modelling

AI model→Learning based→Supervised learning→Classification

Each day has a known outcome — flood the next day or not — so this is supervised binary classification.

Because the warning will be used on future monsoons, the test set is the two most recent monsoons, 2023 and 2024 (242 flood days : 11 days), and the models train on 2015–2022 (968 flood days : 49 days).

How the models work

Both models use class_weight="balanced". Without it, a model trained on nineteen calm days for every flood day learns that saying "no flood" is almost always right. Weighting makes each flood day count nineteen times as much, so missing one is costly to the model.

Logistic Regression, with its inputs scaled, gives a probability of a flood tomorrow. The Random Forest of 300 trees votes; its probability is the share of trees voting "flood".

9Step 8 — Evaluation

On 2023–2024, Logistic Regression's counts were 9 2 2 229 (TP, FN, FP, TN) — recall 0.818, precision 0.818; the Random Forest's were 9 2 1 230 — recall 0.818, precision 0.9. Both have accuracy near 98%, but so would a model that never warned: accuracy says nothing here.

With only eleven flood days in the test years, the scores are fragile, so the models were also checked with leave-one-year-out cross validation: each monsoon is held out in turn, and predicted by a model trained on the other nine. Days from the same monsoon never sit on both sides of a split. Pooling every held-out prediction, Logistic Regression caught floods with recall 0.967 and precision 0.659; the Random Forest with recall 0.917 and precision 0.859.

That is the real choice. Logistic Regression misses fewer floods; the Random Forest raises fewer false alarms and has the higher F1. The officer was clear that a missed flood is worse, so Logistic Regression is deployed — while its false alarms are watched, because warning fatigue is a real danger.

Two monsoons had no floods at all. Recall is undefined for them — there is nothing to catch — and the program reports that instead of scoring them as 0, which would wrongly drag the average down.

5Confusion matrices, 2023–2024
Confusion matrix
Confusion matrices, 2023–2024
how to read it

For each model on the two test monsoons: TP (flood warned of), FN (flood missed), FP (false alarm) and TN (calm day, no warning).

what it shows

Both models catch the same number of floods; the Random Forest raises one false alarm fewer. With only 11 flood days in the test years, one day either way changes the scores a lot — which is why leave-one-year-out validation matters.

6Leave-one-year-out cross validation
Bar chart
Leave-one-year-out cross validation
how to read it

Recall in each held-out monsoon, with the number of flood days under each year. Two monsoons had no floods, so recall cannot be measured for them.

what it shows

Most years every flood is caught. 2024 had a single flood, and both models missed it — a flood driven by unmeasured hill rain. Recall in a year with one flood is either 0 or 1, so the pooled figures are the fairer summary.

7Warnings caught against false alarms
Line chart
Warnings caught against false alarms
how to read it

For Logistic Regression on 2023–2024: the share of floods warned of (red line) and the number of false alarms (blue bars) at each threshold.

what it shows

A low threshold catches more floods but raises more false alarms; a high one the reverse. At 0.5 the balance is good for these monsoons, and the officer's limit on false alarms is met.

Module 5From Deployment to Feedback

10Step 9 — Deployment

The chosen model is saved to flood_model.pkl. Each evening it would be given the day's readings; if tomorrow's flood probability is 0.5 or more, a warning is issued. The program shows two days: an ordinary monsoon day (no warning) and a day after three days of heavy rain with the river already high (warning).

As the panchayat member asked, the output is a clear yes/no warning sent by SMS to the panchayat and announced — with the probability kept for the officers.

8Feedback: the 2025 monsoon
Line chart
Feedback: the 2025 monsoon
how to read it

The river level through 2025, the danger level, and an orange marker on each evening a warning was issued for the next day.

what it shows

The warnings fall just before each crossing of the danger level. Replaying a monsoon the model never saw is the most convincing test a warning system can get before real use.

11Step 10 — Feedback

The 2025 monsoon is replayed evening by evening. It had 7 flood days; the model had warned of 7 of them the evening before, with 0 false alarms.

One good monsoon does not prove the system: 2024 showed that a flood driven by unmeasured hill rain can be missed entirely. The feedback plan is to record, after every monsoon, every flood missed and every false alarm, and to look for their causes — a missed flood after hill rain is the strongest argument for installing a new rain gauge in those hills.

Feedback from villagers matters equally: if warnings arrive too late at night, or people stop responding after false alarms, the threshold and the way warnings are delivered must change.

12Ethical considerations

Missed floods cost lives

Recall was put first because a missed flood can kill. The model's known blind spot — floods from unmeasured rain — must be told to the authorities, so they do not rely on it alone.

False alarms erode trust

Every false alarm makes the next warning less likely to be obeyed. Reporting false alarms alongside recall keeps that cost visible.

Reaching everyone

Warnings by SMS miss families without phones. The poorest households, often living closest to the river, must be reached by announcements and volunteers too.

Accountability

The model advises; the district authority decides and is accountable for warnings. It should never be the only source.

13Source code

The whole program, laid out as the ten steps of the methodology. Every chart and number on this page came out of running it.

flood_warning.py
# ---------------------------------------------------------------------------
# flood_warning.py
#
# Capstone project: a next-day flood warning for a river town, from today's
# rain, upstream rain, river level, soil wetness and dam releases.
#
# CBSE Class XII, Artificial Intelligence (843), Part C - Capstone Project.
# SDG 13 (Climate Action) and SDG 11 (Sustainable Cities and Communities).
#
# Data Science Methodology, 10 steps. CLASSIFICATION of a RARE event: floods
# happen on a few days a year, so accuracy is useless and the project is
# judged on recall, precision and F1. Two models - Logistic Regression and a
# Random Forest, both told to weight the rare class - are tested on the two
# latest monsoons, and checked with leave-one-year-out cross validation.
#
# Libraries: pandas, numpy, matplotlib, scikit-learn, joblib.
# ---------------------------------------------------------------------------

# %% Step 1 - Business Understanding
print("=" * 64)
print("A FLOOD EARLY-WARNING MODEL")
print("=" * 64)
print("Problem : the river crosses its danger level with little warning")
print("Goal    : warn a day ahead, so people and cattle can move in time")
print()

# %% Step 2 - Analytic Approach
print("Analytic approach : predictive - classification of a rare event")
print()

# %% Step 3 - Data Requirements
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import LeaveOneGroupOut
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score, accuracy_score
import joblib

pd.set_option("display.width", 140)
pd.set_option("display.max_columns", 20)
DANGER = 104.0

# %% Step 4 - Data Collection
data = pd.read_csv("river_gauge.csv", parse_dates=["date"])
print("Days collected :", len(data))
print("Monsoons       :", data["date"].dt.year.min(), "to", data["date"].dt.year.max())
print(data.head(6))
print()

# %% Step 5 - Data Understanding
print("--- Step 5: Data Understanding ---")
print("Missing values:")
print(data.isnull().sum().to_string())
print()
floods = data["flood_next_day"].sum()
print("Days followed by a flood :", floods, "of", len(data))
print("Share of flood days      :", round(data["flood_next_day"].mean() * 100, 1), "%")
print("Accuracy of a model that NEVER warns :", round((1 - data["flood_next_day"].mean()) * 100, 1), "%")
print()

# Chart 1: one monsoon, with the danger level.
one = data[data["date"].dt.year == 2022]
plt.figure(figsize=(10, 4))
plt.plot(one["date"], one["river_level_m"], color="#4e79a7")
plt.axhline(DANGER, color="#e15759", linestyle="--", label="Danger level (104 m)")
plt.title("River level through the 2022 monsoon")
plt.ylabel("River level (m)")
plt.legend()
plt.grid(True, linestyle="--", alpha=0.3)
plt.tight_layout()
plt.savefig("chart1_season_2022.png")
plt.close()

# Chart 2: how rare floods are.
plt.figure(figsize=(6, 4))
plt.bar(["No flood next day", "Flood next day"], data["flood_next_day"].value_counts().sort_index(),
        color=["#9c9c9c", "#e15759"])
plt.title("An unbalanced dataset")
plt.ylabel("Days")
plt.tight_layout()
plt.savefig("chart2_balance.png")
plt.close()

# Chart 3: upstream rain against today's river level.
calm = data[data["flood_next_day"] == 0]
flood = data[data["flood_next_day"] == 1]
plt.figure(figsize=(7, 5))
plt.scatter(calm["upstream_rain_mm"], calm["river_level_m"], s=10, alpha=0.4, color="#9c9c9c", label="No flood next day")
plt.scatter(flood["upstream_rain_mm"], flood["river_level_m"], s=30, color="#e15759", label="Flood next day")
plt.axhline(DANGER, color="#e15759", linestyle="--", linewidth=0.8)
plt.title("Upstream rain and today's river level")
plt.xlabel("Upstream rain today (mm)")
plt.ylabel("River level today (m)")
plt.legend()
plt.grid(True, linestyle="--", alpha=0.3)
plt.tight_layout()
plt.savefig("chart3_upstream.png")
plt.close()

# %% Step 6 - Data Preparation
print("--- Step 6: Data Preparation ---")
before = len(data)
data = data.drop_duplicates(subset="date").sort_values("date").reset_index(drop=True)
print("Duplicate days removed      :", before - len(data))
data["rain_mm"] = pd.to_numeric(data["rain_mm"])
blank = data["rain_mm"].isnull().sum()
# A blank local rain reading is filled with that day's upstream rain, the
# nearest gauge - better than a seasonal average on a stormy day.
data["rain_mm"] = data["rain_mm"].fillna(data["upstream_rain_mm"])
print("Blank rain readings filled  :", blank, "(from the upstream gauge)")

# Feature engineering: rain over the last three days, and the gap between
# today's level and the danger mark.
data["year"] = data["date"].dt.year
data["rain_3day_mm"] = data.groupby("year")["rain_mm"].transform(lambda r: r.rolling(3, min_periods=1).sum())
data["upstream_3day_mm"] = data.groupby("year")["upstream_rain_mm"].transform(lambda r: r.rolling(3, min_periods=1).sum())
data["metres_below_danger"] = DANGER - data["river_level_m"]
features = ["rain_mm", "upstream_rain_mm", "rain_3day_mm", "upstream_3day_mm", "river_level_m",
            "rise_24h_m", "metres_below_danger", "soil_wetness", "dam_release_cumecs"]
print("Features used               :", len(features))
print()

# Chart 4: three-day upstream rain for flood and calm days.
plt.figure(figsize=(6.5, 4.3))
plt.boxplot([data[data["flood_next_day"] == 0]["upstream_3day_mm"], data[data["flood_next_day"] == 1]["upstream_3day_mm"]],
            labels=["No flood next day", "Flood next day"])
plt.title("Upstream rain over the last three days")
plt.ylabel("mm")
plt.grid(True, axis="y", linestyle="--", alpha=0.3)
plt.tight_layout()
plt.savefig("chart4_upstream_3day.png")
plt.close()

# %% Step 7 - Modelling
print("--- Step 7: Modelling ---")
# A warning system is used on future monsoons, so the test is the two
# latest monsoons, 2023 and 2024 - never mixed into training.
train = data[data["year"] <= 2022]
test = data[data["year"] >= 2023]
X_train, y_train = train[features], train["flood_next_day"]
X_test, y_test = test[features], test["flood_next_day"]
print("Training days (2015-2022) :", len(train), " flood days :", y_train.sum())
print("Testing days (2023-2024)  :", len(test), " flood days :", y_test.sum())

# class_weight="balanced" tells each model that a missed flood day counts
# as much as all the calm days together - otherwise it learns to say "no".
logistic = make_pipeline(StandardScaler(), LogisticRegression(class_weight="balanced", max_iter=2000))
forest = RandomForestClassifier(n_estimators=300, min_samples_leaf=3, class_weight="balanced", random_state=0)
logistic.fit(X_train, y_train)
forest.fit(X_train, y_train)
print()

# %% Step 8 - Evaluation
print("--- Step 8: Evaluation ---")
results = {}
for name, model in [["Logistic Regression", logistic], ["Random Forest", forest]]:
    pred = model.predict(X_test)
    tn, fp, fn, tp = confusion_matrix(y_test, pred).ravel()
    results[name] = pred
    print(name)
    print("  " + name + " TP, FN, FP, TN :", tp, fn, fp, tn)
    print("  " + name + " accuracy  :", round(accuracy_score(y_test, pred), 3))
    print("  " + name + " precision :", round(precision_score(y_test, pred, zero_division=0), 3))
    print("  " + name + " recall    :", round(recall_score(y_test, pred), 3))
    print("  " + name + " F1        :", round(f1_score(y_test, pred), 3))
print()

# Leave-one-year-out cross validation: each monsoon in turn is held out and
# predicted by a model trained on all the other monsoons. Days from the same
# season never sit on both sides of the split.
groups = data["year"]
logo = LeaveOneGroupOut()
pooled = {"Logistic Regression": np.zeros(len(data), dtype=int), "Random Forest": np.zeros(len(data), dtype=int)}
year_recall = {"Logistic Regression": [], "Random Forest": []}
years = []
year_floods = []
for tr, te in logo.split(data[features], data["flood_next_day"], groups):
    years.append(int(groups.iloc[te[0]]))
    yt = data["flood_next_day"].iloc[te]
    year_floods.append(int(yt.sum()))
    for name in pooled:
        if name == "Logistic Regression":
            m = make_pipeline(StandardScaler(), LogisticRegression(class_weight="balanced", max_iter=2000))
        else:
            m = RandomForestClassifier(n_estimators=300, min_samples_leaf=3, class_weight="balanced", random_state=0)
        m.fit(data[features].iloc[tr], data["flood_next_day"].iloc[tr])
        p = m.predict(data[features].iloc[te])
        pooled[name][te] = p
        # Recall is undefined for a monsoon with no floods: nothing to catch.
        year_recall[name].append(recall_score(yt, p) if yt.sum() > 0 else np.nan)
print("Leave-one-year-out recall for each held-out monsoon:")
print("  Year  Floods  Logistic  Forest")
for i in range(len(years)):
    if year_floods[i] == 0:
        print("  ", years[i], "    0     no floods to catch")
    else:
        print("  ", years[i], str(year_floods[i]).rjust(5), "   ", round(year_recall["Logistic Regression"][i], 2),
              "    ", round(year_recall["Random Forest"][i], 2))
# Pooled: every day's held-out prediction, from the model that never saw its monsoon.
y_all = data["flood_next_day"]
for name in pooled:
    print(name, "- pooled recall    :", round(recall_score(y_all, pooled[name]), 3))
    print(name, "- pooled precision :", round(precision_score(y_all, pooled[name]), 3))
    print(name, "- pooled F1        :", round(f1_score(y_all, pooled[name]), 3))
print()

# Chart 5: confusion matrices on 2023-2024.
plt.figure(figsize=(10, 4.3))
for k, name in enumerate(["Logistic Regression", "Random Forest"]):
    cm = confusion_matrix(y_test, results[name], labels=[1, 0])
    plt.subplot(1, 2, k + 1)
    plt.imshow(cm, cmap="Reds")
    labels = [["TP", "FN"], ["FP", "TN"]]
    for r in range(2):
        for c in range(2):
            plt.text(c, r, labels[r][c] + "\n" + str(cm[r][c]), ha="center", va="center", fontsize=12,
                     color="white" if cm[r][c] > cm.max() / 2 else "black")
    plt.xticks([0, 1], ["Warned", "Not warned"])
    plt.yticks([0, 1], ["Flood", "No flood"])
    plt.title(name)
plt.tight_layout()
plt.savefig("chart5_confusion.png")
plt.close()

# Chart 6: recall in each held-out year (years with no floods left blank).
x = np.arange(len(years))
plt.figure(figsize=(9, 4))
plt.bar(x - 0.2, np.nan_to_num(year_recall["Logistic Regression"]), width=0.4, color="#4e79a7", label="Logistic Regression")
plt.bar(x + 0.2, np.nan_to_num(year_recall["Random Forest"]), width=0.4, color="#59a14f", label="Random Forest")
for i in range(len(years)):
    if year_floods[i] == 0:
        plt.text(x[i], 0.04, "no\nfloods", ha="center", fontsize=8)
plt.xticks(x, [str(y) + "\n(" + str(f) + ")" for y, f in zip(years, year_floods)])
plt.ylim(0, 1.05)
plt.ylabel("Recall (floods warned of)")
plt.xlabel("Held-out monsoon (flood days)")
plt.title("Leave-one-year-out cross validation")
plt.legend()
plt.tight_layout()
plt.savefig("chart6_by_year.png")
plt.close()

# The threshold: Logistic Regression's probability, and how many floods
# are caught - and false alarms raised - at each level.
prob = logistic.predict_proba(X_test)[:, 1]
thresholds = np.round(np.arange(0.1, 0.91, 0.1), 1)
recalls = []
alarms = []
for t in thresholds:
    warn = prob >= t
    recalls.append(recall_score(y_test, warn))
    alarms.append(int((warn & (y_test == 0)).sum()))
chosen = 0.5
print("Threshold used                     :", chosen)
print("False alarms in two monsoons at 0.5 :", alarms[list(thresholds).index(chosen)])
print()

# Chart 7: floods caught and false alarms at each threshold.
fig, ax1 = plt.subplots(figsize=(8, 4.3))
ax1.plot(thresholds, recalls, marker="o", color="#e15759", label="Floods warned of (recall)")
ax1.set_ylabel("Recall")
ax1.set_ylim(0, 1.05)
ax1.set_xlabel("Probability needed to issue a warning")
ax2 = ax1.twinx()
ax2.bar(thresholds, alarms, width=0.05, alpha=0.35, color="#4e79a7", label="False alarms")
ax2.set_ylabel("False alarms in 2023-2024")
ax1.set_title("Warnings caught against false alarms")
fig.legend(loc="upper right", bbox_to_anchor=(0.88, 0.88))
fig.tight_layout()
plt.savefig("chart7_threshold.png")
plt.close()

# %% Step 9 - Deployment
print("--- Step 9: Deployment ---")
joblib.dump(logistic, "flood_model.pkl")
model = joblib.load("flood_model.pkl")
today = pd.DataFrame({
    "rain_mm": [12.0, 85.0], "upstream_rain_mm": [20.0, 140.0], "rain_3day_mm": [30.0, 190.0],
    "upstream_3day_mm": [45.0, 260.0], "river_level_m": [98.4, 102.9], "rise_24h_m": [0.1, 1.4],
    "metres_below_danger": [5.6, 1.1], "soil_wetness": [0.45, 0.95], "dam_release_cumecs": [0, 900],
})
p_today = model.predict_proba(today)[:, 1]
for i, label in enumerate(["An ordinary monsoon day", "After three days of heavy rain"]):
    action = "ISSUE FLOOD WARNING for tomorrow" if p_today[i] >= chosen else "no warning"
    print(label.ljust(32), "probability", round(p_today[i], 3), "->", action)
print()

# %% Step 10 - Feedback
print("--- Step 10: Feedback ---")
# The 2025 monsoon arrives. Each evening the model issues (or does not
# issue) a warning for the next day; afterwards the warnings are checked.
s25 = pd.read_csv("monsoon_2025.csv", parse_dates=["date"])
s25["year"] = 2025
s25["rain_3day_mm"] = s25["rain_mm"].rolling(3, min_periods=1).sum()
s25["upstream_3day_mm"] = s25["upstream_rain_mm"].rolling(3, min_periods=1).sum()
s25["metres_below_danger"] = DANGER - s25["river_level_m"]
warn25 = model.predict_proba(s25[features])[:, 1] >= chosen
flood25 = s25["flood_next_day"] == 1
print("Flood days in the 2025 monsoon      :", int(flood25.sum()))
print("Of these, warned the evening before :", int((warn25 & flood25).sum()))
print("False alarms in 2025                :", int((warn25 & ~flood25).sum()))
print("Recall in 2025                      :", round(recall_score(flood25, warn25), 3))

# Chart 8: the 2025 monsoon, warnings and floods.
plt.figure(figsize=(10, 4))
plt.plot(s25["date"], s25["river_level_m"], color="#4e79a7", label="River level")
plt.axhline(DANGER, color="#e15759", linestyle="--", label="Danger level")
warned_days = s25["date"][warn25]
plt.scatter(warned_days, [DANGER + 1.2] * len(warned_days), marker="v", color="#f28e2b", s=40, label="Warning issued for next day")
plt.title("Feedback: the 2025 monsoon")
plt.ylabel("River level (m)")
plt.legend(loc="upper left")
plt.grid(True, linestyle="--", alpha=0.3)
plt.tight_layout()
plt.savefig("chart8_monsoon_2025.png")
plt.close()

The library calls it is built from

CallStepWhat it does
pd.read_csv(parse_dates=["date"])Step 4Reads the daily records with real dates
value_counts() / mean()Step 5Measures how rare floods are
fillna(data["upstream_rain_mm"])Step 6Fills a blank local reading from the upstream gauge
groupby("year")[...].transform(rolling(3).sum())Step 6Three-day rain totals within each monsoon
LogisticRegression(class_weight="balanced")Step 7Weights the rare flood days
RandomForestClassifier(class_weight="balanced")Step 7The forest, with the same weighting
LeaveOneGroupOut() with groups = yearStep 8Leave-one-year-out cross validation
precision_score / recall_score / f1_scoreStep 8The metrics that matter for a rare event
predict_proba() >= thresholdSteps 8–10Turns a probability into a warning
joblib.dump() / load()Step 9Saves and reloads the model

14Output of a real run

Output
================================================================
A FLOOD EARLY-WARNING MODEL
================================================================
Problem : the river crosses its danger level with little warning
Goal    : warn a day ahead, so people and cattle can move in time

Analytic approach : predictive - classification of a rare event

Days collected : 1211
Monsoons       : 2015 to 2024
        date  rain_mm  upstream_rain_mm  river_level_m  soil_wetness  dam_release_cumecs  rise_24h_m  flood_next_day
0 2015-06-01      0.0              13.9          97.14          0.27                 0.0        0.00               0
1 2015-06-02      0.0               0.0          97.71          0.24                15.0        0.57               0
2 2015-06-03      0.0               0.0          96.60          0.22                 0.0       -1.11               0
3 2015-06-04      3.9               0.3          96.83          0.22                 0.0        0.23               0
4 2015-06-05     11.1               3.9          97.18          0.26                 0.0        0.35               0
5 2015-06-06      0.0               4.7          98.38          0.23                 0.0        1.20               0

--- Step 5: Data Understanding ---
Missing values:
date                  0
rain_mm               8
upstream_rain_mm      0
river_level_m         0
soil_wetness          0
dam_release_cumecs    0
rise_24h_m            0
flood_next_day        0

Days followed by a flood : 60 of 1211
Share of flood days      : 5.0 %
Accuracy of a model that NEVER warns : 95.0 %

--- Step 6: Data Preparation ---
Duplicate days removed      : 1
Blank rain readings filled  : 8 (from the upstream gauge)
Features used               : 9

--- Step 7: Modelling ---
Training days (2015-2022) : 968  flood days : 49
Testing days (2023-2024)  : 242  flood days : 11

--- Step 8: Evaluation ---
Logistic Regression
  Logistic Regression TP, FN, FP, TN : 9 2 2 229
  Logistic Regression accuracy  : 0.983
  Logistic Regression precision : 0.818
  Logistic Regression recall    : 0.818
  Logistic Regression F1        : 0.818
Random Forest
  Random Forest TP, FN, FP, TN : 9 2 1 230
  Random Forest accuracy  : 0.988
  Random Forest precision : 0.9
  Random Forest recall    : 0.818
  Random Forest F1        : 0.857

Leave-one-year-out recall for each held-out monsoon:
  Year  Floods  Logistic  Forest
   2015     6     1.0      1.0
   2016     0     no floods to catch
   2017     0     no floods to catch
   2018    16     1.0      0.88
   2019     5     1.0      1.0
   2020     4     1.0      1.0
   2021    10     1.0      0.9
   2022     8     1.0      1.0
   2023    10     0.9      0.9
   2024     1     0.0      0.0
Logistic Regression - pooled recall    : 0.967
Logistic Regression - pooled precision : 0.659
Logistic Regression - pooled F1        : 0.784
Random Forest - pooled recall    : 0.917
Random Forest - pooled precision : 0.859
Random Forest - pooled F1        : 0.887

Threshold used                     : 0.5
False alarms in two monsoons at 0.5 : 2

--- Step 9: Deployment ---
An ordinary monsoon day          probability 0.0 -> no warning
After three days of heavy rain   probability 0.995 -> ISSUE FLOOD WARNING for tomorrow

--- Step 10: Feedback ---
Flood days in the 2025 monsoon      : 7
Of these, warned the evening before : 7
False alarms in 2025                : 0
Recall in 2025                      : 1.0

15The same project in Orange

The same classifiers and class balancing are available in Orange's widgets. The tool is Orange Data Mining.

WidgetWhat it does
FileLoad river_gauge.csv; set flood_next_day as the target (categorical).
Feature ConstructorAdd the three-day rain totals and metres below danger.
Select RowsSplit by year: 2015–2022 for training, 2023–2024 for testing.
Logistic Regression and Random ForestIn each, tick the option to balance the class distribution.
Test and ScoreTest on test data; read precision, recall and F1 for class 1 — not just CA.
Confusion MatrixCount floods missed and false alarms.

16Testing

Every case was run: each gives the whole program a small input of its own.

Test caseExpectedActualResult
Ten monsoons of daysDays collected : 1211Days collected : 1211Pass
A model that never warns would look 95% accurateAccuracy of a model that NEVER warns : 95.0 %Accuracy of a model that NEVER warns : 95.0 %Pass
A monsoon with no floods is reported, not scored as 0 recall2016 0 no floods to catch2016 0 no floods to catchPass
Pooled across all held-out years, Logistic Regression misses few floodsLogistic Regression - pooled recall : 0.967Logistic Regression - pooled recall : 0.967Pass
Blank rain readings are filled from the upstream gaugeBlank rain readings filled : 8 (from the upstream gauge)Blank rain readings filled : 8 (from the upstream gauge)Pass
Floods that always follow very wet days: every flood warned ofLogistic Regression recall : 1.0Logistic Regression recall : 1.0Pass
Three blank rain readings are filled, none droppedBlank rain readings filled : 3 (from the upstream gauge)Blank rain readings filled : 3 (from the upstream gauge)Pass
The data file is missingFileNotFoundError: [Errno 2] No such file or directory: 'river_gauge.csv'FileNotFoundError: [Errno 2] No such file or directory: 'river_gauge.csv'Pass

17Team roles and timeline

A capstone is done by a group of 3 to 5. A suggested split of the work, and a timeline across the year:

RoleResponsible for
Project leadBusiness understanding; interviews with officers, gauge readers and villagers
Data leadRiver, rain and dam data; joining them by date
AnalystData understanding; the imbalance analysis; engineered features
ModellerThe balanced models, leave-one-year-out validation and the threshold
Documentation and outreachThe report, the 2025 replay, how warnings reach people, and the video
WhenMethodology stepsDeliverable
April – May1 Business Understanding, 2 Analytic ApproachProblem statement; interviews before the monsoon
June – July3 Data Requirements, 4 Data CollectionTen monsoons of data collected
August5 Data Understanding, 6 Data PreparationImbalance measured; features engineered
September – October7 Modelling, 8 EvaluationModels compared year by year
November9 Deployment, 10 FeedbackThe latest monsoon replayed
December – JanuaryDocumentation and videoReport and 3-minute video

18Results, limits and what next

what the project found
  • Floods follow 5.0 % of monsoon days; a model that never warns would be 95.0 % accurate and useless.
  • Pooled over ten held-out monsoons, Logistic Regression caught floods with recall 0.967, the Random Forest with 0.917 but fewer false alarms.
  • Three-day upstream rain, a high river and wet ground are the warning signs.
  • Floods driven by rain in ungauged hills can be missed entirely — the 2024 flood was.
  • Replayed on the 2025 monsoon, the model warned of 7 of 7 floods the evening before.

Limitations

  • Sample data for one imaginary gauge
  • Few flood days — a few dozen in ten years — so every score is uncertain
  • Cannot see rain that no gauge records
  • One day ahead only, at one station

Future scope

  • Add rainfall estimates from satellites or radar to cover ungauged hills
  • Forecast two or three days ahead, with the meteorological department's rain forecast as a feature
  • Extend to several gauge stations along the river
  • Design how warnings reach households without phones

What the team had to learn

  • Why accuracy misleads on unbalanced data, and what to use instead
  • class_weight="balanced" in scikit-learn
  • Leave-one-group-out cross validation, and why recall is undefined when there are no positives
  • Rolling-window features with pandas
  • Disaster warning systems and how they reach people

19Conclusion

The capstone built a next-day flood warning that, across ten held-out monsoons, catches almost every flood. Its most important lesson came before any model: for a rare event, a 95%-accurate model can be worthless, and the methodology's evaluation step must use recall, precision and F1.

Choosing between the models was a choice between two kinds of harm — missed floods and false alarms. Putting the officer's priorities into the decision, not just the numbers, is what turns a model into a warning system.

The model's limit is clear: rain that no gauge measures. The 2024 flood it missed is the best argument for better data — a new rain gauge in the hills would do more for the warning than any change to the model.

20The 3-minute video script

Part C asks for a video of exactly three minutes with six parts. A script, timed to the second:

TimePartWhat to say and show
0:00 – 0:25a. Problem statementShow a flooded village. "Rivers rise overnight, and many towns get no warning. A day's notice saves lives."
0:25 – 0:40b. SDG alignmentSDG 13 and SDG 11 icons. "Resilience to climate disasters, and fewer lives lost to them."
0:40 – 1:15c. AI concepts and algorithms"Predictive classification of a rare event. Logistic Regression and a Random Forest with balanced class weights, judged on recall and false alarms, validated one monsoon at a time."
1:15 – 2:15d. How it worksChart 2 — 'a model that never warns is 95% accurate'. Then the features, the year-by-year validation, and the 2025 replay with warnings before each flood.
2:15 – 2:45e. Conclusion"Almost every flood warned of a day ahead — except floods from rain no gauge measures. Better data is the next step."
2:45 – 3:00f. Acknowledgement"We thank our AI teacher, [name], and the officers and villagers who spoke to us."

21Questions for the viva

▸Why is accuracy useless for your project?

Floods happen on about 5% of days, so a model that never warns is 95% accurate but catches no floods.

▸What does class_weight="balanced" do?

It makes each flood day count as much as many calm days, so the model is penalised heavily for missing floods.

▸What is leave-one-year-out cross validation?

Each monsoon is held out in turn and predicted by a model trained on the other monsoons, so days from the same season never appear on both sides.

▸Why did you pool the predictions instead of averaging recall per year?

Two years had no floods, so recall was undefined, and a year with one flood gives recall of only 0 or 1. Pooling every held-out prediction gives a fairer figure.

▸Why did you choose Logistic Regression over the Random Forest?

It missed fewer floods (higher recall). The forest had fewer false alarms, but a missed flood is the worse error.

▸Why did the model miss the 2024 flood?

It was caused partly by rain in hills with no gauge, which the model never sees.

▸Why fill blank rain readings from the upstream gauge?

On a stormy day the nearest gauge is a much better estimate than a seasonal average.

▸Which SDGs does it align to?

SDG 13, Climate Action (target 13.1), and SDG 11, Sustainable Cities and Communities (target 11.5).

22References

  • Central Water Commission, Government of India — flood forecasting — https://cwc.gov.in/
  • India Meteorological Department — rainfall and heavy-rain warnings — https://mausam.imd.gov.in/
  • National Disaster Management Authority — Guidelines on Management of Floods — https://ndma.gov.in/
  • scikit-learn: class_weight and LeaveOneGroupOut — https://scikit-learn.org/stable/
  • CBSE, Artificial Intelligence (Subject Code 843), Class XII curriculum, 2026-27 — Part C
  • CBSE, Artificial Intelligence Student Handbook, Class XII — Unit 2, Data Science Methodology
  • Rollins, J. B. (2015). Foundational Methodology for Data Science. IBM Analytics.
  • scikit-learn — https://scikit-learn.org/stable/; pandas, NumPy and Matplotlib documentation
  • LambdaLab — https://www.lambdalab.in