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

How Much Will This Field Yield?

A capstone in regression: predicting a plot's harvest from its crop, weather, soil and fertiliser — Linear Regression against a Random Forest, validated with K-fold, and checked again after a drought season.

1Introduction

Before a harvest, nobody knows exactly how much a field will produce. A farmer decides how much fertiliser to buy, a cooperative books storage space, and a district office estimates how much grain will reach the mandi — all on guesswork.

The guesswork has a cost. Fertiliser is often applied far beyond what the crop can use, because more is assumed to be better. Storage is booked too small or too large. Procurement plans are made on last year's numbers.

Yet the things that decide a yield are mostly known well before harvest: the crop, the season's rainfall and temperature, the soil's pH and nitrogen, how much fertiliser went in, and whether the plot is irrigated. This capstone asks whether a model can turn those into a reliable yield prediction — and uses the Data Science Methodology, step by step, to find out.

2SDG 2
Zero Hunger
12SDG 12
Responsible Consumption and Production
alignment to the goal

SDG 2 aims to double the productivity of small farmers, and SDG 12 to use inputs such as fertiliser responsibly. A yield forecast does both: it tells a farmer what the plot is likely to produce, and it shows how much each extra kilogram of fertiliser actually adds — which, past a point, is very little. That is the evidence a farmer needs to stop over-applying fertiliser that costs money and runs off into rivers.

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?
  • • Small farmers deciding inputs
  • • Farmer producer organisations planning storage and sales
  • • District agriculture officers estimating production
What?
  • • Yield is unknown until harvest
  • • Fertiliser is over-applied because its real effect is not measured
  • • Storage and procurement plans are made on guesses
Where?
  • • Rice, wheat and maize plots in one agro-climatic region
  • • Both irrigated and rain-fed land
When?
  • • Mid-season, when weather and inputs are known but the harvest is not
  • • Before deciding a second dose of fertiliser
Why?
  • • Better input decisions save money and protect soil and water
  • • Earlier, better production estimates help procurement and prices
How?
  • • Learn the relationship between plot conditions and past yields
  • • Predict yield for new plots and show which factors matter most

Problem statement

Our farmers and district agriculture officers have a problem of not knowing a plot's yield before harvest when they decide fertiliser, storage and procurement during the season. An ideal solution would predict each plot's yield from its crop, weather, soil and inputs, accurately enough to plan with, and show which factors drive it.

Who would use it: Farmers and farmer producer organisations planning inputs and storage; agriculture department officers estimating district production before the harvest.

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 farmers: when do you decide how much fertiliser to apply?Mostly at sowing and again about a month later. That fixed 'when' in the canvas: the prediction is useful mid-season, so it can only use information known by then — not the harvest weather.
To farmers: how do you know whether extra fertiliser was worth it?Most did not know; a good harvest was credited to the fertiliser even in a good-rain year. This made separating the effect of fertiliser from rainfall a goal of the project, not a side result.
To farmers: do you keep records of what you applied?Many keep input bills but not per-plot notes. The data requirement became 'fertiliser per plot, from bills', and the survey was designed around bills.
To an agriculture officer: how is district production estimated now?From crop-cutting experiments at harvest, plus last year's figures for early estimates. That gave the 'existing system' to beat: last year's average.
To an agriculture officer: how accurate would a forecast need to be?Within about half a tonne per hectare would be useful for planning. This became the yardstick for the evaluation step — the RMSE had to come in under 0.5 t/ha.
To a cooperative manager: what would you do with a yield forecast?Book storage and transport earlier. That confirmed the deployment idea: a per-plot forecast that can be added up for a whole village.
To everyone: what would make you distrust the model?A single number with no explanation. So the report shows the error range and which factors matter most, not just the prediction.

How it is handled today

Crop-cutting experiments

The official method: a small area of sample fields is harvested and weighed. Accurate, but it happens at harvest — too late for decisions during the season — and covers only sample fields.

Last year's average

Easy, and often used for planning. It ignores this season's rainfall, this plot's soil and how much fertiliser went in.

The farmer's experience

Valuable and specific to the land, but it cannot easily separate the effect of fertiliser from the effect of a good monsoon — which is exactly what decides whether extra fertiliser was worth it.

Objectives

  1. To frame the yield problem with the 5W1H canvas and choose an analytic approach
  2. To specify, collect and understand a dataset of plots with their conditions and yields
  3. To prepare the data: remove duplicates and impossible values, fill missing rainfall sensibly, and encode text columns
  4. To train Linear Regression and a Random Forest, and compare them with a train-test split and 5-fold cross validation using MAE, MSE, RMSE and R²
  5. To deploy the better model as a saved file that predicts yield for new plots
  6. To use the next season's harvest as feedback, and decide whether the model needs retraining

3Step 2 — Analytic Approach

DescriptiveDiagnosticPredictivePrescriptive

The question is "how much will this plot yield?" — a number to be forecast from past patterns. In the handbook's four types of analytics, that is predictive analytics. Because the target is a continuous number, not a category, the technique is regression.

The project also has a descriptive part (summarising past yields in Step 5) and a prescriptive hint at the end: the feature importances and the fertiliser coefficient suggest where inputs can be reduced without losing yield.

CandidateWhy it was considered
Linear RegressionThe model the handbook teaches. Simple, fast and explainable: each feature gets one coefficient. Its weakness is that it assumes every effect is a straight line.
Random Forest RegressorMany decision trees averaged together. It can follow curved effects — such as fertiliser helping less and less — and interactions, at the cost of being harder to explain.
Classification (rejected)Grouping yields into low / medium / high would throw away information. The farmer needs a number of tonnes, so regression is the right family.
Module 2From Requirements to Collection

4Step 3 — Data Requirements

The analytic approach decides the data. A regression model needs many past plots, each with its conditions and the yield actually harvested — that yield is the target. The data must be structured (a table), with numbers for the measurements and a small set of text categories for crop and irrigation.

