Analyze India Unemployment Trends

Use live World Bank data to chart unemployment trends in India with Python.

Introduction

30 Second Summary

Official numbers often arrive in tables that are hard to understand at a glance. Their story becomes clearer when the years flow in order and different groups appear side by side.

In this project, you will build a shareable unemployment-trend analysis in Google Colab. Your notebook will turn live annual data from the World Bank Indicators API into a cleaned comparison chart with exportable evidence.

What You'll Build

You'll run one notebook to transform raw newest-first records into a chronological comparison that you can explain or share.

By the end of this project, you'll have:

  • A reproducible notebook you can rerun to retrieve the latest available annual unemployment series without an API key.
  • A chronological comparison where the Total, Male, and Female lines reveal how each rate changes over time.
  • A shareable evidence pack containing peak-year summaries, latest-year summaries, a CSV dataset, a PNG chart, plus documented findings and limitations.
  • Secret Mission: Add a five-year moving average that exposes the longer-term direction while preserving the annual line.

Are there any prerequisites?

You need a Google account with Google Colab access plus an internet connection. No local Python installation is required.

Before We Start

This checkpoint commits you to a reproducible Google Colab notebook that investigates unemployment trends in India with live World Bank modeled ILO estimates. Your finished analysis will document its methods, findings, and limitations for the audience you want to inform.

Set Up Your Colab Notebook

Your analysis needs a stable environment that another person can run without repeating Windows setup.

Google Colab provides that browser-based environment. A pinned runtime keeps the notebook aligned with the library versions used in this project.

The title names the investigation. The research question gives every chart a clear purpose.

In this step, get ready to:
  • Create the named notebook with its research question.
  • Configure the pinned Colab environment.
  • Confirm all required imports.
Configure the Colab notebook

Colab runs your notebook on a hosted runtime. Selecting runtime version 2026.07 gives the notebook Python 3.12.13 for the pinned libraries you install next.

  • Open Google Colab in your browser.
  • Create a blank notebook using Colab's notebook creation option.
  • Select the notebook name at the top left.
  • Enter india_unemployment_trends as the notebook name.
  • Open the Runtime menu.
  • Choose Change runtime type.
  • Set Runtime Version to 2026.07.
  • Apply the runtime change using the confirmation control at the bottom of the dialog.

✔️ The pinned runtime is selected

Your notebook now uses runtime version 2026.07 with Python 3.12.13.

ⓧ A different runtime is selected

The notebook needs the pinned runtime before you install its libraries.

  • Return to the Runtime menu.
  • Choose Change runtime type.
  • Set Runtime Version to 2026.07.
  • Apply the runtime change using the confirmation control at the bottom of the dialog.

You should now see runtime version 2026.07 selected.

ⓧ The pinned runtime is unavailable

The notebook cannot match the project environment until the required runtime appears in the list.

  • Refresh your browser tab.
  • Return to the india_unemployment_trends notebook from earlier.
  • Open the Runtime menu.
  • Choose Change runtime type.
  • Check the Runtime Version list for 2026.07 again.

Still Missing the Pinned Runtime?

If the required option still does not appear, help me troubleshoot the missing Colab runtime.

  • Add a text cell using the notebook toolbar.
  • Enter # Unemployment Trends in India in the text cell.
  • Run the title cell.

You should see Unemployment Trends in India rendered as the notebook title.

  • Add a second text cell below the title.
  • Enter How have total, male, and female unemployment rates in India changed over time? as the research question.
  • Run the research-question cell.

You should see the one-sentence question directly below the notebook title.

Install the pinned libraries

pandas handles the table-shaped API response. Matplotlib turns the cleaned values into charts.

Exact version pins make the notebook environment reproducible.

  • Add a new code cell below the research question.
  • Install pandas by running this command in the new cell:
!pip install pandas==3.0.5

What Does This pandas Command Do?

  • The !pip prefix runs the package installer from a Colab code cell.
  • The version pin requests pandas 3.0.5.
  • Confirm that the pandas installation cell finishes without an exception.

That first pin is in place. Your notebook now has the exact pandas release used for this project.

Did the pandas Installation Fail?

  • Confirm that your browser still has internet access.
  • Run the pandas installation cell again.

If the cell still fails, help me troubleshoot the pandas installation.

  • Add another code cell below the pandas installation cell.
  • Install Matplotlib by running this command in the new cell:
!pip install matplotlib==3.11.1

What Does This Matplotlib Command Do?

  • The command downloads Matplotlib from the Python package index.
  • The version pin requests Matplotlib 3.11.1.
  • Confirm that the Matplotlib installation cell finishes without an exception.

Both analysis libraries are now pinned inside the hosted runtime.

Did the Matplotlib Installation Fail?

  • Confirm that the pandas installation cell has finished.
  • Run the Matplotlib installation cell again.

