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.
| 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 |
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.
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 columninfo() 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.
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# 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'])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).
# 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)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.
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 columnsFault 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.
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()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 castThis 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
.
├── 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
- Inspect types and null counts before analysing; a numeric column read as text fails silently and quietly corrupts every aggregation built on it
pd.cutexpresses grade banding more clearly than chained conditionals, and makes the boundaries explicit and reviewableto_csv(index=False)prevents theUnnamed: 0column 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