Using the 5W1H questions again: the data must cover all three crops, irrigated and rain-fed land, and a realistic spread of rainfall and fertiliser — otherwise the model cannot learn what happens at the edges.

Data neededTypeFormatSource
Crop grownCategoricalText: Rice / Wheat / MaizeFarmer records
Season rainfallNumericMillimetresIndia Meteorological Department, district data
Mean temperatureNumeric°CIndia Meteorological Department
Soil pH and nitrogenNumericpH units; kg/haSoil Health Card
Fertiliser appliedNumerickg/haFarmer records, input purchase bills
IrrigationCategoricalYes / NoFarmer records
Yield harvested (target)NumericTonnes per hectareWeighed at harvest

5Step 4 — Data Collection

The handbook distinguishes primary data, collected first-hand, from secondary data that already exists. A real version of this project would combine both: yields and fertiliser use collected from farmers through a survey (primary), and rainfall, temperature and soil values taken from the India Meteorological Department and Soil Health Cards (secondary).

This sample dataset, crop_yield.csv, holds 904 plots. A second file, next_season.csv, holds 120 plots from the following season and is kept aside for the Feedback step.

For your own capstone, public data exists to start from: the Directorate of Economics and Statistics publishes district crop yields, IMD publishes rainfall, and data.gov.in hosts many agricultural datasets. Combining them by district and year is itself a worthwhile data preparation exercise — and must be credited.

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
plot_idtextAn ID for the plot — no farmer's namenot used
croptextRice, Wheat or Maizefeature (one-hot encoded)
rainfall_mmdecimalRainfall in the growing season, mmfeature
temperature_cdecimalMean temperature in the season, °Cfeature
soil_phdecimalSoil pH from the Soil Health Cardfeature
nitrogen_kg_hadecimalSoil nitrogen, kg/hafeature
fertiliser_kg_hadecimalFertiliser applied, kg/hafeature
irrigatedYes / NoWhether the plot is irrigatedfeature (1 / 0)
yield_t_hadecimalYield harvested, tonnes per hectaretarget

crop_yield.csv — One season of plots, with their conditions and harvested yield. It contains the mistakes real survey data contains. 904 rows, 41.6 KB. Download the file

next_season.csv — The following season's plots — a drought year — used only in the Feedback step. 120 rows, 5.6 KB. Download the file

The first eight rows of crop_yield.csv

plot_idcroprainfall_mmtemperature_csoil_phnitrogen_kg_hafertiliser_kg_hairrigatedyield_t_ha
P10001Maize759.026.36.184.0121.0No4.79
P10002Rice1448.023.66.472.099.0No4.49
P10003Rice1278.028.65.975.053.0Yes4.24
P10004Rice1139.027.37.672.0122.0Yes5.34
P10005Rice1245.026.36.3127.0116.0Yes5.39
P10006Wheat797.023.16.554.095.0No4.83
P10006Wheat797.023.16.554.095.0No4.83
P10007Maize624.028.56.878.071.0Yes3.47

The first five rows of next_season.csv

plot_idcroprainfall_mmtemperature_csoil_phnitrogen_kg_hafertiliser_kg_hairrigatedyield_t_ha
N20001Rice906.026.96.396.091.0No4.84
N20002Rice770.026.97.0108.0190.0No5.67
N20003Rice585.028.36.772.0109.0Yes5.32
N20004Wheat353.016.07.189.0126.0Yes4.8
N20005Wheat417.012.45.868.0129.0Yes3.87
Module 3From Understanding to Preparation

6Step 5 — Data Understanding

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

The handbook's question for this step is whether the data represents the problem. Three checks were made: are there missing or impossible values, is each crop and condition covered, and do the features relate to yield at all?

The missing-value count shows blank rainfall in a few rows; the summary statistics show a maximum yield of 42 t/ha and a minimum of −1 — both impossible, since even the best rice in India yields well under 10 t/ha. Those are found here and fixed in the next step.

Univariate analysis (charts 1 and 3) and a scatter plot (chart 2) show what the data looks like, and pairwise correlation (chart 4, after preparation) measures how each feature moves with yield.

1Distribution of yield
Histogram
Distribution of yield
how to read it

Each bar counts the plots whose yield falls in that range. Values above 10 are drawn at 10 so the one impossible value of 42 does not squash the chart.

what it shows

Most plots yield between 3 and 6 t/ha, in a single hump — what a healthy yield distribution looks like. The thin bar at the far right is the impossible 42, drawn at 10. A histogram is the quickest way to spot such values before they mislead a model.

2Fertiliser used and yield
Scatter plot
Fertiliser used and yield
how to read it

Each dot is a plot: fertiliser across, yield up.

what it shows

Yield rises with fertiliser at first and then flattens: going from 0 to 100 kg/ha adds much more than going from 150 to 250. That curve is the most important thing in the dataset, for two reasons. It is the evidence that heavy fertiliser use is wasteful, and it tells us a straight-line model will struggle — which is why a Random Forest is tried as well.

3Yield of each crop
Box plot
Yield of each crop
how to read it

One box per crop. The line inside the box is the median, the box holds the middle half of the plots, and the whiskers reach the rest; dots beyond them are unusual plots.

what it shows

Rice yields the most (median 4.91 t/ha), then wheat (4.48 t/ha) and maize (4.24 t/ha). The boxes overlap a good deal, so the crop alone cannot predict yield — but it shifts the whole range, which is why crop must be a feature.

7Step 6 — Data Preparation

Duplicated rows
4

drop_duplicates() removes exact repeats, which would otherwise count one plot twice and could appear in both the training and the testing data.

Crop names typed differently
"rice", " WHEAT", "maize "

strip() and title() make them Rice, Wheat and Maize — otherwise the model would see six crops instead of three.

Impossible yields
2