If the second installation still fails, help me troubleshoot the Matplotlib installation.

Import the analysis modules

Imports make each installed library available to later notebook cells. The shorter names pd plus plt keep the analysis code readable.

  • Add a new code cell below the installation cells.
  • Import the required modules by running this code in the new cell:
import json
import urllib.request

import matplotlib.pyplot as plt
import pandas as pd

What Do These Imports Provide?

  • The json module converts the API response into Python data.
  • The urllib.request module opens the live data endpoint.
  • The pd name gives concise access to pandas.
  • The plt name gives concise access to Matplotlib's plotting interface.
  • Confirm that the import cell finishes without an exception.

The completed cell shows that all four imports are available in the current runtime.

Did an Import Fail?

  • Return to the installation cell for the library named in the exception.
  • Run that installation cell again.
  • Run the import cell again.

If the import still fails, help me troubleshoot the Colab import.

Before the final check, consider whether you expect a readiness message or an import exception.

  • Add another code cell below the import cell.
  • Print the setup status by running this code in the new cell:
print("Setup ready: pandas and Matplotlib imported")

What Should You See?

You should see Setup ready: pandas and Matplotlib imported directly below the cell. This confirms that the setup cells completed in the intended order.

Do You Not See the Readiness Message?

  • Run the import cell first.
  • Run the readiness cell again.

If no message appears, help me inspect the Colab cell order.

✔️ Awesome, I've got everything!

Your reusable import block is complete. The installation cells plus readiness check remain separate in Colab.

ⓧ I'd like to double check the full code

import json
import urllib.request

import matplotlib.pyplot as plt
import pandas as pd

Your notebook is ready to use the World Bank Indicators API. Next, you'll fetch the total unemployment series. Its first chart exposes the raw order returned by the API.

Fetch and Plot Raw Data

Your pinned Google Colab environment is ready. The next job is to bring live unemployment evidence into the notebook.

The World Bank Indicators API returns the newest observation first. Plotting that raw order gives you an immediate chart with a backwards timeline.

In this step, get ready to:
  • Request the total-unemployment series from the World Bank.
  • Convert the JSON observations into a raw DataFrame.
  • Plot the API order to reveal its reversed timeline.
Fetch the World Bank response

The request combines the India indicator endpoint with SL.UEM.TOTL.ZS. This indicator represents total unemployment as a percentage of the labor force.

  • Insert a code cell directly below the setup cell using the notebook toolbar.
  • Build the API request in the new cell by pasting this code:
API_BASE = "https://api.worldbank.org/v2/country/IND/indicator"
TOTAL_INDICATOR = "SL.UEM.TOTL.ZS"
url = f"{API_BASE}/{TOTAL_INDICATOR}?format=json&per_page=1000"

with urllib.request.urlopen(url, timeout=30) as response:
    payload = json.load(response)

What does this code do?

  • The API_BASE value stores the World Bank endpoint for India indicators.
  • The TOTAL_INDICATOR value selects the total unemployment series.
  • The URL requests a JSON response with enough space for the complete annual series.
  • The urllib.request.urlopen call retrieves the response within a 30-second timeout.
  • The json.load(response) call converts the response into Python data stored in payload.
  • Run the request cell using its run button.
  • Confirm the cell completes without an exception.

The live response is now stored in payload for the next cell.

Did the request cell stop?

  • Check that your device still has an internet connection.
  • Compare the endpoint with the code block above for missing quotation marks.
  • Rerun the earlier import cell if urllib.request or json is unavailable.

Still stuck? Help me troubleshoot the World Bank request.

Build and inspect the raw table

The JSON response stores observations inside a nested list. A pandas DataFrame turns those observations into columns you can inspect.

  • Insert another code cell directly below the request cell using the notebook toolbar.
  • Create the raw table in the new cell by pasting this code:
raw_total = pd.DataFrame(
    [
        {
            "Year": observation["date"],
            "Rate": observation["value"],
            "Indicator": "Total",
        }
        for observation in payload[1]
    ]
)

print(raw_total.head(8))

What does this code do?

  • The list comprehension processes every observation in the API response.
  • The Year column stores each observation date.
  • The Rate column stores each unemployment value.
  • The Indicator column identifies every row as Total.
  • The final line prints the first eight rows without changing their API order.

Before you run this cell, which year do you expect to appear first if the response is newest-first?

  • Run the table cell using its run button.

You will see a table whose first row begins with a recent year such as 2025. The following rows continue backwards through time.

You now have live World Bank data in your notebook. Its ordering has already exposed an important detail about the response.

Table did not appear?

  • Rerun the request cell to recreate payload.
  • Check that payload[1] uses the number 1.
  • Compare the braces around Year, Rate, and Indicator with the code above.

Need another pair of eyes? Help me fix the raw DataFrame cell.

Plot the newest-first timeline

A line chart turns the table order into a visible timeline. This first Matplotlib chart uses raw_total exactly as it arrived.

  • Insert a code cell directly below the table cell using the notebook toolbar.
  • Create the raw chart in the new cell by pasting this code:
plt.plot(raw_total["Year"], raw_total["Rate"])
plt.title("Raw India Total Unemployment Series")
plt.xlabel("Year")
plt.ylabel("Unemployment rate (% of labor force)")
plt.show()

What does this code do?

  • The plt.plot call uses Year for the horizontal axis.
  • The same call uses Rate for the vertical axis.
  • The title plus axis labels explain what the chart measures.
  • The plt.show() call displays the finished figure below the cell.

Before you run this cell, which direction do you think the years will move across the x-axis?

  • Run the chart cell using its run button.

You will see the line run from newer years toward older years. The backwards direction is the intended shortfall in this raw chart.

You have turned the API response into a visible data-quality problem. The timeline now gives you concrete evidence that response order matters.

Chart did not render?

  • Rerun the table cell to recreate raw_total.
  • Check that the column names use the exact capitalization shown above.
  • Rerun the earlier import cell if plt is unavailable.

Still no chart? Help me troubleshoot the raw Matplotlib plot.

✔️ Awesome, I've got everything!

Your notebook now fetches the total series and plots the raw API order.

ⓧ I'd like to double check the full code

import json
import urllib.request

import matplotlib.pyplot as plt
import pandas as pd

API_BASE = "https://api.worldbank.org/v2/country/IND/indicator"
TOTAL_INDICATOR = "SL.UEM.TOTL.ZS"
url = f"{API_BASE}/{TOTAL_INDICATOR}?format=json&per_page=1000"

with urllib.request.urlopen(url, timeout=30) as response:
    payload = json.load(response)

raw_total = pd.DataFrame(
    [
        {
            "Year": observation["date"],
            "Rate": observation["value"],
            "Indicator": "Total",
        }
        for observation in payload[1]
    ]
)

print(raw_total.head(8))
plt.plot(raw_total["Year"], raw_total["Rate"])
plt.title("Raw India Total Unemployment Series")
plt.xlabel("Year")
plt.ylabel("Unemployment rate (% of labor force)")
plt.show()

Your notebook now retrieves live unemployment data and exposes its reversed timeline. Next, you will clean the observations so the chart follows chronological order.

Clean and Sort the Timeline

Your first chart made the API's ordering problem visible. The line traveled from recent years toward older years.

The raw response also contains unavailable historical rates. Its year values arrive as text.

A clean time series needs numeric values in chronological order. This step turns the raw table into evidence you can read from left to right.

In this step, get ready to:
  • Remove missing values from the total unemployment series.
  • Convert the data types before sorting the timeline.
  • Replace the raw chart with a chronological chart.
Clean and sort the total series

The total_data variable stores the cleaned table as a pandas DataFrame. Removing missing values prevents blank rates from entering the chart.

Type conversion makes Year an int64 column. It makes Rate a float64 column before chronological sorting.

  • Return to the Google Colab notebook from the previous step.
  • Find the code cell that starts with print(raw_total.head(8)).
  • Clear the contents of that cell.
  • Build the chronological table by pasting this code into the empty cell:
total_data = raw_total.dropna(subset=["Rate"])
total_data = total_data.astype({"Year": "int64", "Rate": "float64"})
total_data = total_data.sort_values(["Indicator", "Year"], ignore_index=True)

print(total_data.head(8))

What does this cleaning pipeline do?

  • The dropna(subset=["Rate"]) operation removes every row whose unemployment rate is unavailable.
  • The astype() operation converts the year values into integers. It converts the rate values into floating-point numbers.
  • The sort_values() operation orders the rows by indicator first. It orders each indicator from the oldest year to the newest year.
  • The final print() call displays the first eight cleaned rows for inspection.

Before you run the cell, predict the year that appears first after the sort.

  • Run the edited cell using its play button.

You should see the first displayed row begin with 1991. Every displayed Rate value is filled.

Table still starts at 2025?

  • Confirm that you ran the edited cell after adding the cleaning pipeline.
  • Check that the sort_values() line uses Year with ignore_index=True.
  • Make sure the printed table uses total_data instead of raw_total.

Still seeing the newest year first? Help me troubleshoot the sorting in my total_data DataFrame.

Replace the raw chart

Matplotlib follows the row order supplied to the x-axis. Plotting total_data makes the timeline move from the oldest observation to the newest observation.

  • Place your cursor on a blank line below print(total_data.head(8)) in the same cell.
  • Create the chronological chart by pasting this code:
