# ---------------------------------------------------------------------------
# student_support.py
#
# AI project on Statistical Data: spotting, early in the year, the students
# who may need extra help to pass - so that help reaches them in time.
#
# CBSE Class X, Artificial Intelligence (417), Part C project work.
# SDG 4 (Quality Education) and SDG 10 (Reduced Inequalities).
#
# Two models are compared:
#   - the TEACHER'S RULE, written by a person: flag a student whose
#     attendance is below 75%
#   - a LEARNED RULE: the computer tries every feature and every cut-off
#     on the training data, and keeps the one rule that makes the fewest
#     mistakes. (This is a decision tree with a single question, which is
#     called a decision stump.)
#
# Libraries: pandas (the data), numpy (the arithmetic), matplotlib (charts).
# ---------------------------------------------------------------------------

# %% Stage 1 - Problem scoping
print("=" * 60)
print("WHO NEEDS A HELPING HAND?")
print("=" * 60)
print("Who   : class teachers, and students who are falling behind")
print("What  : struggling students are often noticed only after the exam")
print("Where : a school, in the first months of the year")
print("Why   : help given early can change the result")
print()

# %% Stage 2 - Data acquisition
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

pd.set_option("display.width", 120)
pd.set_option("display.max_columns", 20)

data = pd.read_csv("student_records.csv")

print("--- Stage 2: Data acquisition ---")
print("Rows read    :", len(data))
print("Columns read :", len(data.columns))
print()
print("The first 10 rows:")
print(data.head(10))
print()

# %% Stage 3 - Data exploration
print("--- Stage 3: Data exploration ---")
features = ["attendance_pct", "homework_pct", "unit_test_avg", "study_hours_week"]
label = "needs_support"

missing = data["unit_test_avg"].isnull().sum()
data = data.dropna(subset=["unit_test_avg"])
print("Rows with no unit test average   :", missing, "(removed)")

# Attendance is a percentage, so anything above 100 is a typing mistake.
wrong = (data["attendance_pct"] > 100).sum()
data = data[data["attendance_pct"] <= 100]
print("Rows with attendance above 100%  :", wrong, "(removed)")
print("Rows left                        :", len(data))
print()

print("Students who needed support and who did not:")
print(data[label].value_counts())
print()
print("Average of each feature in the two groups:")
print(data.groupby(label)[features].mean().round(1))
print()

yes = data[data[label] == "Yes"]
no = data[data[label] == "No"]

# Chart 1: attendance against unit test average.
plt.figure(figsize=(7, 5))
plt.scatter(no["attendance_pct"], no["unit_test_avg"], color="#4e79a7", alpha=0.55, s=18, label="Passed comfortably")
plt.scatter(yes["attendance_pct"], yes["unit_test_avg"], color="#e15759", alpha=0.75, s=18, label="Needed support")
plt.axvline(75, color="#999999", linestyle="--")
plt.title("Attendance and unit test average")
plt.xlabel("Attendance (%)")
plt.ylabel("Unit test average (%)")
plt.legend()
plt.grid(True, linestyle="--", alpha=0.4)
plt.tight_layout()
plt.savefig("chart1_attendance_tests.png")
plt.close()

# Chart 2: unit test average in the two groups.
plt.figure(figsize=(7, 4.5))
plt.hist(no["unit_test_avg"], bins=18, alpha=0.6, color="#4e79a7", label="Passed comfortably")
plt.hist(yes["unit_test_avg"], bins=18, alpha=0.6, color="#e15759", label="Needed support")
plt.title("Unit test average")
plt.xlabel("Unit test average (%)")
plt.ylabel("Number of students")
plt.legend()
plt.tight_layout()
plt.savefig("chart2_unit_tests.png")
plt.close()

# %% Stage 4 - Modelling
print("--- Stage 4: Modelling ---")

data = data.sample(frac=1, random_state=2)
cut = int(len(data) * 0.8)
train = data[:cut]
test = data[cut:]
print("Training rows :", len(train))
print("Testing rows  :", len(test))
print()

# The learned rule: "flag the student if <feature> is below <cut-off>".
# Try every feature, and every value it takes in the training data as the
# cut-off. Count the mistakes each rule makes on the training students, and
# keep the rule with the fewest.
truth = train[label] == "Yes"
best_mistakes = len(train) + 1
rules_tried = 0
for f in features:
    for cutoff in sorted(train[f].unique()):
        rules_tried = rules_tried + 1
        flagged = train[f] < cutoff
        mistakes = (flagged != truth).sum()
        if mistakes < best_mistakes:
            best_mistakes = mistakes
            best_feature = f
            best_cutoff = cutoff

print("Rules tried                :", rules_tried)
print("Learned rule               : flag if", best_feature, "<", best_cutoff)
print("Mistakes on training data  :", best_mistakes, "of", len(train))
print("Teacher's rule             : flag if attendance_pct < 75")
print()