A yield of 42 t/ha (a decimal point lost) and one of −1 cannot be real. Rows with yield of 0 or less, or above 15 t/ha, are removed. Guessing the intended value would be inventing data.

Blank rainfall
18 (median of the same crop)

Removing these rows would waste good yield records. They are filled with the median rainfall of the same crop, because rice plots receive far more rain than wheat plots — one overall median would be wrong for both. The median is used rather than the mean because it is not pulled by extreme values.

Feature engineering

Machine learning models need numbers. irrigated becomes 1 for Yes and 0 for No. crop is one-hot encoded with get_dummies(): three new columns, crop_Rice, crop_Wheat and crop_Maize, each 1 or 0. One-hot encoding is used instead of numbering the crops 1, 2, 3 because a number would tell the model that maize is "more" than rice — which means nothing.

That gives nine features. The plot ID is not one of them: an ID carries no information about yield, and a model given it could only memorise.

4Correlation of each feature with yield
Horizontal bar chart
Correlation of each feature with yield
how to read it

The Pearson correlation of each prepared feature with yield, from −1 to +1. Green bars move with yield, red bars against it.

what it shows

Being a rice plot, rainfall and fertiliser have the strongest positive correlations; being a maize plot the strongest negative, because maize yields less per hectare in this region. Soil pH shows almost no correlation — but that does not mean it does not matter. pH hurts yield when it is too low or too high, so its effect is a curve that a correlation coefficient, which only measures straight-line relationships, cannot see. The Random Forest's feature importance (chart 8) tells a different story.

Module 4From Modelling to Evaluation

8Step 7 — Modelling

AI model→Learning based→Supervised learning→Regression

Every training plot comes with its harvested yield — the label — so this is supervised learning, and because the label is a number it is regression.

The data is split with train_test_split() into 718 plots for training and 180 for testing (80 : 20, as the handbook recommends for a dataset of this size), with a fixed random_state so the split is the same every run.

How the models work

Linear Regression fits one straight-line equation: yield = intercept + coefficient₁ × rainfall + coefficient₂ × temperature + … It learned an intercept of 3.013 and a fertiliser coefficient of 0.0049 t/ha per kg — on average each extra kilogram of fertiliser adds about 5 kg of grain per hectare. That average hides the curve in chart 2: the first kilograms add much more than that, the last ones much less.

The Random Forest builds 200 decision trees, each on a random sample of the training plots and features, and averages their predictions. Each tree asks questions such as "is fertiliser above 140?" and "is this a rice plot?", so together they can follow curves and interactions a single line cannot. min_samples_leaf=3 stops any tree from memorising single plots.

9Step 8 — Evaluation

On the 180 test plots, Linear Regression scored an RMSE of 0.38 t/ha and R² of 0.544; the Random Forest an RMSE of 0.357 t/ha and R² of 0.597. The handbook's three regression metrics all agree: MAE is the average size of an error, MSE squares errors so big ones count more, and RMSE brings that back to tonnes per hectare.

A single split can mislead, so 5-fold cross validation was run as well. The mean RMSE across folds was 0.368 for Linear Regression and 0.358 for the Random Forest — the same ranking, with every row of data used for testing once.

The gap is real but small. An R² near 0.6 means the models explain about 60% of the variation in yield; the rest comes from things the data does not record — pests, sowing dates, seed variety. A capstone report should say so plainly rather than chasing a higher number: no model can predict what it was never told.

The Random Forest is chosen for deployment. Linear Regression remains useful alongside it, because its fertiliser coefficient is something a farmer can understand in one sentence.

5Predicted against actual: both models
Scatter plot
Predicted against actual: both models
how to read it

Each dot is a test plot: real yield across, prediction up, for Linear Regression (left) and the Random Forest (right). A perfect model puts every dot on the dashed line.

what it shows

Both clouds follow the line. Look at the ends: Linear Regression's dots flatten towards the middle at both the low and the high end, because a straight line cannot bend to follow the fertiliser curve. The forest's dots stay closer to the line at the ends — the visual reason for its lower RMSE.

Neither cloud is tight. Some of the scatter cannot be removed: two plots with identical conditions still yield differently because of pests, sowing date and luck, none of which is in the data.

6Random Forest: residuals
Histogram
Random Forest: residuals
how to read it

A residual is actual minus predicted yield for one test plot. Bars left of the red line are plots the model over-predicted; bars right of it, plots it under-predicted.

what it shows

The residuals form one hump centred on zero — the mean residual is -0.034 t/ha — so the model is not biased high or low. 83.3 % of test plots are predicted within half a tonne per hectare. A lopsided or two-humped residual chart would have meant the model was missing something systematic.

75-fold cross validation
Bar chart
5-fold cross validation
how to read it

The RMSE of each model in each of the five folds. Each fold is a different fifth of the data, held out once.

what it shows

The Random Forest has the lower error in most folds, and both models are consistent from fold to fold. That consistency is the point of cross validation: one train-test split could have been lucky or unlucky, but five folds agreeing means the comparison can be trusted.

8What the Random Forest relied on
Horizontal bar chart
What the Random Forest relied on
how to read it

How much each feature contributed to the forest's decisions, adding up to 1.

what it shows

Fertiliser and the crop matter most, then nitrogen, temperature and soil pH. Soil pH ranks well here although its correlation was zero — the forest can use its curved effect. Comparing charts 4 and 8 is a good viva answer: correlation and importance answer different questions.

Module 5From Deployment to Feedback

10Step 9 — Deployment

The trained forest is saved with joblib.dump() to yield_model.pkl and loaded back with joblib.load(). An app or website — the curriculum's "website containing an ML model" activity — would load that file and predict without training again.

The program then predicts three new plots: a rain-fed rice plot after a good monsoon, an irrigated wheat plot, and a rain-fed maize plot with little fertiliser. In use, a farmer or extension worker would enter the plot's details mid-season and get a yield estimate with the typical error beside it: about ± 0.357 t/ha.