plt.plot(total_data["Year"], total_data["Rate"], linewidth=2, label="Total")
plt.title("India Total Unemployment Trend: Modeled ILO Estimates")
plt.xlabel("Year")
plt.ylabel("Unemployment rate (% of labor force)")
plt.legend()
plt.show()

How does the replacement chart work?

  • The first line plots the numeric Year column against the cleaned Rate column.
  • The linewidth=2 setting makes the trend easier to follow.
  • The label="Total" value names the line in the legend.
  • The title identifies the rates as modeled ILO estimates.

Before you run the cell, predict which year appears at the far left of the replacement chart.

  • Run the completed cell using its play button.

You should see 1991 at the left edge. The chart ends at 2025 on the right.

You have turned the raw response into a Total unemployment chart that reads chronologically.

Chart still runs backwards?

  • Confirm that both chart columns come from total_data.
  • Check that the sorting line appears before the plotting lines.
  • Run the whole cell again after correcting either reference.

Need help finding the mismatch? Help me check why my cleaned unemployment chart still runs from newer years to older years.

✔️ Awesome, I've got everything!

Your cleaned table begins at 1991. Your chart now moves from older years to newer years.

ⓧ I'd like to double check the full code

import json
import urllib.request

import matplotlib.pyplot as plt
import pandas as pd

API_BASE = "https://api.worldbank.org/v2/country/IND/indicator"
TOTAL_INDICATOR = "SL.UEM.TOTL.ZS"
url = f"{API_BASE}/{TOTAL_INDICATOR}?format=json&per_page=1000"

with urllib.request.urlopen(url, timeout=30) as response:
    payload = json.load(response)

raw_total = pd.DataFrame(
    [
        {
            "Year": observation["date"],
            "Rate": observation["value"],
            "Indicator": "Total",
        }
        for observation in payload[1]
    ]
)

total_data = raw_total.dropna(subset=["Rate"])
total_data = total_data.astype({"Year": "int64", "Rate": "float64"})
total_data = total_data.sort_values(["Indicator", "Year"], ignore_index=True)

print(total_data.head(8))
plt.plot(total_data["Year"], total_data["Rate"], linewidth=2, label="Total")
plt.title("India Total Unemployment Trend: Modeled ILO Estimates")
plt.xlabel("Year")
plt.ylabel("Unemployment rate (% of labor force)")
plt.legend()
plt.show()

Your total unemployment timeline is clean. Next up, you will add the male and female series to reveal differences hidden by the total rate.

Compare Demographic Trends

Your cleaned Total series now runs chronologically from 1991 to 2025. Next, you'll use the World Bank Indicators API to compare the national rate with male and female rates.

A total rate can hide differences between groups. A reusable function gives every JSON response the same path into a cleaned DataFrame.

In this step, get ready to:
  • Define one reusable function for every indicator.
  • Clean the combined three-series dataset.
  • Draw a labeled chronological comparison chart.
Build a reusable fetch layer

Each indicator follows the same API structure. An indicator map keeps the official codes beside the labels that appear in your chart.

  • Replace the code from TOTAL_INDICATOR through raw_total with this indicator map by pasting the code below:
INDICATORS = {
    "SL.UEM.TOTL.ZS": "Total",
    "SL.UEM.TOTL.MA.ZS": "Male",
    "SL.UEM.TOTL.FE.ZS": "Female",
}

How Does the Indicator Map Help?

  • Each dictionary key holds the code for one unemployment indicator.
  • Each dictionary value holds the label used throughout the analysis.
  • One map gives the download loop a consistent source for all three series.
  • Run the indicator cell.

Good progress. The cell completes without an exception because your three indicator codes now share one valid configuration.

Indicator Cell Not Completing?

  • Check that every indicator code and label remains inside the braces for INDICATORS.
  • Check that each dictionary entry ends with a comma.

Still blocked? Help me check my INDICATORS dictionary for a Python syntax problem.

The repeated request logic now belongs in one function. Its validation check stops the analysis if an API response does not include an observation list.

  • Add a new code cell beneath the indicator cell.
  • Create the reusable fetch function by pasting this code:
def fetch_indicator(indicator_code, label):
    """Fetch one World Bank indicator and return its raw observations."""
    url = f"{API_BASE}/{indicator_code}?format=json&per_page=1000"

    with urllib.request.urlopen(url, timeout=30) as response:
        payload = json.load(response)

    if not isinstance(payload, list) or len(payload) < 2 or payload[1] is None:
        raise RuntimeError(f"No observations returned for {indicator_code}")

    rows = [
        {
            "Year": observation["date"],
            "Rate": observation["value"],
            "Indicator": label,
        }
        for observation in payload[1]
    ]
    return pd.DataFrame(rows)