# For the chart: mistakes made by every cut-off of the chosen feature.
cutoffs = sorted(train[best_feature].unique())
cutoff_mistakes = []
for cutoff in cutoffs:
    cutoff_mistakes.append(((train[best_feature] < cutoff) != truth).sum())

# Chart 3: how the computer chose its cut-off.
plt.figure(figsize=(7, 4))
plt.plot(cutoffs, cutoff_mistakes, color="#f28e2b")
plt.axvline(best_cutoff, color="#e15759", linestyle="--", label="Fewest mistakes: " + str(best_cutoff))
plt.title("Mistakes on training data for every cut-off")
plt.xlabel("Cut-off for " + best_feature)
plt.ylabel("Mistakes")
plt.legend()
plt.grid(True, linestyle="--", alpha=0.4)
plt.tight_layout()
plt.savefig("chart3_choosing_cutoff.png")
plt.close()

teacher = []
learned = []
for i in range(len(test)):
    row = test.iloc[i]
    if row["attendance_pct"] < 75:
        teacher.append("Yes")
    else:
        teacher.append("No")
    if row[best_feature] < best_cutoff:
        learned.append("Yes")
    else:
        learned.append("No")

# %% Stage 5 - Evaluation
print("--- Stage 5: Evaluation ---")
actual = list(test[label])

results = []
for name, predicted in [["Teacher's rule", teacher], ["Learned rule", learned]]:
    tp = fn = fp = tn = 0
    for i in range(len(actual)):
        if actual[i] == "Yes" and predicted[i] == "Yes":
            tp = tp + 1
        elif actual[i] == "Yes":
            fn = fn + 1
        elif predicted[i] == "Yes":
            fp = fp + 1
        else:
            tn = tn + 1
    accuracy = (tp + tn) / len(actual) * 100
    precision = 0
    if tp + fp > 0:
        precision = tp / (tp + fp) * 100
    recall = 0
    if tp + fn > 0:
        recall = tp / (tp + fn) * 100
    f1 = 0
    if precision + recall > 0:
        f1 = 2 * precision * recall / (precision + recall)
    results.append([name, tp, fn, fp, tn, accuracy, precision, recall, f1])
    print(name)
    print("  " + name + " TP, FN, FP, TN :", tp, fn, fp, tn)
    print("  " + name + " accuracy  :", round(accuracy, 1), "%")
    print("  " + name + " precision :", round(precision, 1), "%")
    print("  " + name + " recall    :", round(recall, 1), "%")
    print("  " + name + " F1 score  :", round(f1, 1), "%")
    print()

# Chart 4: confusion matrix of the learned rule.
r = results[1]
grid = [[r[1], r[2]], [r[3], r[4]]]
plt.figure(figsize=(5, 4.3))
plt.imshow(grid, cmap="Oranges")
names = [["TP", "FN"], ["FP", "TN"]]
for row in range(2):
    for col in range(2):
        if grid[row][col] > max(max(grid)) / 2:
            ink = "white"
        else:
            ink = "black"
        plt.text(col, row, names[row][col] + "\n" + str(grid[row][col]),
                 ha="center", va="center", fontsize=14, color=ink)
plt.xticks([0, 1], ["Flagged", "Not flagged"])
plt.yticks([0, 1], ["Needed support", "Did not"])
plt.xlabel("What the rule said")
plt.ylabel("What happened in the exam")
plt.title("Learned rule: confusion matrix")
plt.tight_layout()
plt.savefig("chart4_matrix.png")
plt.close()

# Chart 5: the two rules side by side.
x = np.arange(4)
plt.figure(figsize=(8, 4.5))
plt.bar(x - 0.2, results[0][5:9], width=0.4, label="Teacher's rule (attendance < 75)", color="#9c9c9c")
plt.bar(x + 0.2, results[1][5:9], width=0.4, label="Learned rule", color="#f28e2b")
plt.xticks(x, ["Accuracy", "Precision", "Recall", "F1 score"])
plt.ylim(0, 100)
plt.ylabel("Percent")
plt.title("The teacher's rule and the learned rule")
plt.legend()
plt.tight_layout()
plt.savefig("chart5_rules_compared.png")
plt.close()

# %% Stage 6 - Deployment
print("--- Stage 6: Deployment ---")
print("Three students after the first unit tests:")
students = pd.DataFrame({
    "roll_no":          ["9401", "9402", "9403"],
    "attendance_pct":   [92, 70, 88],
    "homework_pct":     [55, 85, 90],
    "unit_test_avg":    [36.5, 64.0, 71.5],
    "study_hours_week": [3, 7, 8],
})
for i in range(len(students)):
    s = students.iloc[i]
    if s[best_feature] < best_cutoff:
        advice = "suggest to the class teacher for extra classes"
    else:
        advice = "no flag"
    print("Roll", s["roll_no"], ": attendance", s["attendance_pct"], "%, unit tests",
          s["unit_test_avg"], "% ->", advice)
print("(The model only suggests. The class teacher decides, and talks to the student first.)")