11Step 10 — Feedback

The last step closes the loop. After the next harvest, the real yields come back, and the model's predictions for that season can be checked against them. next_season.csv plays that role — and it is a drought year, with average rainfall falling from 837 mm to 558 mm.

The model's RMSE was 0.357 t/ha on last season's test data and 0.4 t/ha on the drought season. The program applies a simple rule: retrain if the error grows by more than 20%. Here it grew by about 12%, so the model stays in use — but the drift is real, and in a monitoring dashboard it would be the first warning sign.

Feedback from people matters as much as feedback from numbers. Farmers might report that predictions are too high on sandy soils, or for a new seed variety; each of those becomes a new feature or new training data, and the methodology loops back to Data Requirements.

12Ethical considerations

Fairness to small farmers

If most training plots come from larger, better-recorded farms, the model will be least accurate for the smallholders SDG 2 is about. The data must cover them deliberately.

How predictions are used

A yield prediction must never be used to deny a farmer credit or insurance on its own — it has an error of about half a tonne per hectare. It supports decisions; it does not make them.

Privacy

Plot-level records reveal a family's income. Data must be collected with consent, stored with plot IDs only, and shared only in aggregate.

Transparency

The Random Forest is harder to explain than a line. Reporting feature importances and the Linear Regression coefficients alongside it keeps the reasoning visible to the people who rely on it.

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.

crop_yield.py
# ---------------------------------------------------------------------------
# crop_yield.py
#
# Capstone project: predicting the yield of a farm plot (tonnes per hectare)
# from its crop, weather, soil and the fertiliser used.
#
# CBSE Class XII, Artificial Intelligence (843), Part C - Capstone Project.
# SDG 2 (Zero Hunger) and SDG 12 (Responsible Consumption and Production).
#
# The program follows the 10 steps of the Data Science Methodology:
#   1 Business Understanding   2 Analytic Approach   3 Data Requirements
#   4 Data Collection          5 Data Understanding  6 Data Preparation
#   7 Modelling                8 Evaluation          9 Deployment
#   10 Feedback
#
# Two regression models are compared - Linear Regression, as in the
# handbook, and a Random Forest - with a train-test split and with 5-fold
# cross validation, using MAE, MSE, RMSE and R squared.
#
# Libraries: pandas, numpy, matplotlib, scikit-learn, joblib.
# ---------------------------------------------------------------------------

# %% Step 1 - Business Understanding
print("=" * 64)
print("HOW MUCH WILL THIS FIELD YIELD?")
print("=" * 64)
print("Problem : farmers and planners cannot tell before harvest how much")
print("          a plot will yield, so input use and storage are guesswork.")
print("Goal    : predict yield (t/ha) from crop, weather, soil and inputs.")
print()

# %% Step 2 - Analytic Approach
# The answer is a number (tonnes per hectare), so this is PREDICTIVE
# analytics using REGRESSION.
print("Analytic approach : predictive - regression (the target is a number)")
print()

# %% Step 3 - Data Requirements
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split, KFold, cross_val_score
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import joblib

pd.set_option("display.width", 130)
pd.set_option("display.max_columns", 20)

required = ["crop", "rainfall_mm", "temperature_c", "soil_ph", "nitrogen_kg_ha",
            "fertiliser_kg_ha", "irrigated", "yield_t_ha"]
print("Columns required :", required)

# %% Step 4 - Data Collection
data = pd.read_csv("crop_yield.csv")
print("Rows collected   :", len(data))
missing_columns = [c for c in required if c not in data.columns]
print("Required columns missing from the file :", missing_columns)
print()
print(data.head(8))
print()

# %% Step 5 - Data Understanding
print("--- Step 5: Data Understanding ---")
print("Missing values in each column:")
print(data.isnull().sum().to_string())
print()
print("Summary statistics:")
print(data.describe().round(2))
print()

# Chart 1: the distribution of yield.
plt.figure(figsize=(7, 4))
plt.hist(data["yield_t_ha"].clip(0, 10), bins=30, color="#59a14f", edgecolor="white")
plt.title("Distribution of yield (values above 10 shown at 10)")
plt.xlabel("Yield (tonnes per hectare)")
plt.ylabel("Number of plots")
plt.tight_layout()
plt.savefig("chart1_yield_distribution.png")
plt.close()

# Chart 2: fertiliser against yield.
ok = data[(data["yield_t_ha"] > 0) & (data["yield_t_ha"] < 15)]
plt.figure(figsize=(7, 5))
plt.scatter(ok["fertiliser_kg_ha"], ok["yield_t_ha"], s=12, alpha=0.5, color="#4e79a7")
plt.title("Fertiliser used and yield")
plt.xlabel("Fertiliser (kg per hectare)")
plt.ylabel("Yield (t/ha)")
plt.grid(True, linestyle="--", alpha=0.4)
plt.tight_layout()
plt.savefig("chart2_fertiliser_yield.png")
plt.close()

# Chart 3: yield of each crop, as box plots.
plt.figure(figsize=(7, 4.5))
crops_seen = sorted(ok["crop"].str.strip().str.title().unique())
plt.boxplot([ok[ok["crop"].str.strip().str.title() == c]["yield_t_ha"] for c in crops_seen], labels=crops_seen)
plt.title("Yield of each crop")
plt.ylabel("Yield (t/ha)")
plt.grid(True, axis="y", linestyle="--", alpha=0.4)
plt.tight_layout()
plt.savefig("chart3_yield_by_crop.png")
plt.close()
for c in crops_seen:
    part = ok[ok["crop"].str.strip().str.title() == c]["yield_t_ha"]
    print("Median yield of " + c.ljust(6), ":", round(part.median(), 2), "t/ha")
print()