What Does This Function Do?

  • The url uses the indicator code supplied to the function.
  • The urllib.request.urlopen call retrieves the selected series.
  • The json.load call turns the response into Python data.
  • The payload check prevents missing observations from flowing into the analysis.
  • The returned table keeps only Year, Rate, and Indicator.
  • Run the function cell.

The cell completes without an exception. Your notebook can now retrieve any series listed in INDICATORS through the same function.

Function Cell Not Completing?

  • Check that the entire function body is indented beneath fetch_indicator.
  • Check that the return line aligns with the rows assignment.

Need another pair of eyes? Help me find the indentation problem in fetch_indicator().

Combine and clean every series

The indicator map can drive one download per label. Combining the returned tables gives the cleaning pipeline one dataset to process.

  • Add a new code cell beneath the function cell.
  • Fetch all three series into raw_data by pasting this code:
frames = [
    fetch_indicator(indicator_code, label)
    for indicator_code, label in INDICATORS.items()
]
raw_data = pd.concat(frames, ignore_index=True)

How Are the Series Combined?

  • The list comprehension calls fetch_indicator() once for each entry in INDICATORS.
  • The frames list holds the three returned tables.
  • The pd.concat call stacks those tables into raw_data.
  • The ignore_index=True setting gives the combined rows one continuous index.
  • Run the download cell.

That is a solid milestone. The cell finishes without an exception because raw_data now contains the Total, Male, and Female responses.

Download Cell Not Completing?

  • Confirm that you ran the INDICATORS cell before this cell.
  • Confirm that you ran the fetch_indicator cell before this cell.
  • Retry the cell after confirming your internet connection is active.

Still having trouble? Help me diagnose why the three World Bank series are not downloading.

The combined response has the same missing historical rates and text-based years as the original Total series. Applying one pipeline keeps the three groups comparable.

  • Find the three lines that create and transform total_data.
  • Replace those lines with this all-series cleaning pipeline:
data = raw_data.dropna(subset=["Rate"])
data = data.astype({"Year": "int64", "Rate": "float64"})
data = data.sort_values(["Indicator", "Year"], ignore_index=True)

What Does the Cleaning Pipeline Preserve?

  • The dropna call removes rows without an unemployment rate.
  • The astype call turns years and rates into numeric values.
  • The sort_values call orders every indicator from older years to newer years.
  • The shared pipeline applies the same preparation rules to every demographic series.
  • Run the cleaning cell.

The cell completes without a missing-value or type-conversion exception. The cleaned data table is ready for a chronological comparison.

Cleaning Cell Not Completing?

  • Check that the source table is named raw_data.
  • Check that the column names preserve the capitalization in Year, Rate, and Indicator.

Need help with the pipeline? Help me troubleshoot the combined data cleaning cell.

Draw the demographic comparison

A shared chart places every group on the same axes. Matplotlib can loop through the indicator labels to draw one line per group.

  • Replace the code from print(total_data.head(8)) through plt.show() with this comparison chart:
fig, ax = plt.subplots(figsize=(12, 6), layout="constrained")

for label in INDICATORS.values():
    subset = data[data["Indicator"] == label]
    ax.plot(subset["Year"], subset["Rate"], linewidth=2, label=label)

ax.set_title("Unemployment Trends in India: Modeled ILO Estimates")
ax.set_xlabel("Year")
ax.set_ylabel("Unemployment rate (% of labor force)")
ax.legend()
plt.show()

How Does the Comparison Work?

  • The plt.subplots call creates one figure with one set of axes.
  • The loop filters data to one label at a time.
  • The ax.plot call draws each filtered series with its matching label.
  • The legend connects each line to Total, Male, or Female.
  • Before you run the chart cell, predict whether the three lines will overlap completely.
  • Run the chart cell.

You'll see one chronological chart with three labeled lines named Total, Male, and Female.

The lines cover the cleaned annual timeline from older years on the left to newer years on the right. You can now see demographic differences that the Total series alone could hide.

Missing a Line or Legend Label?

  • Check that the loop uses INDICATORS.values().
  • Check that the filter compares the Indicator column with label.
  • Check that ax.legend() appears before plt.show().

Still missing part of the chart? Help me troubleshoot my three-line unemployment comparison.

✔️ Awesome, I've got everything!

Your notebook now contains the complete three-series comparison.

ⓧ I'd like to double check the full code

import json
import urllib.request

import matplotlib.pyplot as plt
import pandas as pd

API_BASE = "https://api.worldbank.org/v2/country/IND/indicator"
INDICATORS = {
    "SL.UEM.TOTL.ZS": "Total",
    "SL.UEM.TOTL.MA.ZS": "Male",
    "SL.UEM.TOTL.FE.ZS": "Female",
}


