Analyze Global Development Data
Explore global health and wealth trends using pandas and seaborn.
Introduction
30 Second Summary
Every country in the world has a story of progress, setbacks, and change. Over the past 55 years, billions of people have seen dramatic shifts in health, wealth, and population. These patterns are hidden in data, waiting for someone to visualize them.
In this project, you will build a data analysis notebook that explores global development trends from 1952 to 2007 using Python. You'll load a real-world dataset, create visualizations, and uncover insights about how life expectancy and wealth have changed across continents.
What You'll Build
A complete Jupyter Notebook that loads, explores, and visualizes the Gapminder dataset using pandas, matplotlib, and seaborn.
By the end of this project, you'll have:
- A working Jupyter Notebook that loads and explores a real-world dataset using pandas.
- A collection of publication-quality visualizations including scatter plots, line charts, bar charts, histograms, and box plots that reveal global health and wealth trends.
- Hands-on experience with the core data analysis workflow: load, explore, transform, visualize, interpret.
- Secret Mission: Build a faceted multi-panel visualization comparing all continents side-by-side across decades.
Are there any prerequisites?
You only need basic Python familiarity (variables, loops, functions). No prior pandas or data science experience is needed.
Before We Start
Before We Start...
Set Up Your Environment
Before you can analyze data, you need the right tools installed. This project uses three Python libraries that form the backbone of data science work.
In this step, you will install pandas (for working with tabular data), matplotlib (for creating charts), and seaborn (for statistical visualizations). Then you will launch a Jupyter Notebook to confirm everything is working.
In this step, get ready to:
- Install Python libraries (pandas, matplotlib, seaborn).
- Launch Jupyter Notebook and create a new notebook.
- Verify your setup by running an import cell.
Install the required packages
This project requires Python 3.11 or higher because pandas 3.0.3 requires it. Let's confirm you have the right version.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check your Python version by running this command:
python3 --version
What about Windows?
On Windows, the command may be python --version instead of python3 --version. Use whichever one responds with a version number.
✔️ I see version 3.11 or higher
You are ready to install the project libraries. Continue to the next instruction below.
ⓧ I see an older version
Your system has an older Python that won't work with pandas 3.0.3. You need to install Python 3.11 or newer.
On macOS, your system may ship with an older Python by default. Installing a newer version from python.org does not override the system Python, but you will need to use the full version command (python3.11 or python3.12) or use a version manager.
- Visit python.org/downloads and download the latest Python 3 installer for your operating system.
- Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal, then run python3 --version again to confirm you see 3.11 or higher.
ⓧ Command not found
Python is not installed on your machine yet.
- Visit python.org/downloads and download the latest Python 3 installer for your operating system.
- Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal, then run python3 --version (macOS) or python --version (Windows) to confirm you see 3.11 or higher.
- Install the three libraries your project needs by running this command:
pip install pandas==3.0.3 matplotlib==3.10.9 seaborn==0.13.2
What does this command do?
This tells pip (Python's package installer) to download and install exact versions of three libraries. Pinning versions ensures everyone working through this project gets the same behavior.
- pandas 3.0.3 provides DataFrames for working with tabular data.
- matplotlib 3.10.9 is the foundational charting library for Python.
- seaborn 0.13.2 builds on matplotlib with a higher-level API for statistical plots.
You should see download progress bars and a success message for each package.
Seeing a permission error?
If you see a permissions error, try running the command with the --user flag at the end. If pip is not found, try pip3 instead.
Help me troubleshoot my pip install error
Launch Jupyter Notebook
Jupyter Notebook gives you an interactive environment where you can write code, run it cell by cell, and see results inline. It is the standard tool for exploratory data analysis.
- Install Jupyter Notebook by running this command:
pip install notebook
- Once the install finishes, launch Jupyter by running:
jupyter notebook
Your default web browser opens with the Jupyter file browser. You should see a directory listing of your current folder.
What should I see?
A browser tab opens at a URL like http://localhost:8888. The page shows a file browser with your current folder contents. Your terminal stays running in the background to keep the notebook server alive.
- In the Jupyter browser tab, click New in the top right corner.
- Select Python 3 from the dropdown. A new notebook tab opens.
- Click the title text at the top of the page (it says Untitled by default).
- Rename the notebook to gapminder_analysis and press Enter. Jupyter automatically saves it as gapminder_analysis.ipynb.
Browser didn't open?
Check your terminal output for a URL containing localhost:8888 with a token parameter. Copy and paste that full URL into your browser manually.
Help me access Jupyter Notebook in my browser
Verify the setup
With your notebook open, you can confirm all three libraries are importable and working.
- In the first empty cell of your notebook, paste the following code:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
print(f"pandas version: {pd.__version__}")
print(f"matplotlib version: {plt.matplotlib.__version__}")
print(f"seaborn version: {sns.__version__}")
print("Setup complete!")
- Run the cell by pressing Shift+Enter.
What does this code do?
Each import line loads a library and gives it a short alias for convenience. These aliases (pd, plt, sns) are industry-standard conventions you will see in every data science notebook.
The print statements confirm the exact version of each library. If any import fails, Python raises an error immediately so you know which library is missing.
You should see output showing the three version numbers followed by Setup complete! with no error messages.
Seeing a ModuleNotFoundError?
This means the Jupyter kernel is using a different Python than the one where you installed the libraries. Try running !pip install pandas matplotlib seaborn directly inside a notebook cell (the ! tells Jupyter to run a terminal command).
Help me fix my ModuleNotFoundError
✔️ Awesome, I've got everything!
Great. Double check you see version numbers printed with no errors, then continue to Step 2.
ⓧ I'd like to double check the full code
Here is the complete cell you should have in your notebook:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
print(f"pandas version: {pd.__version__}")
print(f"matplotlib version: {plt.matplotlib.__version__}")
print(f"seaborn version: {sns.__version__}")
print("Setup complete!")
Your environment is ready. Next up, you will load a real-world dataset and explore what is inside it.
Load and Explore the Dataset
Your environment is ready with all the libraries installed. Now it's time to bring in some real-world data.
Before you can visualize anything, you need to understand the shape and quality of your data. In this step, you'll load the Gapminder dataset directly from the internet into a pandas DataFrame and run a series of exploration commands to learn what you're working with.
In this step, get ready to:
- Load the Gapminder CSV file from a URL into a DataFrame.
- Explore the dataset's structure, columns, and summary statistics.
- Confirm the data has no missing values or unexpected issues.
Load the data from a URL
The Gapminder dataset is a CSV file hosted publicly on GitHub. Instead of downloading it manually, you can read it directly into memory with a single pandas command.
- In your notebook, create a new code cell below Cell 1 and load the dataset by running this code:
url = "https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv"
df = pd.read_csv(url)
print(f"Dataset loaded: {df.shape[0]} rows, {df.shape[1]} columns")
What does this code do?
url stores the web address of the raw CSV file on GitHub.
pd.read_csv(url) downloads the file and converts it into a DataFrame called df. A DataFrame is a table-like structure with rows and columns that pandas uses to organize data.
df.shape returns the number of rows and columns, giving you a quick size check.
- Run the cell by pressing Shift+Enter.
- You should see the output: Dataset loaded: 1704 rows, 6 columns.
Seeing a connection error?
Make sure you have an active internet connection. The pd.read_csv() function needs to download the file from GitHub.
Check that the URL string has no typos or extra spaces. Copy it exactly as shown above.
Help me debug a connection error when loading a CSV with pandas
Explore the structure of your data
You know the dataset has 1,704 rows and 6 columns, but you don't yet know what those columns contain or what the values look like. Pandas has built-in exploration methods that answer these questions in seconds.
- Create a new cell and preview the first 10 rows by running:
df.head(10)
- Run the cell. You should see a table showing the first 10 rows with columns: country, continent, year, lifeExp, pop, and gdpPercap.
- Create a new cell and inspect the data types and memory usage by running:
df.info()
- Run the cell. This shows each column's data type (object for text, int64 for whole numbers, float64 for decimals) and confirms there are 1704 non-null entries per column.
- Create a new cell and generate summary statistics by running:
df.describe()
- Run the cell. You should see the count, mean, standard deviation, min, max, and quartile values for the numeric columns.
- Create a new cell and check how many countries belong to each continent by running:
df['continent'].value_counts()
- Run the cell. You should see a breakdown of how many data points each continent has, with Africa having the most entries.
What are these exploration methods doing?
- .head(10) shows the first N rows so you can eyeball the actual values and column names.
- .info() reveals data types and whether any column has missing values.
- .describe() calculates statistics (mean, median, min, max) for every numeric column at once.
- .value_counts() counts how often each unique value appears in a column. Perfect for categorical data like continents.
Seeing a NameError for df?
This means Cell 2 (where you loaded the data) hasn't been run yet. In Jupyter, each cell must be executed in order. Go back to Cell 2, click inside it, and press Shift+Enter to run it first.
Help me fix a NameError in my Jupyter Notebook
Check for data quality issues
Before visualizing data, you need to confirm it's clean. Missing values or unexpected ranges can produce misleading charts. This quick check gives you confidence that the dataset is ready for analysis.
- Create a new cell and check for missing values and dataset boundaries by running:
print(f"Missing values:\n{df.isnull().sum()}")
print(f"\nUnique countries: {df['country'].nunique()}")
print(f"Year range: {df['year'].min()} - {df['year'].max()}")
What does this code do?
- df.isnull().sum() counts the number of missing values in each column. Zero across the board means no data gaps.
- df['country'].nunique() counts how many distinct countries are in the dataset.
- .min() and .max() on the year column tell you the time span the data covers.
- Run the cell. You should see 0 missing values for every column, 142 unique countries, and a year range of 1952 to 2007.
Why does clean data matter?
If a column had missing values, pandas would silently skip those rows in calculations. Your averages and charts would be based on incomplete data without you realizing it. Always check before you visualize.
Seeing missing values in your output?
The Gapminder dataset from this URL should have zero missing values. If you see non-zero counts, double check that your Cell 2 uses the exact URL shown above. A different dataset source may have gaps.
Help me understand why my dataset has missing values
✔️ Awesome, I've got everything!
Great. Double check you've run all cells in order (1 through 7) and your notebook shows no errors.
ⓧ I'd like to double check the full code
Here's everything your notebook should contain so far. Each section below is a separate cell.
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
print(f"pandas version: {pd.__version__}")
print(f"matplotlib version: {plt.matplotlib.__version__}")
print(f"seaborn version: {sns.__version__}")
print("Setup complete!")
url = "https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv"
df = pd.read_csv(url)
print(f"Dataset loaded: {df.shape[0]} rows, {df.shape[1]} columns")
df.head(10)
df.info()
df.describe()
df['continent'].value_counts()
print(f"Missing values:\n{df.isnull().sum()}")
print(f"\nUnique countries: {df['country'].nunique()}")
print(f"Year range: {df['year'].min()} - {df['year'].max()}")
You now know exactly what's in your dataset: 142 countries, 55 years of data, and zero missing values. Next up, you'll turn these numbers into visualizations that reveal the story hiding inside the data.
Create Your First Visualizations
You now know your dataset inside and out. It has 1,704 rows, 6 columns, zero missing values, and covers 142 countries across 55 years.
Numbers alone don't tell the full story though. In this step, you'll create three classic chart types that reveal the relationship between wealth, health, and time across the globe.
In this step, get ready to:
- Create a scatter plot showing the relationship between GDP and life expectancy.
- Plot a line chart of life expectancy trends over time by continent.
- Build a bar chart of the top 10 wealthiest countries.
Create a scatter plot of wealth vs health
The scatter plot is the signature Gapminder visualization. It maps GDP per capita on the x-axis against life expectancy on the y-axis, revealing whether wealthier countries tend to have longer-living populations.
- Create a new code cell in your notebook (Cell 8) and paste in the following code:
plt.figure(figsize=(10, 6))
sns.scatterplot(data=df[df['year'] == 2007], x='gdpPercap', y='lifeExp',
hue='continent', size='pop', sizes=(20, 500), alpha=0.7)
plt.xscale('log')
plt.xlabel('GDP per Capita (USD, log scale)')
plt.ylabel('Life Expectancy (years)')
plt.title('Wealth vs Health of Nations (2007)')
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
What does this code do?
This cell filters the DataFrame to only the year 2007, then plots each country as a dot.
- hue='continent' colors each dot by its continent so you can spot geographic patterns at a glance.
- size='pop' scales each dot's area by the country's population. Larger countries get bigger dots.
- plt.xscale('log') applies a logarithmic scale to the x-axis. GDP values range from hundreds to tens of thousands, so a log scale spreads them out evenly.
- Run the cell by pressing Shift+Enter.
- You should see a colorful scatter plot with dots of varying sizes. Countries in Africa (lower-left) tend to have lower GDP and lower life expectancy, while European countries cluster in the upper-right.
Chart not showing up?
- Make sure you ran Cell 1 (the imports cell) earlier in this session. If you restarted the notebook kernel, you need to re-run all previous cells first.
- Check that plt.show() is the last line. Without it, some notebook environments won't render the figure.
Why isn't my scatter plot rendering in Jupyter?
Plot life expectancy trends over time
A line chart is ideal for showing change over time. You'll group the data by year and continent to see how each region's average life expectancy has evolved from 1952 to 2007.
- Create a new code cell (Cell 9) and paste in the following code:
continent_avg = df.groupby(['year', 'continent'])['lifeExp'].mean().reset_index()
plt.figure(figsize=(10, 6))
sns.lineplot(data=continent_avg, x='year', y='lifeExp', hue='continent', marker='o')
plt.xlabel('Year')
plt.ylabel('Average Life Expectancy (years)')
plt.title('Life Expectancy Trends by Continent (1952-2007)')
plt.tight_layout()
plt.show()
What does this code do?
The first line does the heavy lifting. groupby(['year', 'continent']) splits the data into groups for each year-continent combination, then .mean() calculates the average life expectancy within each group.
The result is a new DataFrame called continent_avg with one row per year per continent. sns.lineplot then draws a line for each continent, with markers at each data point.
- Run the cell by pressing Shift+Enter.
- You should see five colored lines trending upward. Every continent's life expectancy has improved over time, but at different rates.
Lines look jagged or unexpected?
- Make sure you typed continent_avg (with an underscore) in both the variable assignment and the data= parameter.
- If you see a NameError, confirm that Cell 2 (which loads df) was run earlier in this session.
Why is my line chart showing an error or unexpected shape?
Chart the top 10 wealthiest countries
A horizontal bar chart makes it easy to compare ranked values. You'll filter to just 2007, sort by GDP per capita, and display the top 10.
- Create a new code cell (Cell 10) and paste in the following code:
top_gdp = df[df['year'] == 2007].nlargest(10, 'gdpPercap')
plt.figure(figsize=(10, 6))
sns.barplot(data=top_gdp, x='gdpPercap', y='country', hue='continent', dodge=False)
plt.xlabel('GDP per Capita (USD)')
plt.ylabel('')
plt.title('Top 10 Wealthiest Countries (2007)')
plt.tight_layout()
plt.show()
What does this code do?
nlargest(10, 'gdpPercap') is a pandas shortcut that returns the 10 rows with the highest values in the specified column. This avoids sorting the entire DataFrame manually.
dodge=False tells seaborn to keep bars aligned on the same row instead of splitting them side-by-side. The hue='continent' still colors each bar by its continent for easy geographic identification.
- Run the cell by pressing Shift+Enter.
- You should see a horizontal bar chart with 10 country names on the y-axis and colored bars extending to the right. Norway, Kuwait, and Singapore typically appear near the top.
Bars overlapping or labels cut off?
- plt.tight_layout() should prevent labels from being clipped. If you still see cutoff text, try increasing the figure width in figsize=(12, 6).
- If bars look stacked oddly, confirm dodge=False is present. Without it, seaborn may try to offset bars for different hue values.
Why does my bar chart look wrong or have overlapping elements?
✔️ Awesome, I've got everything!
Great. Double check that you've run all three cells (8, 9, and 10) and see a chart rendered below each one.
ⓧ I'd like to double check the full code
Here are the three new cells you added in this step. Compare them against your notebook:
# --- Cell 8: Scatter Plot - GDP vs Life Expectancy ---
plt.figure(figsize=(10, 6))
sns.scatterplot(data=df[df['year'] == 2007], x='gdpPercap', y='lifeExp',
hue='continent', size='pop', sizes=(20, 500), alpha=0.7)
plt.xscale('log')
plt.xlabel('GDP per Capita (USD, log scale)')
plt.ylabel('Life Expectancy (years)')
plt.title('Wealth vs Health of Nations (2007)')
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
# --- Cell 9: Line Chart - Life Expectancy Over Time ---
continent_avg = df.groupby(['year', 'continent'])['lifeExp'].mean().reset_index()
plt.figure(figsize=(10, 6))
sns.lineplot(data=continent_avg, x='year', y='lifeExp', hue='continent', marker='o')
plt.xlabel('Year')
plt.ylabel('Average Life Expectancy (years)')
plt.title('Life Expectancy Trends by Continent (1952-2007)')
plt.tight_layout()
plt.show()
# --- Cell 10: Bar Chart - Top 10 Countries by GDP ---
top_gdp = df[df['year'] == 2007].nlargest(10, 'gdpPercap')
plt.figure(figsize=(10, 6))
sns.barplot(data=top_gdp, x='gdpPercap', y='country', hue='continent', dodge=False)
plt.xlabel('GDP per Capita (USD)')
plt.ylabel('')
plt.title('Top 10 Wealthiest Countries (2007)')
plt.tight_layout()
plt.show()
You now have three publication-quality visualizations that tell a clear story about global development. Next up, you'll dig deeper with grouping, filtering, and country-level analysis to answer specific questions about the data.
Group, Filter, and Dig Deeper
Your visualizations already tell a powerful story about global trends. But charts alone don't answer specific questions like "which continent is doing best overall?" or "which countries improved the most?"
In this step, you'll use pandas grouping and filtering to slice the data and answer targeted questions. This is where data analysis goes from pretty pictures to real insights.
In this step, get ready to:
- Compare continents using groupby and aggregation.
- Track a single country's journey over time with subplots.
- Find the biggest improvers by merging and computing differences.
Compare continents with groupby
Your line chart showed trends over time, but what does each continent look like as a whole in 2007? The .groupby() method lets you split the data by category and calculate summary statistics for each group.
- Create a new cell in your notebook (Cell 11) and calculate continent-level statistics by running this code:
summary_2007 = df[df['year'] == 2007].groupby('continent').agg(
avg_life_exp=('lifeExp', 'mean'),
avg_gdp=('gdpPercap', 'mean'),
total_pop=('pop', 'sum'),
num_countries=('country', 'count')
).round(1)
print(summary_2007)
What does this code do?
This code chains three operations together to produce a continent-level summary table.
- df[df['year'] == 2007] filters the DataFrame to only 2007 data.
- .groupby('continent') splits the filtered data into one group per continent.
- .agg() applies different aggregation functions to different columns. The named-tuple syntax (e.g. avg_life_exp=('lifeExp', 'mean')) lets you rename the output columns at the same time.
- .round(1) keeps the table clean by rounding to one decimal place.
- Run the cell. You should see a table with five rows (one per continent) and four columns showing average life expectancy, average GDP, total population, and number of countries.
Seeing a KeyError?
- Double check that column names match exactly: lifeExp, gdpPercap, pop, country. These are case-sensitive.
- Make sure Cell 2 (loading the data) ran successfully before this cell.
Why is my groupby returning an error?
Track a country over time
Continent averages are useful, but they hide individual country stories. China's transformation from one of the poorest countries to a global economic powerhouse is one of the most dramatic stories in this dataset.
- Create a new cell (Cell 12) and build a two-panel subplot showing China's journey by running this code:
country_data = df[df['country'] == 'China']
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(country_data['year'], country_data['lifeExp'], marker='o', color='coral')
axes[0].set_title("China: Life Expectancy Over Time")
axes[0].set_xlabel('Year')
axes[0].set_ylabel('Life Expectancy (years)')
axes[1].plot(country_data['year'], country_data['gdpPercap'], marker='s', color='steelblue')
axes[1].set_title("China: GDP per Capita Over Time")
axes[1].set_xlabel('Year')
axes[1].set_ylabel('GDP per Capita (USD)')
plt.tight_layout()
plt.show()
What does this code do?
- df[df['country'] == 'China'] filters the full DataFrame to only rows where the country is China.
- plt.subplots(1, 2) creates a figure with two side-by-side panels. The axes array lets you control each panel independently.
- axes[0] plots life expectancy on the left panel. axes[1] plots GDP per capita on the right. This puts two related stories next to each other for easy comparison.
- Run the cell. You should see two line charts side by side showing China's life expectancy rising steadily and its GDP per capita spiking dramatically after 1980.
Only seeing one chart or a blank output?
- Make sure you have plt.show() at the end of the cell. Without it, Jupyter may not render the figure.
- Check that 'China' is capitalized exactly. The country names in the dataset use title case.
Why isn't my subplot showing both panels?
Find the biggest improvers
Which countries gained the most life expectancy over the 55-year span of this dataset? To answer this, you need to compare 1952 values against 2007 values for each country. This requires a merge operation, which is how pandas joins two DataFrames on a shared column.
- Create a new cell (Cell 13) and calculate the biggest life expectancy improvers by running this code:
life_exp_1952 = df[df['year'] == 1952][['country', 'lifeExp']].rename(columns={'lifeExp': 'lifeExp_1952'})
life_exp_2007 = df[df['year'] == 2007][['country', 'lifeExp']].rename(columns={'lifeExp': 'lifeExp_2007'})
improvement = life_exp_1952.merge(life_exp_2007, on='country')
improvement['gain'] = improvement['lifeExp_2007'] - improvement['lifeExp_1952']
top_improvers = improvement.nlargest(10, 'gain')
plt.figure(figsize=(10, 6))
sns.barplot(data=top_improvers, x='gain', y='country', color='seagreen')
plt.xlabel('Life Expectancy Gain (years)')
plt.ylabel('')
plt.title('Top 10 Countries: Life Expectancy Improvement (1952-2007)')
plt.tight_layout()
plt.show()
What does this code do?
- The first two lines create separate DataFrames for 1952 and 2007, each with only country and lifeExp columns. The .rename() gives them unique column names so they don't clash when merged.
- .merge(..., on='country') joins the two DataFrames side-by-side where country names match. This gives you one row per country with both its 1952 and 2007 life expectancy.
- Subtracting the two columns creates a gain column showing how many years of life expectancy each country added.
- .nlargest(10, 'gain') picks the top 10 countries with the largest improvement, which are then plotted as a horizontal bar chart.
- Run the cell. You should see a horizontal bar chart showing 10 countries ranked by their life expectancy gain from 1952 to 2007.
Seeing an empty chart or merge error?
- If the chart is blank, check that df still contains data. Re-run Cell 2 if needed.
- If you see a merge error, make sure both life_exp_1952 and life_exp_2007 have a country column. The [['country', 'lifeExp']] selector must include both columns.
Why is my pandas merge failing?
✔️ Awesome, I've got everything!
Great. Double check that your notebook is saved before moving on.
ⓧ I'd like to double check the full code
Here are the three cells you added in this step. Compare them against your notebook:
# Cell 11: Continent Summary Statistics
summary_2007 = df[df['year'] == 2007].groupby('continent').agg(
avg_life_exp=('lifeExp', 'mean'),
avg_gdp=('gdpPercap', 'mean'),
total_pop=('pop', 'sum'),
num_countries=('country', 'count')
).round(1)
print(summary_2007)
# Cell 12: Country Deep Dive - China
country_data = df[df['country'] == 'China']
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(country_data['year'], country_data['lifeExp'], marker='o', color='coral')
axes[0].set_title("China: Life Expectancy Over Time")
axes[0].set_xlabel('Year')
axes[0].set_ylabel('Life Expectancy (years)')
axes[1].plot(country_data['year'], country_data['gdpPercap'], marker='s', color='steelblue')
axes[1].set_title("China: GDP per Capita Over Time")
axes[1].set_xlabel('Year')
axes[1].set_ylabel('GDP per Capita (USD)')
plt.tight_layout()
plt.show()
# Cell 13: Biggest Improvers
life_exp_1952 = df[df['year'] == 1952][['country', 'lifeExp']].rename(columns={'lifeExp': 'lifeExp_1952'})
life_exp_2007 = df[df['year'] == 2007][['country', 'lifeExp']].rename(columns={'lifeExp': 'lifeExp_2007'})
improvement = life_exp_1952.merge(life_exp_2007, on='country')
improvement['gain'] = improvement['lifeExp_2007'] - improvement['lifeExp_1952']
top_improvers = improvement.nlargest(10, 'gain')
plt.figure(figsize=(10, 6))
sns.barplot(data=top_improvers, x='gain', y='country', color='seagreen')
plt.xlabel('Life Expectancy Gain (years)')
plt.ylabel('')
plt.title('Top 10 Countries: Life Expectancy Improvement (1952-2007)')
plt.tight_layout()
plt.show()
You've moved beyond surface-level charts into real analytical territory. Next up, you'll explore how data is distributed across countries using histograms and box plots.
Analyze Distributions and Document Findings
You've grouped, filtered, and compared countries to answer targeted questions about the data. Now it's time to look at how values are spread across the entire dataset.
Distribution charts (histograms and box plots) reveal patterns that averages hide. They show you the full range of outcomes, where most countries cluster, and which ones are outliers. You'll finish by documenting your findings in a markdown cell, which is what separates a real analysis from a collection of charts.
In this step, get ready to:
- Create histograms comparing life expectancy distributions across decades.
- Build a box plot showing continental variation and outliers.
- Document your key findings in a markdown narrative cell.
Compare life expectancy distributions across time
An average tells you one number, but a histogram shows the full picture. You can see whether most countries cluster together or are spread far apart.
- Create a new code cell in your notebook and add the following code to compare how life expectancy was distributed in 1952 versus 2007:
# Compare life expectancy distributions: 1952 vs 2007
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 1952 distribution with KDE curve
sns.histplot(data=df[df['year'] == 1952], x='lifeExp', bins=20, kde=True, ax=axes[0], color='salmon')
axes[0].set_title('Life Expectancy Distribution (1952)')
axes[0].set_xlabel('Life Expectancy (years)')
# 2007 distribution with KDE curve
sns.histplot(data=df[df['year'] == 2007], x='lifeExp', bins=20, kde=True, ax=axes[1], color='steelblue')
axes[1].set_title('Life Expectancy Distribution (2007)')
axes[1].set_xlabel('Life Expectancy (years)')
plt.tight_layout()
plt.show()
What does this code do?
This creates two side-by-side histograms using matplotlib's subplot system. Each histogram shows how many countries fall into each life expectancy range for a given year.
- sns.histplot with kde=True overlays a smooth curve (kernel density estimate) that shows the overall shape of the distribution.
- bins=20 splits the life expectancy range into 20 equal intervals, so you can see granular differences.
- The salmon (1952) and steelblue (2007) colors make it easy to compare the two eras at a glance.
- Run the cell by pressing Shift+Enter.
- You should see two histograms side by side. The 1952 chart shows countries spread widely across life expectancies, while the 2007 chart clusters most countries toward the higher end (70-80 years).
Not seeing two side-by-side charts?
- Make sure you ran Cell 2 first (loading the dataset). The variable df must exist before you can filter it.
- Check for a typo in the column name. It must be lifeExp with a capital E, not lifeexp or life_exp.
Help me fix my histogram code
Visualize continental variation with a box plot
A box plot shows the median, the middle 50% of values, and any outliers for each group. This makes it perfect for comparing how much variation exists within each continent.
- Create a new code cell and add the following box plot code:
# Box plot showing life expectancy spread within each continent
plt.figure(figsize=(10, 6))
sns.boxplot(data=df[df['year'] == 2007], x='continent', y='lifeExp', hue='continent', palette='Set2')
plt.xlabel('Continent')
plt.ylabel('Life Expectancy (years)')
plt.title('Life Expectancy by Continent (2007)')
plt.tight_layout()
plt.show()
What does this code do?
sns.boxplot draws one box per continent. Each box shows the median (center line), the interquartile range (the box itself, containing the middle 50% of countries), and whiskers extending to the rest of the data.
Any dots beyond the whiskers are outliers. The palette='Set2' parameter applies a distinct color to each continent for clarity.
- Run the cell with Shift+Enter.
- You should see a box plot with five colored boxes. Notice that Africa's box is the tallest (widest spread), while Europe's is the shortest and highest positioned.
Seeing a warning about hue?
- Make sure you included hue='continent' in the sns.boxplot() call. This is what assigns colors to each continent.
- If you see a plot but with no colors, double-check that the palette parameter is spelled correctly.
Help me debug my box plot
Document your findings in markdown
Writing down what you discovered is what separates analysis from just making charts. A markdown cell lets you add narrative text directly into your notebook alongside the code.
- Create a new cell in your notebook.
- Change the cell type from Code to Markdown using the dropdown in the toolbar at the top of the notebook.
- Paste the following into the markdown cell:
## Key Findings
- Global life expectancy increased dramatically from 1952 to 2007
- There is a strong positive correlation between GDP per capita and life expectancy
- Africa has the widest spread in life expectancy, with some countries making huge gains and others stagnating
- Asian countries (especially China and India) show the most dramatic population growth
Why add markdown to a notebook?
Jupyter Notebooks support two cell types: code and markdown. Markdown cells render as formatted text, letting you weave explanations and conclusions between your code. This makes your notebook readable as a complete report, not just a script.
Professional data analysts always include narrative cells that explain their reasoning and highlight key takeaways.
- Run the markdown cell by pressing Shift+Enter.
- The raw markdown text should render into a formatted heading and bullet list. You should see a bold "Key Findings" heading followed by four insight bullet points.
Cell showing raw text instead of formatted output?
- Make sure you changed the cell type to Markdown using the dropdown in the toolbar. If it still says Code, the cell will try to execute the text as Python and throw an error.
- Double-click the rendered cell to edit it again if you need to make changes.
Help me with my markdown cell
✔️ Awesome, I've got everything!
Great. Double check you've saved your notebook before moving on.
ⓧ I'd like to double check the full code
Here are all three cells you added in this step. Make sure they match yours exactly.
# Cell 14: Distribution Comparison
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
sns.histplot(data=df[df['year'] == 1952], x='lifeExp', bins=20, kde=True, ax=axes[0], color='salmon')
axes[0].set_title('Life Expectancy Distribution (1952)')
axes[0].set_xlabel('Life Expectancy (years)')
sns.histplot(data=df[df['year'] == 2007], x='lifeExp', bins=20, kde=True, ax=axes[1], color='steelblue')
axes[1].set_title('Life Expectancy Distribution (2007)')
axes[1].set_xlabel('Life Expectancy (years)')
plt.tight_layout()
plt.show()
# Cell 15: Box Plot by Continent
plt.figure(figsize=(10, 6))
sns.boxplot(data=df[df['year'] == 2007], x='continent', y='lifeExp', hue='continent', palette='Set2')
plt.xlabel('Continent')
plt.ylabel('Life Expectancy (years)')
plt.title('Life Expectancy by Continent (2007)')
plt.tight_layout()
plt.show()
## Key Findings
- Global life expectancy increased dramatically from 1952 to 2007
- There is a strong positive correlation between GDP per capita and life expectancy
- Africa has the widest spread in life expectancy, with some countries making huge gains and others stagnating
- Asian countries (especially China and India) show the most dramatic population growth
Secret mission
Faceted Continent Comparison Across Decades
Your charts so far show one time period or one metric at a time. What if you could compare every continent's wealth-vs-health story across three pivotal decades in a single glance? Build a faceted multi-panel visualization that tells the complete story of global development.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally on your machine, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Jupyter Notebook server (running process in your terminal).
- gapminder_analysis.ipynb notebook file.
- gapminder_faceted.png saved image file.
- Python packages installed via pip (pandas, matplotlib, seaborn, notebook).
Keep everything running
No action needed. Choose this if you're still actively exploring the dataset or want to add more visualizations to your notebook.
- Your Jupyter Notebook server stays running in the terminal where you launched it.
- Your notebook and saved PNG remain on your Desktop.
- The installed Python packages stay available for future projects.
Pause - I'll come back to this later
Shut down the Jupyter Notebook server to free up the terminal process. Your files and packages stay intact for when you return.
- Switch to the terminal window where Jupyter is running.
- Press Ctrl+C to stop the Jupyter Notebook server.
- Confirm the shutdown when prompted by typing y and pressing Enter.
- To resume later, open your terminal, navigate to the folder containing your notebook, and run jupyter notebook again.
Delete - I don't want to use this again
Remove all project files and uninstall the Python packages you installed for this project.
- Stop the Jupyter Notebook server by pressing Ctrl+C in the terminal where it is running.
- Delete the notebook and saved image by running these commands:
macOS
cd ~/Desktop
rm gapminder_analysis.ipynb gapminder_faceted.png
Windows
cd ~\Desktop
del gapminder_analysis.ipynb, gapminder_faceted.png
- Uninstall the Python packages you installed for this project by running:
pip uninstall pandas matplotlib seaborn notebook -y
What about Python itself?
If you installed Python specifically for this project and no longer need it, you can remove it through your system's application manager. If you use Python for other work, leave it installed.
Nice Work!
Nice Work!
You just completed a full data analysis project using real-world data from the Gapminder dataset. You went from a blank notebook to a collection of publication-quality visualizations that reveal 55 years of global development trends.
Here's what you learned:
- Loaded and explored a real dataset using pandas methods like .head(), .info(), .describe(), and .value_counts() to understand its structure.
- Created scatter plots, line charts, bar charts, histograms, and box plots using matplotlib and seaborn to visualize global health and wealth trends.
- Used groupby() and .agg() to summarize data by category, and filtered DataFrames to answer specific questions about countries and continents.
- Secret Mission: Built a faceted multi-panel visualization comparing all continents side-by-side across decades using seaborn's relplot.
Ready to quiz yourself?