# %% Step 6 - Data Preparation
print("--- Step 6: Data Preparation ---")
before = len(data)
data = data.drop_duplicates()
print("Duplicate rows removed          :", before - len(data))

data["crop"] = data["crop"].str.strip().str.title()
print("Crops after tidying the names   :", sorted(data["crop"].unique()))

impossible = ((data["yield_t_ha"] <= 0) | (data["yield_t_ha"] > 15)).sum()
data = data[(data["yield_t_ha"] > 0) & (data["yield_t_ha"] <= 15)]
print("Impossible yields removed       :", impossible)

# Missing rainfall is filled with the median rainfall of the same crop:
# rice fields get far more rain than wheat fields, so one overall median
# would be wrong for both.
blank_rain = data["rainfall_mm"].isnull().sum()
data["rainfall_mm"] = data.groupby("crop")["rainfall_mm"].transform(lambda s: s.fillna(s.median()))
print("Blank rainfall values filled    :", blank_rain, "(median of the same crop)")
print("Rows ready for modelling        :", len(data))

# Text columns become numbers. irrigated: Yes = 1, No = 0. crop: one
# column per crop (one-hot encoding), so no crop is treated as "bigger".
data["irrigated"] = (data["irrigated"] == "Yes").astype(int)
data = pd.get_dummies(data, columns=["crop"], dtype=int)
features = ["rainfall_mm", "temperature_c", "soil_ph", "nitrogen_kg_ha", "fertiliser_kg_ha",
            "irrigated", "crop_Maize", "crop_Rice", "crop_Wheat"]
print("Features used                   :", len(features))
print()

# Chart 4: correlation of every feature with yield.
corr = data[features + ["yield_t_ha"]].corr()["yield_t_ha"].drop("yield_t_ha").sort_values()
print("Correlation of each feature with yield:")
print(corr.round(2).to_string())
print()
plt.figure(figsize=(7, 4.5))
colours = ["#e15759" if v < 0 else "#59a14f" for v in corr.values]
plt.barh(corr.index, corr.values, color=colours)
plt.axvline(0, color="#999999")
plt.title("Correlation with yield")
plt.xlabel("Correlation coefficient")
plt.tight_layout()
plt.savefig("chart4_correlation.png")
plt.close()

# %% Step 7 - Modelling
print("--- Step 7: Modelling ---")
X = data[features]
y = data["yield_t_ha"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=True, random_state=7)
print("Training rows :", len(X_train))
print("Testing rows  :", len(X_test))

linear = LinearRegression()
linear.fit(X_train, y_train)
forest = RandomForestRegressor(n_estimators=200, min_samples_leaf=3, random_state=0)
forest.fit(X_train, y_train)

print("Linear Regression - intercept :", round(linear.intercept_, 3))
print("Linear Regression - fertiliser coefficient :",
      round(linear.coef_[features.index("fertiliser_kg_ha")], 4), "t/ha per kg")
print()

# %% Step 8 - Evaluation
print("--- Step 8: Evaluation ---")
results = []
for name, model in [["Linear Regression", linear], ["Random Forest", forest]]:
    pred = model.predict(X_test)
    mae = mean_absolute_error(y_test, pred)
    mse = mean_squared_error(y_test, pred)
    rmse = np.sqrt(mse)
    r2 = r2_score(y_test, pred)
    results.append([name, mae, mse, rmse, r2])
    print(name)
    print("  " + name + " MAE  :", round(mae, 3), "t/ha")
    print("  " + name + " MSE  :", round(mse, 3))
    print("  " + name + " RMSE :", round(rmse, 3), "t/ha")
    print("  " + name + " R2   :", round(r2, 3))
print()

# 5-fold cross validation: every row is in the test fold exactly once.
folds = KFold(n_splits=5, shuffle=True, random_state=1)
cv_lin = np.sqrt(-cross_val_score(LinearRegression(), X, y, cv=folds, scoring="neg_mean_squared_error"))
cv_rf = np.sqrt(-cross_val_score(RandomForestRegressor(n_estimators=200, min_samples_leaf=3, random_state=0),
                                 X, y, cv=folds, scoring="neg_mean_squared_error"))
print("RMSE in each of the 5 folds (t/ha):")
print("  Fold  Linear  Forest")
for k in range(5):
    print("  ", k + 1, "  ", round(cv_lin[k], 3), " ", round(cv_rf[k], 3))
print("Mean cross-validated RMSE, Linear Regression :", round(cv_lin.mean(), 3))
print("Mean cross-validated RMSE, Random Forest     :", round(cv_rf.mean(), 3))
print()

best_pred = forest.predict(X_test)

# Chart 5: predicted against actual, for both models side by side.
lin_pred = linear.predict(X_test)
plt.figure(figsize=(10, 4.8))
for k in range(2):
    plt.subplot(1, 2, k + 1)
    pred = [lin_pred, best_pred][k]
    plt.scatter(y_test, pred, s=14, alpha=0.6, color=["#9c9c9c", "#59a14f"][k])
    plt.plot([1, 9], [1, 9], color="#e15759", linestyle="--", label="Perfect prediction")
    plt.title(["Linear Regression", "Random Forest"][k])
    plt.xlabel("Actual yield (t/ha)")
    plt.ylabel("Predicted yield (t/ha)")
    plt.legend()
    plt.grid(True, linestyle="--", alpha=0.4)
plt.tight_layout()
plt.savefig("chart5_predicted_actual.png")
plt.close()

# Chart 6: the residuals (actual minus predicted) of the forest.
residuals = y_test - best_pred
print("Mean residual (Random Forest)        :", round(residuals.mean(), 3), "t/ha")
print("Residuals within +/- 0.5 t/ha        :", round((residuals.abs() <= 0.5).mean() * 100, 1), "%")
plt.figure(figsize=(7, 4))
plt.hist(residuals, bins=25, color="#59a14f", edgecolor="white")
plt.axvline(0, color="#e15759", linestyle="--")
plt.title("Random Forest: residuals on the test plots")
plt.xlabel("Actual minus predicted yield (t/ha)")
plt.ylabel("Number of plots")
plt.tight_layout()
plt.savefig("chart6_residuals.png")
plt.close()

# Chart 7: RMSE in each fold for both models.
x = np.arange(5)
plt.figure(figsize=(7, 4))
plt.bar(x - 0.2, cv_lin, width=0.4, label="Linear Regression", color="#9c9c9c")
plt.bar(x + 0.2, cv_rf, width=0.4, label="Random Forest", color="#59a14f")
plt.xticks(x, ["Fold 1", "Fold 2", "Fold 3", "Fold 4", "Fold 5"])
plt.ylabel("RMSE (t/ha) - lower is better")
plt.title("5-fold cross validation")
plt.legend()
plt.tight_layout()
plt.savefig("chart7_kfold.png")
plt.close()

# Chart 8: which features the forest relied on.
importance = pd.Series(forest.feature_importances_, index=features).sort_values()
print("Feature importance (Random Forest):")
print(importance.round(3).to_string())
print()
plt.figure(figsize=(7, 4.5))
plt.barh(importance.index, importance.values, color="#f28e2b")
plt.title("What the Random Forest relied on")
plt.xlabel("Importance")
plt.tight_layout()
plt.savefig("chart8_importance.png")
plt.close()

# %% Step 9 - Deployment
print("--- Step 9: Deployment ---")
# The trained model is saved to a file, so an app or website can load it
# and predict without training again.
joblib.dump(forest, "yield_model.pkl")
loaded = joblib.load("yield_model.pkl")
print("Model saved to yield_model.pkl and loaded back")

new_plots = pd.DataFrame({
    "rainfall_mm": [1180, 560, 640], "temperature_c": [26.5, 18.5, 27.0], "soil_ph": [6.7, 7.1, 7.9],
    "nitrogen_kg_ha": [85, 90, 60], "fertiliser_kg_ha": [130, 150, 40], "irrigated": [0, 1, 0],
    "crop_Maize": [0, 0, 1], "crop_Rice": [1, 0, 0], "crop_Wheat": [0, 1, 0],
})
names = ["Rice, rain-fed, good rain", "Wheat, irrigated", "Maize, rain-fed, little fertiliser"]
predicted = loaded.predict(new_plots)
for i in range(len(names)):
    print(names[i].ljust(36), "->", round(predicted[i], 2), "t/ha")
print()

# %% Step 10 - Feedback
print("--- Step 10: Feedback ---")
# After the next season's harvest, the real yields come back. Comparing
# them with what the model predicted shows whether it still works.
nxt = pd.read_csv("next_season.csv")
nxt["irrigated"] = (nxt["irrigated"] == "Yes").astype(int)
nxt = pd.get_dummies(nxt, columns=["crop"], dtype=int)
next_pred = loaded.predict(nxt[features])
next_rmse = np.sqrt(mean_squared_error(nxt["yield_t_ha"], next_pred))
print("Plots in the next season's feedback :", len(nxt))
print("RMSE on last season's test data     :", round(results[1][3], 3), "t/ha")
print("RMSE on the next season             :", round(next_rmse, 3), "t/ha")
print("Average rainfall, training season   :", round(data["rainfall_mm"].mean()), "mm")
print("Average rainfall, next season       :", round(nxt["rainfall_mm"].mean()), "mm")
if next_rmse > 1.2 * results[1][3]:
    print("Feedback: error has grown by more than 20% - retrain with the new season's data.")
else:
    print("Feedback: error is within 20% of last season - the model can stay in use.")

The library calls it is built from

CallStepWhat it does
pd.read_csv()Step 4Reads the dataset into a DataFrame
isnull().sum() / describe()Step 5Counts missing values; summary statistics
drop_duplicates()Step 6Removes repeated rows
groupby().transform(... fillna(median))Step 6Fills blank rainfall with the median of the same crop
pd.get_dummies()Step 6One-hot encodes the crop column
DataFrame.corr()Step 6Pearson correlation of each feature with yield
train_test_split(test_size=0.2)Step 7Splits into 80% training and 20% testing data
LinearRegression().fit()Step 7Trains the linear model
RandomForestRegressor().fit()Step 7Trains the forest of 200 trees
mean_absolute_error / mean_squared_error / r2_scoreStep 8The regression metrics
KFold + cross_val_score()Step 85-fold cross validation
feature_importances_Step 8What the forest relied on
joblib.dump() / joblib.load()Step 9Saves and reloads the trained model

14Output of a real run

Output
================================================================
HOW MUCH WILL THIS FIELD YIELD?
================================================================
Problem : farmers and planners cannot tell before harvest how much
          a plot will yield, so input use and storage are guesswork.
Goal    : predict yield (t/ha) from crop, weather, soil and inputs.

Analytic approach : predictive - regression (the target is a number)

Columns required : ['crop', 'rainfall_mm', 'temperature_c', 'soil_ph', 'nitrogen_kg_ha', 'fertiliser_kg_ha', 'irrigated', 'yield_t_ha']
Rows collected   : 904
Required columns missing from the file : []

  plot_id   crop  rainfall_mm  temperature_c  soil_ph  nitrogen_kg_ha  fertiliser_kg_ha irrigated  yield_t_ha
0  P10001  Maize        759.0           26.3      6.1            84.0             121.0        No        4.79
1  P10002   Rice       1448.0           23.6      6.4            72.0              99.0        No        4.49
2  P10003   Rice       1278.0           28.6      5.9            75.0              53.0       Yes        4.24
3  P10004   Rice       1139.0           27.3      7.6            72.0             122.0       Yes        5.34
4  P10005   Rice       1245.0           26.3      6.3           127.0             116.0       Yes        5.39
5  P10006  Wheat        797.0           23.1      6.5            54.0              95.0        No        4.83
6  P10006  Wheat        797.0           23.1      6.5            54.0              95.0        No        4.83
7  P10007  Maize        624.0           28.5      6.8            78.0              71.0       Yes        3.47

