import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted")
# ════════════════════════════════════════════════════════════════════════════════
# STEP 1 — LOAD DATA
# ════════════════════════════════════════════════════════════════════════════════
# In a real project: df = pd.read_csv("student_dataset.csv")
# Here we generate synthetic but realistic data
np.random.seed(42)
n = 200
df = pd.DataFrame({
"student_id": range(1, n + 1),
"gender": np.random.choice(["Male", "Female"], n, p=[0.52, 0.48]),
"study_hours": np.random.normal(6.5, 2.0, n).clip(1, 14).round(1),
"sleep_hours": np.random.normal(7.0, 1.2, n).clip(4, 10).round(1),
"attendance": np.random.normal(82, 10, n).clip(40, 100).round(0).astype(int),
"math_score": np.random.normal(68, 14, n).clip(20, 100).round(0).astype(int),
"sci_score": np.random.normal(65, 13, n).clip(20, 100).round(0).astype(int),
"placement": np.random.choice(["Placed", "Not Placed"], n, p=[0.62, 0.38]),
})
# Introduce a realistic correlation: more study → better math
df["math_score"] = (df["math_score"] + df["study_hours"] * 2).clip(20, 100).round(0).astype(int)
df["sci_score"] = (df["sci_score"] + df["attendance"] * 0.15).clip(20, 100).round(0).astype(int)
# ════════════════════════════════════════════════════════════════════════════════
# STEP 2 — CLEAN
# ════════════════════════════════════════════════════════════════════════════════
print("Dataset shape:", df.shape)
print("Missing values:\n", df.isnull().sum())
# No missing values in synthetic data; in real data you'd use fillna()/dropna() here
# Add derived columns
df["avg_score"] = ((df["math_score"] + df["sci_score"]) / 2).round(1)
df["result"] = np.where(df["avg_score"] >= 60, "Pass", "Fail")
df["study_category"] = pd.cut(df["study_hours"],
bins=[0, 4, 7, 14],
labels=["Low (≤4h)", "Medium (4–7h)", "High (>7h)"])
# ════════════════════════════════════════════════════════════════════════════════
# STEP 3 — EXPLORE
# ════════════════════════════════════════════════════════════════════════════════
print("\nDescriptive statistics:")
print(df[["study_hours", "attendance", "math_score", "sci_score", "avg_score"]].describe().round(1))
print("\nPlacement counts:\n", df["placement"].value_counts())
# ════════════════════════════════════════════════════════════════════════════════
# STEP 4 — VISUALIZE (three charts answering three questions)
# ════════════════════════════════════════════════════════════════════════════════
fig = plt.figure(figsize=(16, 14))
gs = fig.add_gridspec(2, 2, hspace=0.45, wspace=0.35)
# ── Chart 1: Scatter — "Does studying more lead to a higher exam score?" ──────
ax1 = fig.add_subplot(gs[0, 0])
colors = df["placement"].map({"Placed": "#10B981", "Not Placed": "#EF4444"})
scatter = ax1.scatter(
df["study_hours"], df["math_score"],
c=colors, alpha=0.55, edgecolors="none", s=50
)
# Add a trend line
z = np.polyfit(df["study_hours"], df["math_score"], 1)
p = np.poly1d(z)
x_line = np.linspace(df["study_hours"].min(), df["study_hours"].max(), 100)
ax1.plot(x_line, p(x_line), color="navy", linewidth=1.5, linestyle="--", label="Trend")
ax1.set_title("Q1: Does studying more → higher math score?", fontweight="bold")
ax1.set_xlabel("Weekly Study Hours")
ax1.set_ylabel("Math Score (0–100)")
# Manual legend
legend_elements = [mpatches.Patch(facecolor="#10B981", label="Placed"),
mpatches.Patch(facecolor="#EF4444", label="Not Placed")]
ax1.legend(handles=legend_elements, fontsize=9)
ax1.grid(True, linestyle=":", alpha=0.4)
# ── Chart 2: Box plot — "How do scores differ between placed and unplaced students?" ──
ax2 = fig.add_subplot(gs[0, 1])
sns.boxplot(
data=df, x="placement", y="avg_score",
palette={"Placed": "#10B981", "Not Placed": "#EF4444"},
width=0.45, flierprops={"marker": "o", "markersize": 4, "alpha": 0.5},
ax=ax2
)
# Overlay individual points
sns.stripplot(
data=df, x="placement", y="avg_score",
color="gray", alpha=0.25, size=3, jitter=True, ax=ax2
)
placed_med = df.loc[df["placement"] == "Placed", "avg_score"].median()
notplaced_med = df.loc[df["placement"] == "Not Placed", "avg_score"].median()
ax2.set_title("Q2: Do placed students score higher?", fontweight="bold")
ax2.set_xlabel("Placement Status")
ax2.set_ylabel("Average Score (0–100)")
ax2.annotate(f"Median: {placed_med:.1f}", xy=(0, placed_med), xytext=(0.15, placed_med + 2), fontsize=9, color="#007a4d")
ax2.annotate(f"Median: {notplaced_med:.1f}", xy=(1, notplaced_med), xytext=(1.15, notplaced_med + 2), fontsize=9, color="#b30000")
# ── Chart 3: Heatmap — "Which metrics are most correlated with each other?" ───
ax3 = fig.add_subplot(gs[1, :])
corr_df = df[["study_hours", "sleep_hours", "attendance", "math_score", "sci_score", "avg_score"]].corr()
mask = np.triu(np.ones_like(corr_df, dtype=bool), k=1) # mask upper triangle (mirror)
sns.heatmap(
corr_df,
mask=mask,
annot=True, fmt=".2f",
cmap="RdYlGn",
vmin=-1, vmax=1,
linewidths=0.6,
square=True,
ax=ax3,
annot_kws={"size": 10}
)
ax3.set_title("Q3: Which student metrics are most correlated?", fontweight="bold")
ax3.tick_params(axis="x", rotation=30)
ax3.tick_params(axis="y", rotation=0)
fig.suptitle("Student Performance Analysis — Three Questions, Three Charts",
fontsize=15, fontweight="bold", y=1.01)
plt.savefig("student_visualization_report.png", dpi=150, bbox_inches="tight")
plt.show()
# ════════════════════════════════════════════════════════════════════════════════
# STEP 5 — REFINE & REPORT KEY FINDINGS
# ════════════════════════════════════════════════════════════════════════════════
print("\n── Key Findings ────────────────────────────────────────────")
corr_study_math = df["study_hours"].corr(df["math_score"])
print(f"Correlation (study_hours → math_score): {corr_study_math:.2f}")
print(f"Placed median avg_score: {placed_med:.1f}")
print(f"Not Placed median avg_score: {notplaced_med:.1f}")
print(f"Score gap (placed - not placed): {placed_med - notplaced_med:.1f} points")
top_corr = corr_df["avg_score"].drop("avg_score").sort_values(ascending=False)
print(f"\nStrongest predictors of avg_score:\n{top_corr.round(2)}")