def fetch_indicator(indicator_code, label):
    """Fetch one World Bank indicator and return its raw observations."""
    url = f"{API_BASE}/{indicator_code}?format=json&per_page=1000"

    with urllib.request.urlopen(url, timeout=30) as response:
        payload = json.load(response)

    if not isinstance(payload, list) or len(payload) < 2 or payload[1] is None:
        raise RuntimeError(f"No observations returned for {indicator_code}")

    rows = [
        {
            "Year": observation["date"],
            "Rate": observation["value"],
            "Indicator": label,
        }
        for observation in payload[1]
    ]
    return pd.DataFrame(rows)


frames = [
    fetch_indicator(indicator_code, label)
    for indicator_code, label in INDICATORS.items()
]
raw_data = pd.concat(frames, ignore_index=True)

data = raw_data.dropna(subset=["Rate"])
data = data.astype({"Year": "int64", "Rate": "float64"})
data = data.sort_values(["Indicator", "Year"], ignore_index=True)

fig, ax = plt.subplots(figsize=(12, 6), layout="constrained")

for label in INDICATORS.values():
    subset = data[data["Indicator"] == label]
    ax.plot(subset["Year"], subset["Rate"], linewidth=2, label=label)

ax.set_title("Unemployment Trends in India: Modeled ILO Estimates")
ax.set_xlabel("Year")
ax.set_ylabel("Unemployment rate (% of labor force)")
ax.legend()
plt.show()

Your notebook now compares three unemployment series through one reproducible pipeline. Next, you'll measure the peaks, export the evidence, and document what the chart supports.

Export and Document Findings

Your Google Colab notebook now compares the Total, Male, and Female unemployment series in one chronological chart.

A chart becomes defensible when its numerical evidence and method travel with it.

You will print the peak and latest observations for each series. You will also create reusable exports.

Finally, you will document the source, method, findings, and limitations inside the notebook.

In this step, get ready to:
  • Print peak and latest observations for every unemployment series.
  • Export the cleaned data and presentation chart.
  • Document the evidence and its limitations.
Measure the peak and latest values

A descriptive summary turns each line into values you can quote. The notebook also needs to preserve the evidence trail from raw order to cleaned order.

  • Find the raw_data assignment in the data-fetching cell.
  • Expose the raw Total ordering by adding this code directly below that assignment:
print("Raw total-series order from the API:")
print(raw_data[raw_data["Indicator"] == "Total"].head(8))

What Does This Output Prove?

  • The filter selects observations labeled Total from the World Bank Indicators API response.
  • The head(8) call displays enough rows to reveal the newest-first ordering.

Before you run the cell, predict whether the first displayed year will be the newest or the oldest.

  • Run the data-fetching cell.

You should see a table whose first row has year 2025. This confirms that the raw response begins with the newest observation.

Raw Rows Missing?

  • Check that the two print lines sit below the assignment that creates raw_data.
  • Run the earlier cell that defines INDICATORS if the notebook no longer recognizes that variable.

Still stuck? Help me restore the raw Total output.

The cleaned preview records the second half of the evidence trail. It shows the effect of removing missing rates and sorting the observations chronologically.

  • Find the sort_values assignment in the cleaning cell.
  • Expose the cleaned ordering by adding this code directly below that assignment:
print("\nCleaned chronological data:")
print(data.head(8))

What Does the Cleaned Preview Show?

  • The data table already excludes rows with missing rates.
  • The preview exposes the earliest available numeric years after chronological sorting.

Before you run the cell, predict how the first year will differ from the raw preview.

  • Run the cleaning cell.

You should see the cleaned table begin at 1991. The displayed Rate values should contain no blanks.

Cleaned Preview Still Reversed?

  • Check that the preview runs after the sort_values assignment.
  • Rerun the cleaning cell after moving the print lines.

Need help? Help me check the cleaned chronological preview.

Peak and latest observations answer two different questions. The peak identifies the highest recorded rate in each series. The latest row captures the newest available estimate.

  • Add the numerical summaries below the existing plt.show() line by pasting this code:
for label in INDICATORS.values():
    subset = data[data["Indicator"] == label]
    print(f"\n{label} peak observation:")
    print(subset.nlargest(1, "Rate")[["Year", "Rate"]].to_string(index=False))
    print(f"{label} latest observation:")
    print(subset.tail(1)[["Year", "Rate"]].to_string(index=False))

How Are the Summaries Calculated?

  • The loop creates one subset for each label in INDICATORS.
  • The nlargest call selects the row with the highest Rate value.
  • The tail call selects the final row from each chronological series.
  • The to_string call keeps each Year and Rate result compact.

Before you run the cell, predict whether each series has its peak in the latest year.

  • Run the summary cell.

You should see separate peak and latest tables for Total, Male, and Female. Each table should contain one Year and Rate row.