--- Step 5: Data Understanding ---
Missing values in each column:
plot_id              0
crop                 0
rainfall_mm         18
temperature_c        0
soil_ph              0
nitrogen_kg_ha       0
fertiliser_kg_ha     0
irrigated            0
yield_t_ha           0

Summary statistics:
       rainfall_mm  temperature_c  soil_ph  nitrogen_kg_ha  fertiliser_kg_ha  yield_t_ha
count       886.00         904.00   904.00          904.00            904.00      904.00
mean        837.04          24.00     6.80           79.28            117.68        4.60
std         318.87           4.58     0.59           24.02             46.41        1.37
min         160.00           9.20     4.80           10.00              0.00       -1.00
25%         593.00          20.08     6.40           62.00             88.00        4.22
50%         772.00          25.15     6.80           80.00            117.00        4.57
75%        1073.00          27.70     7.20           96.00            147.00        4.94
max        1824.00          33.20     8.50          140.00            249.00       42.00

Median yield of Maize  : 4.24 t/ha
Median yield of Rice   : 4.91 t/ha
Median yield of Wheat  : 4.48 t/ha

--- Step 6: Data Preparation ---
Duplicate rows removed          : 4
Crops after tidying the names   : ['Maize', 'Rice', 'Wheat']
Impossible yields removed       : 2
Blank rainfall values filled    : 18 (median of the same crop)
Rows ready for modelling        : 898
Features used                   : 9

Correlation of each feature with yield:
crop_Maize         -0.37
crop_Wheat         -0.16
soil_ph            -0.00
irrigated           0.08
temperature_c       0.15
nitrogen_kg_ha      0.29
fertiliser_kg_ha    0.42
rainfall_mm         0.46
crop_Rice           0.48

--- Step 7: Modelling ---
Training rows : 718
Testing rows  : 180
Linear Regression - intercept : 3.013
Linear Regression - fertiliser coefficient : 0.0049 t/ha per kg

--- Step 8: Evaluation ---
Linear Regression
  Linear Regression MAE  : 0.302 t/ha
  Linear Regression MSE  : 0.144
  Linear Regression RMSE : 0.38 t/ha
  Linear Regression R2   : 0.544
Random Forest
  Random Forest MAE  : 0.286 t/ha
  Random Forest MSE  : 0.128
  Random Forest RMSE : 0.357 t/ha
  Random Forest R2   : 0.597

RMSE in each of the 5 folds (t/ha):
  Fold  Linear  Forest
   1    0.352   0.344
   2    0.379   0.354
   3    0.378   0.357
   4    0.376   0.371
   5    0.358   0.366
Mean cross-validated RMSE, Linear Regression : 0.368
Mean cross-validated RMSE, Random Forest     : 0.358

Mean residual (Random Forest)        : -0.034 t/ha
Residuals within +/- 0.5 t/ha        : 83.3 %
Feature importance (Random Forest):
crop_Wheat          0.007
crop_Maize          0.012
irrigated           0.014
rainfall_mm         0.081
soil_ph             0.086
temperature_c       0.101
nitrogen_kg_ha      0.152
crop_Rice           0.254
fertiliser_kg_ha    0.293

--- Step 9: Deployment ---
Model saved to yield_model.pkl and loaded back
Rice, rain-fed, good rain            -> 5.19 t/ha
Wheat, irrigated                     -> 4.74 t/ha
Maize, rain-fed, little fertiliser   -> 3.76 t/ha

--- Step 10: Feedback ---
Plots in the next season's feedback : 120
RMSE on last season's test data     : 0.357 t/ha
RMSE on the next season             : 0.4 t/ha
Average rainfall, training season   : 837 mm
Average rainfall, next season       : 558 mm
Feedback: error is within 20% of last season - the model can stay in use.

15The same project in Orange

The curriculum allows the capstone in Orange, and Unit 4 teaches it. The same workflow, widget by widget: The tool is Orange Data Mining.

WidgetWhat it does
FileLoad crop_yield.csv; set yield_t_ha as the target and plot_id as a meta attribute.
Feature Statistics / DistributionsSpot the blank rainfall and the impossible yields (Step 5).
Select Rows + Impute + PreprocessRemove impossible yields; impute missing rainfall; Orange's learners encode categories automatically (Step 6).
Scatter PlotPlot fertiliser against yield, as in chart 2.
Linear Regression and Random ForestThe two learners (Step 7).
Test and ScoreChoose Cross validation with 5 folds; read MSE, RMSE, MAE and R² for both models (Step 8).
PredictionsFeed in new plots to predict their yield (Step 9).

16Testing

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

