{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a91ea08c",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install uv\n",
    "%uv pip install pandas\n",
    "%uv pip install pandas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "da08ac5c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "434d7a6a",
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv(\"../datasets/clean_dataset.csv\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f18eec6b",
   "metadata": {},
   "outputs": [],
   "source": [
    "subjects = [\n",
    "    \"Mathematics\",\n",
    "    \"English\",\n",
    "    \"Science\",\n",
    "    \"ICT\",\n",
    "    \"Geography\",\n",
    "    \"History\",\n",
    "    \"Civics\",\n",
    "    \"Agriculture\",\n",
    "    \"Business_Studies\",\n",
    "    \"Physical_Education\",\n",
    "    \"Swahili\",\n",
    "    \"French\",\n",
    "    \"Biology\",\n",
    "    \"Chemistry\",\n",
    "    \"Physics\",\n",
    "    \"Entrepreneurship\",\n",
    "    \"Fine_Arts\",\n",
    "    \"Music\",\n",
    "    \"Additional_Math\",\n",
    "    \"Religious_Education\",\n",
    "]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f0d9d8eb",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "Definir contexto de usuario para predicción\n",
    "\"\"\"\n",
    "\n",
    "final_cols = [c for c in df.columns if c.endswith(\"_final\")] # Columnas de notas finales\n",
    "status_cols = [c for c in df.columns if c.endswith(\"_status\")]\n",
    "term_1_columns = [c for c in df.columns if c.endswith(\"_term1\")]\n",
    "term_2_columns = [c for c in df.columns if c.endswith(\"_term2\")]\n",
    "term_3_columns = [c for c in df.columns if c.endswith(\"_term3\")]\n",
    "\n",
    "def check_null_val(data):\n",
    "\n",
    "    if pd.isna(data):\n",
    "\n",
    "        return \"\"\n",
    "    \n",
    "    return str(data)\n",
    "\n",
    "def define_user_context(r):\n",
    "\n",
    "    data = {\n",
    "      \n",
    "        \"student_id\": check_null_val(r.get(\"student_id\")),\n",
    "        \"gender\": check_null_val(r.get(\"gender\")),\n",
    "        \"age\": check_null_val(r.get(\"age\")),\n",
    "        \"country\": check_null_val(r.get(\"country\")),\n",
    "        \"city\": check_null_val(r.get(\"city\")),\n",
    "        \"school_name\": check_null_val(r.get(\"school_name\")),\n",
    "        \"course\": check_null_val(r.get(\"course\")),\n",
    "        \"academic_year\": check_null_val(r.get(\"academic_year\")),\n",
    "        \"class_group\": check_null_val(r.get(\"class_group\")),\n",
    "        \"homeroom_teacher\": check_null_val(r.get(\"homeroom_teacher\")),\n",
    "        \"attendance_absences_total\": check_null_val(r.get(\"attendance_absences_total\")),\n",
    "        \"support_level\": check_null_val(r.get(\"support_level\")),\n",
    "        \"support_level_num\": check_null_val(r.get(\"support_level_num\")),\n",
    "        \"grade_tendency\": check_null_val(r.get(\"grade_tendency\")),\n",
    "        \"sustained_decrease\": check_null_val(r.get(\"sustained_decrease\")),\n",
    "        \"sustained_increase\": check_null_val(r.get(\"sustained_increase\")),\n",
    "        \"notes\": check_null_val(r.get(\"notes\")),\n",
    "    \n",
    "    }\n",
    "\n",
    "    for final_col in final_cols:\n",
    "    \n",
    "        subject = final_col.replace(\"_final\", \"\")\n",
    "\n",
    "        data[f\"{subject}_teacher\"] = check_null_val(r.get(f\"{subject}_teacher\"))\n",
    "        data[f\"{subject}_term1\"] = check_null_val(r.get(f\"{subject}_term1\"))\n",
    "        data[f\"{subject}_term2\"] = check_null_val(r.get(f\"{subject}_term2\"))\n",
    "        data[f\"{subject}_term3\"] = check_null_val(r.get(f\"{subject}_term3\"))\n",
    "        data[f\"{subject}_final\"] = check_null_val(r.get(f\"{subject}_final\"))\n",
    "        data[f\"{subject}_status\"] = check_null_val(r.get(f\"{subject}_status\"))\n",
    "\n",
    "    data[\"avg_final\"] = check_null_val(r.get(\"avg_final\"))\n",
    "    data[\"max_grade\"] = check_null_val(r.get(\"max_grade\"))\n",
    "    data[\"min_grade\"] = check_null_val(r.get(\"min_grade\"))\n",
    "    data[\"passed_subjects\"] = check_null_val(r.get(\"passed_subjects\"))\n",
    "    data[\"failed_subjects\"] = check_null_val(r.get(\"failed_subjects\"))\n",
    "    data[\"passed_rate\"] = check_null_val(r.get(\"passed_rate\"))\n",
    "    data[\"passed_rate_percent\"] = check_null_val(r.get(\"passed_rate_percent\"))\n",
    "    data[\"final_grade_std\"] = check_null_val(r.get(\"final_grade_std\"))\n",
    "    data[\"total_subjects\"] = check_null_val(r.get(\"total_subjects\"))\n",
    "\n",
    "    return data\n",
    "\n",
    "contexts = []\n",
    "\n",
    "for _, r in df.iterrows():\n",
    "    \n",
    "    context = define_user_context(r)\n",
    "    contexts.append(context)\n",
    "\n",
    "df_context = pd.DataFrame(contexts)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "00a98be5",
   "metadata": {},
   "outputs": [],
   "source": [
    "df_context.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2ac2b163",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "Ahora vamos con las predicciones de escenarios ficticios basados en los\n",
    "contextos de los usuarios\n",
    "\"\"\"\n",
    "\n",
    "# Comprobar que ollama está levantado\n",
    "\n",
    "%uv pip install requests\n",
    "\n",
    "import requests\n",
    "\n",
    "res = requests.get(\"http://localhost:11434/api/tags\")\n",
    "\n",
    "if res.status_code != 200:\n",
    "\n",
    "    raise ConnectionError(f\"Ollama no responde correctamente: {res.status_code}\")\n",
    "\n",
    "data = res.json()\n",
    "\n",
    "if data.get(\"models\") == []:\n",
    "\n",
    "    raise RuntimeError(\"Ollama está levantado, pero no hay modelos instalados\")\n",
    "\n",
    "print(\"Modelos detectados:\")\n",
    "\n",
    "for model in data[\"models\"]:\n",
    "\n",
    "    print(\"-\", model[\"name\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "385a0896",
   "metadata": {},
   "outputs": [],
   "source": [
    "def row_to_text(r):\n",
    "    \n",
    "    parts = []\n",
    "\n",
    "    for col, value in r.items():\n",
    "    \n",
    "        if value != \"\":\n",
    "    \n",
    "            parts.append(f\"{col}: {value}\")\n",
    "\n",
    "    return \". \".join(parts)\n",
    "\n",
    "df_context[\"base_text\"] = df_context.apply(row_to_text, axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c7836f0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "from pydantic import BaseModel, Field\n",
    "from typing import Optional\n",
    "\n",
    "Definir el esquema del modelo con base model\n",
    "\n",
    "from langchain_ollama import ChatOllama\n",
    "\n",
    "Crear el modelo\n",
    "\n",
    "prediction_llm = llm.with_structured_output(StudentPrediction)\n",
    "\n",
    "Crear función de predicción\n",
    "\n",
    "Realizar un test\n",
    "\n",
    "Crear predicción para todos los alumnos\n",
    "\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aaa6276c",
   "metadata": {},
   "outputs": [],
   "source": [
    "%uv pip install pydantic\n",
    "%uv pip install typing"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "94825731",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pydantic import BaseModel, Field\n",
    "from typing import Optional, List\n",
    "\n",
    "class StudentBatchPrediction(BaseModel):\n",
    "  \n",
    "    student_id: str = Field(description=\"Student ID\")\n",
    "  \n",
    "    predicted_value: float = Field(description=\"Predicted Technical Drawing grade from 0 to 100\")\n",
    "  \n",
    "    prediction_target: str = Field(description=\"Must be Technical_Drawing_final\")\n",
    "  \n",
    "    confidence: Optional[float] = Field(default=None, description=\"Confidence from 0 to 1\")\n",
    "\n",
    "\n",
    "class StudentBatchPredictions(BaseModel):\n",
    "  \n",
    "    predictions: List[StudentBatchPrediction]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "71149fa2",
   "metadata": {},
   "outputs": [],
   "source": [
    "%uv pip install langchain_ollama\n",
    "%uv pip install asyncio"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5da755c6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_ollama import ChatOllama\n",
    "\n",
    "default_model = \"llama3.2:3b\"\n",
    "temperature = 0\n",
    "url = \"http://localhost:11434\"\n",
    "\n",
    "model = ChatOllama(\n",
    "\n",
    "    model = default_model,\n",
    "    temperature = temperature,\n",
    "    base_url = url,\n",
    "    keep_alive=\"30m\",\n",
    "    num_predict=80,\n",
    "    num_ctx=2048,\n",
    "\n",
    ")\n",
    "\n",
    "batch_prediction_llm = model.with_structured_output(StudentBatchPredictions)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "36f87b6c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test\n",
    "\n",
    "res = model.invoke(\"Responde solamente con OK\")\n",
    "\n",
    "print(res.content)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5967552f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def student_row_to_text(student):\n",
    "\n",
    "    parts = []\n",
    "\n",
    "    for col, value in student.items():\n",
    "    \n",
    "        if col == \"user_context\":\n",
    "    \n",
    "            continue\n",
    "\n",
    "        if value != \"\" and value is not None:\n",
    "    \n",
    "            parts.append(f\"{col}: {value}\")\n",
    "\n",
    "    return \". \".join(parts)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "676094d1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "import asyncio"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eaad4b15",
   "metadata": {},
   "outputs": [],
   "source": [
    "import asyncio\n",
    "import pandas as pd\n",
    "import time\n",
    "import json\n",
    "import re\n",
    "\n",
    "RELEVANT_SUBJECTS = [\n",
    "    \"Mathematics\",\n",
    "    \"Science\",\n",
    "    \"ICT\",\n",
    "    \"Physics\",\n",
    "    \"Chemistry\",\n",
    "    \"Fine_Arts\",\n",
    "    \"Additional_Math\",\n",
    "    \"Business_Studies\",\n",
    "    \"Physical_Education\",\n",
    "]\n",
    "\n",
    "\n",
    "def safe_get(row, col, default=\"\"):\n",
    "    value = row.get(col, default)\n",
    "\n",
    "    if pd.isna(value):\n",
    "        return default\n",
    "\n",
    "    return value\n",
    "\n",
    "\n",
    "def build_compact_student_context(student):\n",
    "    parts = []\n",
    "\n",
    "    # Contexto general\n",
    "    parts.append(f\"student_id: {safe_get(student, 'student_id')}\")\n",
    "    parts.append(f\"age: {safe_get(student, 'age')}\")\n",
    "    parts.append(f\"gender: {safe_get(student, 'gender')}\")\n",
    "    parts.append(f\"country: {safe_get(student, 'country')}\")\n",
    "    parts.append(f\"city: {safe_get(student, 'city')}\")\n",
    "    parts.append(f\"school_name: {safe_get(student, 'school_name')}\")\n",
    "    parts.append(f\"course: {safe_get(student, 'course')}\")\n",
    "    parts.append(f\"class_group: {safe_get(student, 'class_group')}\")\n",
    "    parts.append(f\"attendance_absences_total: {safe_get(student, 'attendance_absences_total')}\")\n",
    "    parts.append(f\"support_level: {safe_get(student, 'support_level')}\")\n",
    "    parts.append(f\"grade_tendency: {safe_get(student, 'grade_tendency')}\")\n",
    "    parts.append(f\"sustained_decrease: {safe_get(student, 'sustained_decrease')}\")\n",
    "    parts.append(f\"sustained_increase: {safe_get(student, 'sustained_increase')}\")\n",
    "\n",
    "    # Resumen global útil\n",
    "    parts.append(f\"avg_final: {safe_get(student, 'avg_final')}\")\n",
    "    parts.append(f\"max_grade: {safe_get(student, 'max_grade')}\")\n",
    "    parts.append(f\"min_grade: {safe_get(student, 'min_grade')}\")\n",
    "    parts.append(f\"passed_subjects: {safe_get(student, 'passed_subjects')}\")\n",
    "    parts.append(f\"failed_subjects: {safe_get(student, 'failed_subjects')}\")\n",
    "    parts.append(f\"passed_rate_percent: {safe_get(student, 'passed_rate_percent')}\")\n",
    "    parts.append(f\"final_grade_std: {safe_get(student, 'final_grade_std')}\")\n",
    "\n",
    "    # Asignaturas relevantes\n",
    "    for subject in RELEVANT_SUBJECTS:\n",
    "        final_col = f\"{subject}_final\"\n",
    "        term1_col = f\"{subject}_term1\"\n",
    "        term2_col = f\"{subject}_term2\"\n",
    "        term3_col = f\"{subject}_term3\"\n",
    "        status_col = f\"{subject}_status\"\n",
    "\n",
    "        if final_col in student.index:\n",
    "            final_value = safe_get(student, final_col)\n",
    "\n",
    "            if final_value != \"\":\n",
    "                parts.append(\n",
    "                    f\"{subject}: \"\n",
    "                    f\"term1={safe_get(student, term1_col)}, \"\n",
    "                    f\"term2={safe_get(student, term2_col)}, \"\n",
    "                    f\"term3={safe_get(student, term3_col)}, \"\n",
    "                    f\"final={safe_get(student, final_col)}, \"\n",
    "                    f\"status={safe_get(student, status_col)}\"\n",
    "                )\n",
    "\n",
    "    notes = safe_get(student, \"notes\")\n",
    "    if notes != \"\":\n",
    "        parts.append(f\"notes: {notes}\")\n",
    "\n",
    "    return \"\\n\".join(str(p) for p in parts if str(p).strip())\n",
    "\n",
    "\n",
    "def extract_json_from_response(text):\n",
    "    \"\"\"\n",
    "    Intenta extraer JSON aunque el modelo meta texto antes/después.\n",
    "    \"\"\"\n",
    "    text = text.strip()\n",
    "\n",
    "    # Caso ideal: respuesta ya es JSON\n",
    "    try:\n",
    "        return json.loads(text)\n",
    "    except json.JSONDecodeError:\n",
    "        pass\n",
    "\n",
    "    # Buscar primer bloque tipo {...}\n",
    "    match = re.search(r\"\\{.*\\}\", text, re.DOTALL)\n",
    "\n",
    "    if not match:\n",
    "        raise ValueError(f\"No se encontró JSON en la respuesta: {text}\")\n",
    "\n",
    "    json_text = match.group(0)\n",
    "\n",
    "    try:\n",
    "        return json.loads(json_text)\n",
    "    except json.JSONDecodeError as e:\n",
    "        raise ValueError(f\"JSON inválido: {json_text}\") from e\n",
    "\n",
    "\n",
    "system_prompt = \"\"\"\n",
    "You are an academic prediction assistant.\n",
    "\n",
    "Task:\n",
    "Predict the student's expected final grade in a new fictional subject: Technical Drawing.\n",
    "\n",
    "Rules:\n",
    "- Return ONLY valid JSON.\n",
    "- Do not use markdown.\n",
    "- Do not wrap the JSON in ```json.\n",
    "- Do not add explanations outside the JSON.\n",
    "- predicted_value must be between 0 and 100.\n",
    "- prediction_target must be \"Technical_Drawing_final\".\n",
    "- Use the student's STEM subjects, Fine Arts, ICT, attendance, support level, trend, and notes.\n",
    "- Keep reasoning_summary and observations very short.\n",
    "\n",
    "Required JSON format:\n",
    "{\n",
    "  \"predicted_value\": 0,\n",
    "  \"prediction_target\": \"Technical_Drawing_final\",\n",
    "  \"reasoning_summary\": \"short explanation\",\n",
    "  \"confidence\": 0.0,\n",
    "  \"observations\": \"short observation\"\n",
    "}\n",
    "\"\"\"\n",
    "\n",
    "user_prompt = \"Predict the final grade for the new subject Technical Drawing.\"\n",
    "\n",
    "\n",
    "async def predict_one_student(student, semaphore):\n",
    "    async with semaphore:\n",
    "        start = time.time()\n",
    "\n",
    "        student_id = student[\"student_id\"]\n",
    "        student_context = build_compact_student_context(student)\n",
    "\n",
    "        human_prompt = f\"\"\"\n",
    "STUDENT CONTEXT:\n",
    "{student_context}\n",
    "\n",
    "USER REQUEST:\n",
    "{user_prompt}\n",
    "\n",
    "Return only valid JSON.\n",
    "\"\"\"\n",
    "\n",
    "        try:\n",
    "            response = await model.ainvoke([\n",
    "                (\"system\", system_prompt),\n",
    "                (\"human\", human_prompt)\n",
    "            ])\n",
    "\n",
    "            raw_text = response.content\n",
    "            prediction = extract_json_from_response(raw_text)\n",
    "\n",
    "            predicted_value = prediction.get(\"predicted_value\")\n",
    "\n",
    "            if predicted_value is not None:\n",
    "                predicted_value = float(predicted_value)\n",
    "                predicted_value = max(0, min(100, predicted_value))\n",
    "\n",
    "            result = {\n",
    "                \"student_id\": student_id,\n",
    "                \"predicted_subject\": \"Technical_Drawing\",\n",
    "                \"predicted_value\": predicted_value,\n",
    "                \"prediction_target\": prediction.get(\"prediction_target\", \"Technical_Drawing_final\"),\n",
    "                \"reasoning_summary\": prediction.get(\"reasoning_summary\"),\n",
    "                \"confidence\": prediction.get(\"confidence\"),\n",
    "                \"observations\": prediction.get(\"observations\"),\n",
    "                \"context_chars\": len(student_context),\n",
    "                \"elapsed_seconds\": round(time.time() - start, 2),\n",
    "                \"raw_response\": raw_text,\n",
    "                \"error\": None,\n",
    "            }\n",
    "\n",
    "            print(\n",
    "                f\"OK - {student_id}: \"\n",
    "                f\"{result['predicted_value']} \"\n",
    "                f\"({result['elapsed_seconds']}s, {len(student_context)} chars)\"\n",
    "            )\n",
    "\n",
    "            return result\n",
    "\n",
    "        except Exception as e:\n",
    "            result = {\n",
    "                \"student_id\": student_id,\n",
    "                \"predicted_subject\": \"Technical_Drawing\",\n",
    "                \"predicted_value\": None,\n",
    "                \"prediction_target\": \"Technical_Drawing_final\",\n",
    "                \"reasoning_summary\": None,\n",
    "                \"confidence\": None,\n",
    "                \"observations\": None,\n",
    "                \"context_chars\": len(student_context),\n",
    "                \"elapsed_seconds\": round(time.time() - start, 2),\n",
    "                \"raw_response\": None,\n",
    "                \"error\": str(e),\n",
    "            }\n",
    "\n",
    "            print(f\"ERROR - {student_id}: {e}\")\n",
    "            return result\n",
    "\n",
    "\n",
    "async def predict_students_async(df_context, limit=5, concurrency_limit=2):\n",
    "    semaphore = asyncio.Semaphore(concurrency_limit)\n",
    "\n",
    "    rows = [\n",
    "        row\n",
    "        for _, row in df_context.head(limit).iterrows()\n",
    "    ]\n",
    "\n",
    "    tasks = [\n",
    "        predict_one_student(row, semaphore)\n",
    "        for row in rows\n",
    "    ]\n",
    "\n",
    "    results = await asyncio.gather(*tasks)\n",
    "\n",
    "    return pd.DataFrame(results)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "08d54cae",
   "metadata": {},
   "outputs": [],
   "source": [
    "df_predictions = await predict_students_async(\n",
    "    df_context=df_context,\n",
    "    limit=3,\n",
    "    concurrency_limit=2\n",
    ")\n",
    "\n",
    "df_predictions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a093f1ce",
   "metadata": {},
   "outputs": [],
   "source": [
    "df_predictions.to_csv(\"../datasets/nlp_pred_technical_drawing.csv\", index = False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e540b36d",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "Predicciones Simuladas — Nuevas Asignaturas y Atributos\n",
    "\n",
    "Las siguientes predicciones siguen el mismo esquema Pydantic que las anteriores\n",
    "pero los valores se generan mediante aproximaciones basadas en reglas.\n",
    "Esto permite obtener CSVs completos para los 500 estudiantes sin\n",
    "saturar el modelo LLM local con miles de llamadas.\n",
    "\n",
    "Los CSVs se guardan directamente en ../datasets/.\n",
    "\"\"\"\n",
    "\n",
    "import random\n",
    "\n",
    "random.seed(42)\n",
    "\n",
    "\n",
    "def safe_float(val, default = 60.0):\n",
    "\n",
    "    try:\n",
    "\n",
    "        v = float(val)\n",
    "\n",
    "        return v if v > 0 else default\n",
    "\n",
    "    except:\n",
    "\n",
    "        return default\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14c839de",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "Predicción 2 — Informática (Computer_Science_final)\n",
    "\n",
    "Asignatura técnica nueva. Se predice a partir del rendimiento en\n",
    "ICT, Mathematics y Physics, ajustado por ausencias y nivel de soporte.\n",
    "\"\"\"\n",
    "\n",
    "class ComputerSciencePrediction(BaseModel):\n",
    "\n",
    "    student_id:        str            = Field(description='Student ID')\n",
    "\n",
    "    predicted_value:   float          = Field(description='Predicted Computer Science grade 0-100')\n",
    "\n",
    "    prediction_target: str            = Field(description='Must be Computer_Science_final')\n",
    "\n",
    "    confidence:        Optional[float] = Field(default=None, description='Confidence 0-1')\n",
    "\n",
    "\n",
    "def simulate_computer_science(student):\n",
    "\n",
    "    ict      = safe_float(student.get('ICT_final'))\n",
    "    math     = safe_float(student.get('Mathematics_final'))\n",
    "    physics  = safe_float(student.get('Physics_final'))\n",
    "    absences = safe_float(student.get('attendance_absences_total'), 0)\n",
    "    support  = str(student.get('support_level_num', '1'))\n",
    "\n",
    "    base = ict * 0.45 + math * 0.35 + physics * 0.20\n",
    "\n",
    "    absence_penalty = min(absences * 0.15, 3.0)\n",
    "\n",
    "    support_bonus = {'0': 0.5, '1': 0.0, '2': -0.3}.get(support, 0.0)\n",
    "\n",
    "    noise = random.gauss(0, 1.5)\n",
    "\n",
    "    predicted = base - absence_penalty + support_bonus + noise\n",
    "\n",
    "    return round(max(40.0, min(100.0, predicted)), 2)\n",
    "\n",
    "\n",
    "results_cs = []\n",
    "\n",
    "for _, row in df_context.iterrows():\n",
    "\n",
    "    predicted_value = simulate_computer_science(row)\n",
    "\n",
    "    status     = 'Pass' if predicted_value >= 50 else 'Fail'\n",
    "\n",
    "    confidence = round(min(0.95, max(0.55, 1 - abs(predicted_value - 65) / 100)), 2)\n",
    "\n",
    "    results_cs.append({\n",
    "        'student_id':        row['student_id'],\n",
    "        'predicted_subject': 'Computer_Science',\n",
    "        'predicted_value':   predicted_value,\n",
    "        'prediction_target': 'Computer_Science_final',\n",
    "        'status':            status,\n",
    "        'confidence':        confidence,\n",
    "        'reasoning_summary': f\"Based on ICT={safe_float(row.get('ICT_final')):.1f}, Math={safe_float(row.get('Mathematics_final')):.1f}, Physics={safe_float(row.get('Physics_final')):.1f}\",\n",
    "    })\n",
    "\n",
    "df_cs = pd.DataFrame(results_cs)\n",
    "\n",
    "df_cs.to_csv('../datasets/nlp_pred_computer_science.csv', index = False)\n",
    "\n",
    "print(f'Computer Science: {len(df_cs)} estudiantes')\n",
    "print(f'  Aprobados:  {(df_cs[\"status\"] == \"Pass\").sum()}')\n",
    "print(f'  Suspendidos: {(df_cs[\"status\"] == \"Fail\").sum()}')\n",
    "print(f'  Media predicha: {df_cs[\"predicted_value\"].mean():.2f}')\n",
    "\n",
    "print(df_cs.head(5).to_string(index = False))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef0d6053",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "Predicción 3 — Alerta de Riesgo Académico (risk_alert)\n",
    "\n",
    "Clasifica a cada estudiante en Low / Medium / High según la probabilidad\n",
    "de necesitar intervención urgente antes del siguiente trimestre.\n",
    "\"\"\"\n",
    "\n",
    "class RiskAlertPrediction(BaseModel):\n",
    "\n",
    "    student_id:   str   = Field(description='Student ID')\n",
    "\n",
    "    risk_level:   str   = Field(description='Low, Medium or High risk')\n",
    "\n",
    "    risk_score:   float = Field(description='Risk score from 0 to 1')\n",
    "\n",
    "    main_factors: str   = Field(description='Main contributing factors')\n",
    "\n",
    "\n",
    "def simulate_risk_alert(student):\n",
    "\n",
    "    avg_final       = safe_float(student.get('avg_final'), 60)\n",
    "    failed_subjects = safe_float(student.get('failed_subjects'), 0)\n",
    "    absences        = safe_float(student.get('attendance_absences_total'), 0)\n",
    "    support         = str(student.get('support_level_num', '1'))\n",
    "    tendency        = str(student.get('grade_tendency', 'Uptrend'))\n",
    "\n",
    "    risk_score = 0.0\n",
    "    factors    = []\n",
    "\n",
    "    if avg_final < 55:\n",
    "  \n",
    "        risk_score += 0.35; factors.append('low avg grade')\n",
    "  \n",
    "    elif avg_final < 60:\n",
    "  \n",
    "        risk_score += 0.20; factors.append('below average grade')\n",
    "  \n",
    "    elif avg_final < 65:\n",
    "\n",
    "        risk_score += 0.08\n",
    "\n",
    "    risk_score += min(failed_subjects * 0.08, 0.30)\n",
    "\n",
    "    if failed_subjects >= 2: factors.append('multiple failures')\n",
    "\n",
    "    risk_score += min(absences * 0.008, 0.15)\n",
    "\n",
    "    if absences > 15: factors.append('high absences')\n",
    "\n",
    "    if tendency == 'Downtrend':\n",
    "\n",
    "        risk_score += 0.15; factors.append('downward trend')\n",
    "\n",
    "    if support == '2':\n",
    "\n",
    "        risk_score += 0.05\n",
    "\n",
    "    risk_score   = round(min(1.0, max(0.0, risk_score + random.gauss(0, 0.03))), 4)\n",
    "\n",
    "    risk_level   = 'High' if risk_score >= 0.55 else ('Medium' if risk_score >= 0.30 else 'Low')\n",
    "\n",
    "    main_factors = ', '.join(factors) if factors else 'none'\n",
    "\n",
    "    return risk_score, risk_level, main_factors\n",
    "\n",
    "\n",
    "results_risk = []\n",
    "\n",
    "for _, row in df_context.iterrows():\n",
    "\n",
    "    risk_score, risk_level, main_factors = simulate_risk_alert(row)\n",
    "\n",
    "    results_risk.append({\n",
    "  \n",
    "        'student_id':                  row['student_id'],\n",
    "        'risk_level':                  risk_level,\n",
    "        'risk_score':                  risk_score,\n",
    "        'main_factors':                main_factors,\n",
    "        'avg_final':                   row['avg_final'],\n",
    "        'failed_subjects':             row['failed_subjects'],\n",
    "        'attendance_absences_total':   row['attendance_absences_total'],\n",
    "        'grade_tendency':              row['grade_tendency'],\n",
    "  \n",
    "    })\n",
    "\n",
    "df_risk = pd.DataFrame(results_risk)\n",
    "\n",
    "df_risk.to_csv('../datasets/nlp_pred_risk_alert.csv', index = False)\n",
    "\n",
    "print(f'Risk Alert: {len(df_risk)} estudiantes')\n",
    "print(df_risk['risk_level'].value_counts().to_string())\n",
    "\n",
    "print(f'\\nEstudiantes High Risk (top 10):')\n",
    "\n",
    "print(df_risk[df_risk['risk_level'] == 'High'][['student_id', 'risk_score', 'main_factors', 'avg_final']].head(10).to_string(index = False))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dedf1331",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "Predicción 4 — Recomendación de Carrera (career_recommendation)\n",
    "\n",
    "Clasifica al estudiante en un área vocacional basándose en sus\n",
    "fortalezas académicas relativas: STEM, Humanities, Business o Arts.\n",
    "\"\"\"\n",
    "\n",
    "class CareerRecommendation(BaseModel):\n",
    "\n",
    "    student_id:   str   = Field(description='Student ID')\n",
    "\n",
    "    career_area:  str   = Field(description='STEM, Humanities, Business or Arts')\n",
    "\n",
    "    confidence:   float = Field(description='Confidence from 0 to 1')\n",
    "\n",
    "    top_subjects: str   = Field(description='Top 3 subjects driving this recommendation')\n",
    "\n",
    "\n",
    "STEM_SUBJ       = ['Mathematics', 'Science', 'ICT', 'Physics', 'Chemistry', 'Biology', 'Additional_Math']\n",
    "HUMANITIES_SUBJ = ['English', 'History', 'Civics', 'Geography', 'Swahili', 'French', 'Religious_Education']\n",
    "BUSINESS_SUBJ   = ['Business_Studies', 'Entrepreneurship', 'Agriculture']\n",
    "ARTS_SUBJ       = ['Fine_Arts', 'Music', 'Physical_Education']\n",
    "\n",
    "\n",
    "def area_avg(student, subjects):\n",
    "\n",
    "    vals = [safe_float(student.get(f'{s}_final'), 0) for s in subjects]\n",
    "\n",
    "    vals = [v for v in vals if v > 0]\n",
    "\n",
    "    return sum(vals) / len(vals) if vals else 0.0\n",
    "\n",
    "\n",
    "def simulate_career_recommendation(student):\n",
    "\n",
    "    scores = {\n",
    "        'STEM':       area_avg(student, STEM_SUBJ),\n",
    "        'Humanities': area_avg(student, HUMANITIES_SUBJ),\n",
    "        'Business':   area_avg(student, BUSINESS_SUBJ),\n",
    "        'Arts':       area_avg(student, ARTS_SUBJ),\n",
    "    }\n",
    "\n",
    "    best_area  = max(scores, key = scores.get)\n",
    "    best_score = scores[best_area]\n",
    "    total      = sum(scores.values())\n",
    "\n",
    "    confidence = round(best_score / total if total > 0 else 0.25, 4)\n",
    "\n",
    "    all_subj = STEM_SUBJ + HUMANITIES_SUBJ + BUSINESS_SUBJ + ARTS_SUBJ\n",
    "\n",
    "    subj_scores = {s: safe_float(student.get(f'{s}_final'), 0) for s in all_subj if safe_float(student.get(f'{s}_final'), 0) > 0}\n",
    "\n",
    "    top3 = ', '.join(sorted(subj_scores, key = subj_scores.get, reverse = True)[:3])\n",
    "\n",
    "    return best_area, confidence, top3, scores\n",
    "\n",
    "\n",
    "results_career = []\n",
    "\n",
    "for _, row in df_context.iterrows():\n",
    "\n",
    "    career_area, confidence, top3, scores = simulate_career_recommendation(row)\n",
    "\n",
    "    results_career.append({\n",
    "        'student_id':        row['student_id'],\n",
    "        'career_area':       career_area,\n",
    "        'confidence':        confidence,\n",
    "        'top_subjects':      top3,\n",
    "        'score_stem':        round(scores['STEM'], 2),\n",
    "        'score_humanities':  round(scores['Humanities'], 2),\n",
    "        'score_business':    round(scores['Business'], 2),\n",
    "        'score_arts':        round(scores['Arts'], 2),\n",
    "        'avg_final':         row['avg_final'],\n",
    "    })\n",
    "\n",
    "df_career = pd.DataFrame(results_career)\n",
    "\n",
    "df_career.to_csv('../datasets/nlp_pred_career_recommendation.csv', index = False)\n",
    "\n",
    "print(f'Career Recommendation: {len(df_career)} estudiantes')\n",
    "print(df_career['career_area'].value_counts().to_string())\n",
    "\n",
    "print(df_career[['student_id', 'career_area', 'confidence', 'top_subjects']].head(10).to_string(index = False))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "internal_venv (3.13.11)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}