Skip to content

About

Python data analysis — pandas, matplotlib and seaborn on student and GDP datasets

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

Repository files navigation

Python for Data Analysis — Data Technician Bootcamp (Week 6)

Python fundamentals and pandas data analysis, completed during Week 6 of a Data Technician bootcamp. The week covered core language syntax, the full pandas workflow from load to export, and visualisation with matplotlib and seaborn in Jupyter.


Overview

Language Python 3
Libraries pandas, NumPy, matplotlib, seaborn
Environment Jupyter Notebook / Google Colab
Datasets student.csv (student marks by class and gender), GDP (nominal) per Capita
Deliverables FizzBuzz solution, pandas exercise set, visualisation notebook

Skills demonstrated

Core Python

FizzBuzz — the classic interview exercise, iterating 1–100 and branching on divisibility:

for num in range(1, 101):
    if num % 3 == 0 and num % 5 == 0:
        print("fizzbuzz")
    elif num % 3 == 0:
        print("fizz")
    elif num % 5 == 0:
        print("buzz")
    else:
        print(num)

Branch order carries the logic. The combined condition has to be tested first — put the num % 3 branch above it and every multiple of 15 prints fizz, because the first matching branch wins and the loop never reaches the rest.

pandas — loading and exploring

import pandas as pd

df = pd.read_csv('student.csv')

df.head()          # first 5 rows
df.info()          # dtypes, non-null counts, memory usage
df.describe()      # summary statistics for numeric columns
df.dtypes          # column types
df.isnull().sum()  # missing values per column

info() and isnull().sum() were run before any analysis — a column silently stored as object instead of a numeric type will break every aggregation downstream without raising an error.

Running this on student.csv immediately surfaced a data quality problem: 35 rows, but only 34 non-null name values, 34 non-null class values and 33 non-null gender values. id and mark were complete. Had the grouping been run first, the per-class and per-gender averages would have quietly excluded those rows without any indication.