Test caseExpectedActualResult
The full datasetRows collected : 904Rows collected : 904Pass
Four duplicated rows are removedDuplicate rows removed : 4Duplicate rows removed : 4Pass
"rice", " WHEAT" and "maize " join their cropsCrops after tidying the names : ['Maize', 'Rice', 'Wheat']Crops after tidying the names : ['Maize', 'Rice', 'Wheat']Pass
A yield of 42 t/ha and one of -1 are impossibleImpossible yields removed : 2Impossible yields removed : 2Pass
Blank rainfall is filled, not droppedBlank rainfall values filled : 18 (median of the same crop)Blank rainfall values filled : 18 (median of the same crop)Pass
The forest beats the straight line in cross validationMean cross-validated RMSE, Random Forest : 0.358Mean cross-validated RMSE, Random Forest : 0.358Pass
Yield that is an exact straight-line rule: Linear Regression finds it (R2 = 1)Linear Regression R2 : 1.0Linear Regression R2 : 1.0Pass
Boundary: 15.0 t/ha is kept, 15.01 is removedImpossible yields removed : 1Impossible yields removed : 1Pass
The data file is missingFileNotFoundError: [Errno 2] No such file or directory: 'crop_yield.csv'FileNotFoundError: [Errno 2] No such file or directory: 'crop_yield.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, the 5W1H canvas, meeting the farmers and officers, the final presentation
Data leadData requirements and collection: the survey, IMD and Soil Health Card data, permissions
AnalystData understanding and preparation: cleaning, filling missing values, encoding, charts
ModellerTraining and comparing the models, cross validation, choosing the final model
Documentation and videoThe project documentation, the 3-minute video script and editing
WhenMethodology stepsDeliverable
April – May1 Business Understanding, 2 Analytic ApproachProblem statement and 5W1H canvas approved by the teacher
June – July3 Data Requirements, 4 Data CollectionSurvey done; secondary data downloaded and credited
August5 Data Understanding, 6 Data PreparationClean dataset and exploratory charts
September – October7 Modelling, 8 EvaluationModels compared with K-fold; final model chosen
November9 Deployment, 10 FeedbackSaved model, predictions, feedback plan
December – JanuaryDocumentation and videoReport, 3-minute video, viva preparation

18Results, limits and what next

what the project found
  • Both models predict yield to within about 0.36–0.38 t/ha on unseen plots; the Random Forest is slightly but consistently better.
  • Cross validation confirms the ranking: mean RMSE 0.358 for the forest against 0.368 for the line.
  • Fertiliser has diminishing returns — the first 100 kg/ha add far more than the next 100.
  • Soil pH matters although its correlation with yield is zero, because its effect is a curve.
  • In a drought season the model's error grew by about 12%: still usable, but drifting.

Limitations

  • Sample data from one region; real yields vary far more between districts
  • No information on pests, sowing date, seed variety or soil type
  • About 40% of the variation in yield is not explained by the features available
  • The model has seen only one season; one drought year in the feedback is not enough to judge it fully

Future scope

  • Add sowing date, seed variety and a pest-incidence score as features
  • Collect several seasons and test on a whole season held out (time-based validation)
  • Recommend the fertiliser dose with the best yield per rupee — a step into prescriptive analytics
  • Build a simple web form around yield_model.pkl, as in the curriculum's website activity

What the team had to learn

  • Regression metrics: MAE, MSE, RMSE and R², and when each is useful
  • K-fold cross validation with scikit-learn, and why it is more reliable than one split
  • One-hot encoding categorical features
  • Why correlation can miss a curved relationship that a tree-based model finds

19Conclusion

The capstone followed all ten steps of the Data Science Methodology to answer one question: can a plot's yield be predicted before harvest? It can — to within about a third of a tonne per hectare, with a Random Forest that cross validation confirms is slightly better than Linear Regression.

The most useful result for SDG 2 and 12 is not the prediction itself but what the data showed along the way: fertiliser's effect flattens out, so the heaviest doses add little but cost. That is a finding a farmer can act on this season.

The feedback step showed the methodology's last lesson. A model is not finished when it is deployed; it must be checked against each new season, and a drought year is exactly when it starts to drift.

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 field and a mandi. "Before harvest, nobody knows how much a field will yield — so fertiliser, storage and procurement are guesswork. Our problem statement: …"
0:25 – 0:45b. SDG alignmentShow the SDG 2 and SDG 12 icons. "More food from the same land, and less fertiliser wasted."
0:45 – 1:20c. AI concepts and algorithms"This is predictive analytics using supervised regression. We compared Linear Regression and a Random Forest, using a train-test split and 5-fold cross validation, judged by MAE, MSE and RMSE." Show the methodology wheel.
1:20 – 2:20d. How it worksScreen-record the notebook: the cleaning counts, chart 2 (fertiliser curve), the K-fold chart, then the three new plots being predicted. End on the feedback lines for the drought season.
2:20 – 2:45e. Conclusion"Our model predicts yield within about a third of a tonne. Fertiliser beyond about 150 kg/ha adds very little — and the model must be checked every season."
2:45 – 3:00f. Acknowledgement"We thank our AI teacher, [name], and the farmers who shared their records." Show the team.

21Questions for the viva

▸Which type of analytics is your project, and why?

Predictive analytics, because it forecasts a future outcome — the yield — from past data. It uses regression because the target is a number.

▸Why did you fill missing rainfall with the median of the same crop?

Rice and wheat plots receive very different rainfall, so one overall value would be wrong for both. The median is used because extreme values do not pull it.

▸What is the difference between MSE and RMSE?

MSE is the average of the squared errors. RMSE is its square root, so it is in the same units as yield — tonnes per hectare — and easier to interpret.

▸Why did you use K-fold cross validation as well as a train-test split?

One split can be lucky or unlucky. K-fold uses every row as test data once, giving five measurements, so the comparison between the models is more reliable.

▸Soil pH has zero correlation with yield. Does it matter?

Yes. Its effect is a curve — yield falls when pH is too low or too high — and correlation only measures straight-line relationships. The Random Forest's importance shows it matters.

▸What does one-hot encoding do?

It turns a text category into several 0/1 columns, one per category, so the model does not treat the categories as numbers with an order.

▸What did the Feedback step tell you?

In a drought season the error grew by about 12%, under our 20% retraining rule. The model can stay in use but should be watched.

▸Which SDGs is your project aligned to?

SDG 2, Zero Hunger, and SDG 12, Responsible Consumption and Production.

22References

  • Directorate of Economics and Statistics, Ministry of Agriculture & Farmers Welfare — district-wise crop production data — https://desagri.gov.in/
  • India Meteorological Department — district rainfall — https://mausam.imd.gov.in/
  • Soil Health Card scheme — https://soilhealth.dac.gov.in/
  • Open Government Data Platform India — https://data.gov.in/
  • 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