Missing a Summary?

  • Check that the summary loop uses the same indentation on all five lines inside the loop.
  • Run the cells that create INDICATORS and data before rerunning the summary.

Need a hand? Help me troubleshoot the peak and latest tables.

Export the chart and cleaned data

A CSV preserves the cleaned evidence in a reusable table. A PNG preserves the comparison chart for reports or presentations.

  • Find the ax.legend() line in the chart cell.
  • Save the chart before it displays by inserting this code directly below that line:
plt.savefig(
    "india_unemployment_trends.png",
    dpi=150,
    bbox_inches="tight",
)

What Does the Export Code Do?

  • The savefig call writes the current chart to india_unemployment_trends.png.
  • The dpi=150 setting gives the chart enough resolution for presentation use.
  • The bbox_inches="tight" setting trims excess space around the saved figure.
  • Saving before plt.show() ensures the current figure is written before it displays.
  • Run the chart cell.
  • Check the runtime file list for the new image.

You should see india_unemployment_trends.png in the file list. The displayed chart should still show the three labeled chronological lines.

PNG File Missing?

  • Check that plt.savefig appears before plt.show().
  • Rerun the chart cell after correcting the order.

Still missing the file? Help me troubleshoot the PNG export.

The final export writes the cleaned table with its Year, Rate, and Indicator columns. A confirmation message makes the completed export easy to verify.

  • Add the table export below the summary loop by pasting this code:
data.to_csv("india_unemployment_trends.csv", index=False)
print("\nSaved: india_unemployment_trends.csv and india_unemployment_trends.png")

How Is the Table Exported?

  • The to_csv call writes every cleaned row to india_unemployment_trends.csv.
  • The index=False argument keeps the DataFrame row index out of the exported file.
  • The final print line confirms the expected CSV and PNG filenames.

Before you run the cell, predict which two filenames the confirmation will mention.

  • Run the export cell.
  • Check the runtime file list for the cleaned table.

You should see Saved: india_unemployment_trends.csv and india_unemployment_trends.png below the summaries. The runtime file list should contain both exports.

CSV File Missing?

  • Check that the export cell runs after the cell that creates data.
  • Check that the filename ends with .csv.

Need more help? Help me troubleshoot the CSV export.

✔️ Awesome, I've got everything!

Your numerical summaries and both export files are ready. Double-check that your notebook has saved the latest code.

ⓧ I'd like to double check the full code

import json
import urllib.request

import matplotlib.pyplot as plt
import pandas as pd

API_BASE = "https://api.worldbank.org/v2/country/IND/indicator"
INDICATORS = {
    "SL.UEM.TOTL.ZS": "Total",
    "SL.UEM.TOTL.MA.ZS": "Male",
    "SL.UEM.TOTL.FE.ZS": "Female",
}


def fetch_indicator(indicator_code, label):
    """Fetch one World Bank indicator and return its raw observations."""
    url = f"{API_BASE}/{indicator_code}?format=json&per_page=1000"

    with urllib.request.urlopen(url, timeout=30) as response:
        payload = json.load(response)

    if not isinstance(payload, list) or len(payload) < 2 or payload[1] is None:
        raise RuntimeError(f"No observations returned for {indicator_code}")

    rows = [
        {
            "Year": observation["date"],
            "Rate": observation["value"],
            "Indicator": label,
        }
        for observation in payload[1]
    ]
    return pd.DataFrame(rows)


frames = [
    fetch_indicator(indicator_code, label)
    for indicator_code, label in INDICATORS.items()
]
raw_data = pd.concat(frames, ignore_index=True)

print("Raw total-series order from the API:")
print(raw_data[raw_data["Indicator"] == "Total"].head(8))

data = raw_data.dropna(subset=["Rate"])
data = data.astype({"Year": "int64", "Rate": "float64"})
data = data.sort_values(["Indicator", "Year"], ignore_index=True)

print("\nCleaned chronological data:")
print(data.head(8))

fig, ax = plt.subplots(figsize=(12, 6), layout="constrained")

for label in INDICATORS.values():
    subset = data[data["Indicator"] == label]
    ax.plot(subset["Year"], subset["Rate"], linewidth=2, label=label)

ax.set_title("Unemployment Trends in India: Modeled ILO Estimates")
ax.set_xlabel("Year")
ax.set_ylabel("Unemployment rate (% of labor force)")
ax.legend()

plt.savefig(
    "india_unemployment_trends.png",
    dpi=150,
    bbox_inches="tight",
)
plt.show()

for label in INDICATORS.values():
    subset = data[data["Indicator"] == label]
    print(f"\n{label} peak observation:")
    print(subset.nlargest(1, "Rate")[["Year", "Rate"]].to_string(index=False))
    print(f"{label} latest observation:")
    print(subset.tail(1)[["Year", "Rate"]].to_string(index=False))

data.to_csv("india_unemployment_trends.csv", index=False)
print("\nSaved: india_unemployment_trends.csv and india_unemployment_trends.png")
Document the analysis

A Markdown section gives readers the context needed to interpret the chart. It also records decisions that the image alone cannot show.

What Makes the Method Reproducible?

The method names the transformations that connect the raw response to the final chart. Another reader can rerun those transformations instead of guessing how the data changed.

  • Enter Findings as the next heading.
  • Write one Total observation using its printed peak and latest Year and Rate values.
  • Write one Male observation using its printed peak and latest Year and Rate values.
  • Write one Female observation using its printed peak and latest Year and Rate values.
  • Enter Limitations as the final heading.
  • Identify the unemployment rates as modeled ILO estimates.
  • Note that the live source can revise historical or latest values.
  • State that a visual association does not establish causation.

Why Do Limitations Matter?

Limitations define what the evidence can support. They protect your analysis from claims that extend beyond the data.

  • Run the Markdown cell to render the documentation.

You should see sections titled Data Source, Method, Findings, and Limitations. The findings should reference values printed by the current live response.

Before the final run, predict the order of the two preview tables and the filenames shown by the saved confirmation.

  • Run each code cell from top to bottom.
  • Review the rendered documentation below the final output.

You should see the raw Total table begin with 2025. The cleaned table should begin with 1991 and contain no blank Rate rows.

You should see one peak table and one latest table for Total, Male, and Female. The final output line should read Saved: india_unemployment_trends.csv and india_unemployment_trends.png.

  • Check the runtime file list for both completed exports.

You should see india_unemployment_trends.csv and india_unemployment_trends.png.

That is the analysis made defensible. Your notebook now connects live source data to numerical evidence, presentation-ready exports, and documented limits.

Secret mission

Secret Mission: Reveal the Five-Year Trend

Annual unemployment values preserve every yearly movement. Those movements can obscure the broader direction. Add a five-year moving average beside the Total series to make the long-term signal visible.

Clean Up Your Resources

Clean Up Your Resources

There are no ongoing costs for this analysis. Decide whether to keep your notebook, pause your work, or delete the files entirely.

Resources you used:

  • Your india_unemployment_trends notebook stored in Google Drive.
  • The exported india_unemployment_trends.csv file in the temporary Google Colab runtime or on your Windows device if downloaded.
  • The exported india_unemployment_trends.png file in the temporary Google Colab runtime or on your Windows device if downloaded.

Keep everything running

Keeping everything requires no cleanup. Choose this option if you want to share the analysis or continue refining your findings.

  • Leave the india_unemployment_trends notebook in Google Drive.
  • Keep any downloaded copies of india_unemployment_trends.csv or india_unemployment_trends.png on your Windows device.
  • Rerun the export cells if the temporary runtime copies disappear after inactivity.

Pause - I'll come back to this later

Pausing leaves the notebook in Google Drive while the temporary runtime can expire after inactivity. Your code stays available for your next session.

  • Close the Google Colab browser tab.
  • Keep any downloaded CSV or PNG copies on your Windows device.
  • Return to the india_unemployment_trends notebook from Google Drive when you are ready to continue.

Delete - I don't want to use this again

Deleting everything removes the saved notebook plus its generated exports. Google Colab and the World Bank API leave no paid resources running.

  • In Google Drive, locate the india_unemployment_trends notebook.
  • Open the notebook's file actions menu.
  • Choose the option that moves the notebook to trash.
  • Permanently delete the notebook from the Google Drive trash.
  • In Google Colab, locate india_unemployment_trends.csv plus india_unemployment_trends.png in the runtime file browser.
  • Delete both runtime files.
  • On your Windows device, open the folder where your browser saves downloads.
  • Select any downloaded copies of india_unemployment_trends.csv or india_unemployment_trends.png.
  • Delete the selected copies.

Your notebook and exported files are now removed. No other project resources need cleanup.

Nice Work!

Nice Work!

You made it! Your Google Colab notebook now turns live World Bank Indicators API data into a documented analysis of total, male, and female unemployment trends.

You've learned how to:

  • Built a reproducible notebook with pinned pandas and Matplotlib versions. Retrieved official unemployment indicators without an API key.
  • Diagnosed the raw API ordering problem. Removed missing values before converting the columns to numeric types. Sorted the cleaned observations into chronological order.
  • Compared total, male, and female rates in one chronological chart. Printed peak and latest observations for each series. Exported the evidence as india_unemployment_trends.csv and india_unemployment_trends.png.
  • Completed the optional Secret Mission by adding a five-year moving average that reveals the longer-term direction while preserving the annual observations.

Ready to quiz yourself?