RangeIndex: 35 entries, 0 to 34
Data columns (total 5 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   id      35 non-null     int64
 1   name    34 non-null     object
 2   class   34 non-null     object
 3   mark    35 non-null     int64
 4   gender  33 non-null     object

describe() gave a mean mark of 74.66 with a standard deviation of 16.40 — a wide spread worth knowing before drawing any conclusion from a class average.

Indexing and slicing

df['name']                      # single column as a Series
df[['name', 'mark']]            # multiple columns as a DataFrame
df.head(3)                      # first 3 rows
df[df['class'] == 'Four']       # boolean filtering
df.loc[df['mark'] >= 60, ['name', 'mark']]   # label-based row and column selection

Data manipulation

# Derived column from a condition
df['passed'] = df['mark'] >= 60

# Rename a column
df = df.rename(columns={'mark': 'score'})

# Drop a column
df = df.drop(columns=['passed'])

# Handle missing values before aggregating
df = df.dropna(subset=['score'])

Aggregation and grouping

df.groupby('class')['score'].mean()      # mean score per class
df['class'].value_counts()               # students per class
df.groupby('gender')['score'].mean()     # mean score per gender

# Multiple statistics at once
df.groupby(['class', 'gender'])['score'].agg(['count', 'mean', 'std']).round(1)

The two together are what make the result readable:

Class Mean score Students
Six 82.57 7
Five 80.00 2
Eight 79.00 1
Fifth 78.00 1
Seven 77.60 10
Three 73.67 3
Four 68.75 8
Nine 41.50 2

Class Nine averages 41.5 against a cohort mean of 74.7 — but on two students, while classes Eight and Fifth rank near the top on one student each. Reading the means alone would rank the classes; reading them next to value_counts() shows that half the ranking rests on samples too small to support it. The two classes with real weight are Seven (10) and Four (8).

Advanced operations

# Pivot table — class as rows, gender as columns, score as values
pd.pivot_table(df, index='class', columns='gender', values='score', aggfunc='mean')

# Grade banding with pd.cut
df['grade'] = pd.cut(df['score'],
                     bins=[0, 60, 70, 85, 100],
                     labels=['D', 'C', 'B', 'A'],
                     right=False)

# Sort descending
df.sort_values('score', ascending=False)

Exporting

df.to_csv('student_grades.csv', index=False)

index=False matters — omitting it writes the DataFrame index as an unnamed first column, which is exactly the defect diagnosed in the GDP exercise below.

Second dataset — GDP (nominal) per Capita

df = pd.read_csv('GDP (nominal) per Capita.csv')

df.head(10)                                  # first 10 rows
df.tail(5)                                   # last 5 rows
print(df[['Country/Territory', 'UN_Region']])   # selected columns

Fault diagnosed: head() and tail() both returned a leading Unnamed: 0 column. This is the index of a previously exported DataFrame having been written to CSV and read back as data. The fix is either to name it meaningfully (a country ID) or to export with index=False in the first place.

Visualisation

Built in a Jupyter notebook using matplotlib and seaborn:

Chart Purpose
Bar chart from a groupby df.groupby("class")["mark"].mean().plot(kind="bar") — average mark per class straight from the aggregation
Scatter plot by gender Marks against gender, alpha=0.7 so overlapping points stay readable
Count plot (seaborn) Students per class — sns.countplot(x='class', data=df)
Histogram with a mean reference line Distribution of marks, with axvline marking the average
Box plot by class Spread, median and quartiles per class
Box plot + strip plot by gender Distribution overlaid with individual observations, jittered so points do not hide each other
Bar plot with 95% confidence intervals Mean score by gender, showing uncertainty rather than a bare average
2×2 subplot dashboard Four views on one figure with suptitle and tight_layout
import matplotlib.pyplot as plt

df.groupby("class")["mark"].mean().plot(kind="bar")
plt.title("Average Mark by Class")
plt.xlabel("Class")
plt.ylabel("Average Mark")
plt.show()

A bug worth keeping

The gender scatter plot was built after casting the column to string:

df['gender'] = df['gender'].astype(str)
plt.scatter(df['gender'], df['mark'], alpha=0.7)

astype(str) converts NaN to the literal string "nan", so the two missing gender values became a third category and the chart rendered a nan column on the x-axis alongside female and male. The cast silenced the missing data rather than handling it.

The fix is to decide what the missing rows mean before casting — drop them, or label them explicitly:

df = df.dropna(subset=['gender'])            # exclude
# or
df['gender'] = df['gender'].fillna('Unknown')  # label, then cast

This is the same lesson as the info() check above, arriving from the other direction: missing data does not disappear when you stop looking at it, it just turns up somewhere less obvious.

import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style='whitegrid')

# Mean by gender with a 95% confidence interval and labelled bars
ax = sns.barplot(x='gender', y='mark', data=df, errorbar=('ci', 95))
for container in ax.containers:
    ax.bar_label(container, fmt='%.1f')
plt.title('Average Marks by Gender (with 95% CI)')
plt.show()

Deliberate choices in the notebook:

  • Example data is generated in code rather than uploaded, so the notebook re-runs anywhere without a missing-file error
  • A data inspection step (dtypes, isnull().sum()) runs before any chart is drawn
  • Missing values are dropped explicitly with the row count reported, not silently
  • A strip plot replaces a plain scatter for a categorical axis, because overlapping identical values are otherwise invisible
  • Bar charts carry confidence intervals — a mean without a spread invites over-reading small differences
  • Chart labels are kept in English, since the default Colab environment has no CJK font installed and would render the labels as missing glyphs

Repository contents

.
├── README.md
├── fizzbuzz.py
├── student.csv                          # id, name, class, mark, gender
├── student_visualisation.ipynb          # pandas + seaborn visualisation notebook
├── gdp_per_capita_analysis.ipynb        # GDP dataset exercises
└── GDP (nominal) per Capita.csv

Key takeaways

  • Inspect types and null counts before analysing; a numeric column read as text fails silently and quietly corrupts every aggregation built on it
  • pd.cut expresses grade banding more clearly than chained conditionals, and makes the boundaries explicit and reviewable
  • to_csv(index=False) prevents the Unnamed: 0 column that appears the next time the file is read
  • A mean deserves an interval — plotting the average without the spread makes noise look like signal
  • Generating example data in the notebook rather than depending on an upload is what makes it reproducible for anyone who opens it

About

Python data analysis — pandas, matplotlib and seaborn on student and GDP datasets

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors