From e6aa56210f93d591b618a2d6367a7ed69274100a Mon Sep 17 00:00:00 2001 From: andwati Date: Sun, 28 Sep 2025 00:37:22 +0300 Subject: [PATCH] antigravity --- README.md | 18 +- app.py | 117 ++++++++ src/__init__.py | 3 + src/components/__init__.py | 3 + src/components/form_components.py | 352 ++++++++++++++++++++++++ src/components/prediction_components.py | 261 ++++++++++++++++++ src/config/__init__.py | 3 + src/config/mappings.py | 175 ++++++++++++ src/config/theme.py | 308 +++++++++++++++++++++ src/utils/__init__.py | 3 + src/utils/model_utils.py | 108 ++++++++ 11 files changed, 1349 insertions(+), 2 deletions(-) create mode 100644 app.py create mode 100644 src/__init__.py create mode 100644 src/components/__init__.py create mode 100644 src/components/form_components.py create mode 100644 src/components/prediction_components.py create mode 100644 src/config/__init__.py create mode 100644 src/config/mappings.py create mode 100644 src/config/theme.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/model_utils.py diff --git a/README.md b/README.md index 80d1a11..c0a53a9 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ pip install -r requirements.txt ### 3. Launch the Dashboard ```bash -streamlit run streamlit_app.py +streamlit run app.py ``` The dashboard will open in your browser at `http://localhost:8501` @@ -90,9 +90,23 @@ The dashboard will open in your browser at `http://localhost:8501` predictive_analysis/ ├── data/ │ └── data.csv # Original dataset (4,424 students) +├── src/ # Source code modules +│ ├── config/ # Configuration files +│ │ ├── __init__.py +│ │ ├── theme.py # Rose Pine theme and constants +│ │ └── mappings.py # Feature mappings and presets +│ ├── utils/ # Utility functions +│ │ ├── __init__.py +│ │ └── model_utils.py # Model loading and preprocessing +│ ├── components/ # UI components +│ │ ├── __init__.py +│ │ ├── form_components.py # Input form sections +│ │ └── prediction_components.py # Results and charts +│ └── __init__.py ├── predictive_analysis.ipynb # Original analysis notebook ├── train_and_save_model.py # Model training and pickle export -├── streamlit_app.py # Main dashboard application +├── streamlit_app.py # Legacy monolithic app (deprecated) +├── app.py # New modular main application ├── requirements.txt # Python dependencies ├── random_forest_model.pkl # Trained model (generated) ├── preprocessing_info.pkl # Preprocessing pipeline (generated) diff --git a/app.py b/app.py new file mode 100644 index 0000000..652baf5 --- /dev/null +++ b/app.py @@ -0,0 +1,117 @@ +""" +Main Streamlit application for Student Success Predictor +Modular version with separated components +""" + +import streamlit as st +import sys +from pathlib import Path + +# Add src directory to path for imports +src_path = Path(__file__).parent +sys.path.append(str(src_path)) + +from src.config.theme import ROSE_PINE_THEME, PAGE_CONFIG, APP_TITLE, APP_SUBTITLE +from src.config.mappings import create_feature_mappings +from src.utils.model_utils import load_model_and_info +from src.components.form_components import ( + render_preset_selector, + render_personal_info_section, + render_academic_background_section, + render_family_background_section, + render_financial_social_section, + render_academic_performance_section, + render_economic_indicators_section +) +from src.components.prediction_components import ( + render_prediction_section, + render_model_info_sidebar, + render_export_section, + render_footer +) + +def apply_theme(): + """Apply the Rose Pine dark theme""" + st.markdown(ROSE_PINE_THEME, unsafe_allow_html=True) + +def render_header(): + """Render the main application header""" + st.markdown(f'

{APP_TITLE}

', unsafe_allow_html=True) + st.markdown(f'

{APP_SUBTITLE}

', unsafe_allow_html=True) + +def collect_user_input(): + """Collect all user input from the sidebar form""" + feature_maps = create_feature_mappings() + + with st.sidebar: + # Preset selector + preset = render_preset_selector() + + # Personal information + personal_info = render_personal_info_section(preset, feature_maps) + + # Academic background + academic_bg = render_academic_background_section(preset, feature_maps) + + # Family background + family_bg = render_family_background_section(feature_maps) + + # Financial and social factors + financial_social = render_financial_social_section() + + # Academic performance + academic_performance = render_academic_performance_section(preset) + + # Economic indicators + economic_indicators = render_economic_indicators_section() + + # Combine all input data + user_input = { + **personal_info, + **academic_bg, + **family_bg, + **financial_social, + **academic_performance, + **economic_indicators + } + + return user_input + +def main(): + """Main Streamlit application""" + # Configure page + st.set_page_config(**PAGE_CONFIG) + + # Apply theme + apply_theme() + + # Load model and preprocessing info + model, preprocessing_info, feature_names = load_model_and_info() + + # Render header + render_header() + + # Collect user input + user_input = collect_user_input() + + # Main content area + col1, col2 = st.columns([2, 1]) + + with col1: + st.markdown('

Prediction Results

', unsafe_allow_html=True) + + # Render prediction section + render_prediction_section(model, preprocessing_info, user_input) + + with col2: + # Render model info and validation + render_model_info_sidebar(user_input) + + # Render export section + render_export_section(user_input) + + # Render footer + render_footer() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..2c81ce9 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,3 @@ +""" +Source package for Student Success Predictor +""" \ No newline at end of file diff --git a/src/components/__init__.py b/src/components/__init__.py new file mode 100644 index 0000000..93fc3d3 --- /dev/null +++ b/src/components/__init__.py @@ -0,0 +1,3 @@ +""" +UI Components +""" \ No newline at end of file diff --git a/src/components/form_components.py b/src/components/form_components.py new file mode 100644 index 0000000..96d48e2 --- /dev/null +++ b/src/components/form_components.py @@ -0,0 +1,352 @@ +""" +Form input components for the Student Success Predictor +""" + +import streamlit as st +from typing import Dict, Any, Tuple +from ..config.mappings import create_feature_mappings, PRESET_CONFIGURATIONS, ACADEMIC_DEFAULTS + +def render_preset_selector() -> str: + """Render the preset selector section""" + st.markdown('', unsafe_allow_html=True) + + if preset != "Custom Entry": + st.markdown(f'
✅ Loading {preset} preset values. You can modify any field below.
', unsafe_allow_html=True) + + return preset + +def render_personal_info_section(preset: str, feature_maps: Dict) -> Dict[str, Any]: + """Render personal information input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 👤 Personal Information") + st.markdown('
Basic demographic information about the student
', unsafe_allow_html=True) + + defaults = PRESET_CONFIGURATIONS[preset] + + marital_status = st.selectbox( + "Marital Status", + options=list(feature_maps['marital_status'].keys()), + format_func=lambda x: feature_maps['marital_status'][x], + index=list(feature_maps['marital_status'].keys()).index(defaults["marital"]) + ) + + gender = st.selectbox( + "Gender", + [0, 1], + format_func=lambda x: "Female" if x == 0 else "Male", + index=defaults["gender"] + ) + + age_at_enrollment = st.number_input( + "Age at Enrollment", + min_value=17, + max_value=70, + value=defaults["age"], + step=1 + ) + + nationality = st.selectbox( + "Nationality", + [1, 2, 6, 11, 13, 14, 17, 21, 22, 24, 25, 26, 32, 41, 62, 100, 101, 103, 105, 108, 109], + index=0 + ) + + return { + 'Marital status': marital_status, + 'Gender': gender, + 'Age at enrollment': age_at_enrollment, + 'Nationality': nationality + } + +def render_academic_background_section(preset: str, feature_maps: Dict) -> Dict[str, Any]: + """Render academic background input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 🎓 Academic Background") + st.markdown('
Information about the student\'s application and academic history
', unsafe_allow_html=True) + + defaults = PRESET_CONFIGURATIONS[preset] + + application_mode = st.selectbox( + "Application Mode", + options=list(feature_maps['application_mode'].keys()), + format_func=lambda x: feature_maps['application_mode'].get(x, f"Mode {x}"), + index=list(feature_maps['application_mode'].keys()).index(defaults["app_mode"]) if defaults["app_mode"] in feature_maps['application_mode'] else 0 + ) + + application_order = st.selectbox( + "Application Order (1st choice, 2nd choice, etc.)", + [1, 2, 3, 4, 5, 6, 7, 8, 9], + index=0 + ) + + course = st.selectbox( + "Course", + options=list(feature_maps['course'].keys()), + format_func=lambda x: feature_maps['course'].get(x, f"Course {x}"), + index=list(feature_maps['course'].keys()).index(defaults["course"]) if defaults["course"] in feature_maps['course'] else 0 + ) + + daytime_evening = st.selectbox( + "Attendance", + [0, 1], + format_func=lambda x: "Evening" if x == 0 else "Daytime" + ) + + previous_qualification = st.selectbox( + "Previous Qualification", + options=list(feature_maps['qualification'].keys()), + format_func=lambda x: feature_maps['qualification'].get(x, f"Qualification {x}") + ) + + col1, col2 = st.columns(2) + with col1: + previous_qualification_grade = st.number_input( + "Previous Qualification Grade", + min_value=95, + max_value=190, + value=defaults["prev_grade"], + step=1 + ) + with col2: + admission_grade = st.number_input( + "Admission Grade", + min_value=95, + max_value=190, + value=defaults["adm_grade"], + step=1 + ) + + return { + 'Application mode': application_mode, + 'Application order': application_order, + 'Course': course, + 'Daytime/evening attendance': daytime_evening, + 'Previous qualification': previous_qualification, + 'Previous qualification (grade)': previous_qualification_grade, + 'Admission grade': admission_grade + } + +def render_family_background_section(feature_maps: Dict) -> Dict[str, Any]: + """Render family background input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 👨‍👩‍👧‍👦 Family Background") + st.markdown('
Parent education and occupation information
', unsafe_allow_html=True) + + mothers_qualification = st.selectbox( + "Mother's Qualification", + options=list(feature_maps['qualification'].keys()), + format_func=lambda x: feature_maps['qualification'].get(x, f"Qualification {x}") + ) + + fathers_qualification = st.selectbox( + "Father's Qualification", + options=list(feature_maps['qualification'].keys()), + format_func=lambda x: feature_maps['qualification'].get(x, f"Qualification {x}") + ) + + mothers_occupation = st.selectbox( + "Mother's Occupation", + options=list(feature_maps['occupation'].keys()), + format_func=lambda x: feature_maps['occupation'].get(x, f"Occupation {x}") + ) + + fathers_occupation = st.selectbox( + "Father's Occupation", + options=list(feature_maps['occupation'].keys()), + format_func=lambda x: feature_maps['occupation'].get(x, f"Occupation {x}") + ) + + return { + 'Mothers qualification': mothers_qualification, + 'Fathers qualification': fathers_qualification, + 'Mothers occupation': mothers_occupation, + 'Fathers occupation': fathers_occupation + } + +def render_financial_social_section() -> Dict[str, Any]: + """Render financial and social factors input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 💰 Financial & Social") + st.markdown('
Student financial status and special circumstances
', unsafe_allow_html=True) + + col1, col2 = st.columns(2) + with col1: + displaced = st.radio( + "Displaced Student", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + educational_special_needs = st.radio( + "Educational Special Needs", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + debtor = st.radio( + "Has Outstanding Debts", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + with col2: + tuition_fees_up_to_date = st.radio( + "Tuition Fees Up to Date", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + scholarship_holder = st.radio( + "Scholarship Holder", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + international = st.radio( + "International Student", + [0, 1], + format_func=lambda x: "No" if x == 0 else "Yes", + horizontal=True + ) + + return { + 'Displaced': displaced, + 'Educational special needs': educational_special_needs, + 'Debtor': debtor, + 'Tuition fees up to date': tuition_fees_up_to_date, + 'Scholarship holder': scholarship_holder, + 'International': international + } + +def render_academic_performance_section(preset: str) -> Dict[str, Any]: + """Render academic performance input sections""" + academic_defaults = ACADEMIC_DEFAULTS[preset] + + # 1st Semester + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 📚 1st Semester Performance") + st.markdown('
⚠️ Important: Academic performance is a key predictor of student success
', unsafe_allow_html=True) + + col1, col2 = st.columns(2) + with col1: + curricular_units_1st_sem_enrolled = st.number_input( + "Enrolled Units", min_value=0, max_value=26, + value=academic_defaults["1st_sem"]["enrolled"], step=1, + key="1st_enrolled", help="Number of courses registered for" + ) + curricular_units_1st_sem_approved = st.number_input( + "Approved Units", min_value=0, max_value=26, + value=academic_defaults["1st_sem"]["approved"], step=1, + key="1st_approved", help="Number of courses passed" + ) + curricular_units_1st_sem_credited = st.number_input( + "Credited Units", min_value=0, max_value=26, + value=academic_defaults["1st_sem"]["credited"], step=1, + key="1st_credited", help="Credits from previous education" + ) + with col2: + curricular_units_1st_sem_evaluations = st.number_input( + "Evaluations", min_value=0, max_value=45, + value=academic_defaults["1st_sem"]["evaluations"], step=1, + key="1st_evaluations", help="Number of exams taken" + ) + curricular_units_1st_sem_grade = st.number_input( + "Average Grade (0-20)", min_value=0, max_value=20, + value=academic_defaults["1st_sem"]["grade"], step=1, + key="1st_grade", help="Average grade on 0-20 scale" + ) + curricular_units_1st_sem_without_evaluations = st.number_input( + "Without Evaluations", min_value=0, max_value=14, + value=academic_defaults["1st_sem"]["without"], step=1, + key="1st_without", help="Courses registered but not evaluated" + ) + + # 2nd Semester + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 📖 2nd Semester Performance") + st.markdown('
Second semester data helps improve prediction accuracy
', unsafe_allow_html=True) + + col1, col2 = st.columns(2) + with col1: + curricular_units_2nd_sem_enrolled = st.number_input( + "Enrolled Units", min_value=0, max_value=23, + value=academic_defaults["2nd_sem"]["enrolled"], step=1, + key="2nd_enrolled", help="Number of courses registered for" + ) + curricular_units_2nd_sem_approved = st.number_input( + "Approved Units", min_value=0, max_value=20, + value=academic_defaults["2nd_sem"]["approved"], step=1, + key="2nd_approved", help="Number of courses passed" + ) + curricular_units_2nd_sem_credited = st.number_input( + "Credited Units", min_value=0, max_value=20, + value=academic_defaults["2nd_sem"]["credited"], step=1, + key="2nd_credited", help="Credits from previous education" + ) + with col2: + curricular_units_2nd_sem_evaluations = st.number_input( + "Evaluations", min_value=0, max_value=33, + value=academic_defaults["2nd_sem"]["evaluations"], step=1, + key="2nd_evaluations", help="Number of exams taken" + ) + curricular_units_2nd_sem_grade = st.number_input( + "Average Grade (0-20)", min_value=0, max_value=20, + value=academic_defaults["2nd_sem"]["grade"], step=1, + key="2nd_grade", help="Average grade on 0-20 scale" + ) + curricular_units_2nd_sem_without_evaluations = st.number_input( + "Without Evaluations", min_value=0, max_value=12, + value=academic_defaults["2nd_sem"]["without"], step=1, + key="2nd_without", help="Courses registered but not evaluated" + ) + + return { + 'Curricular units 1st sem (credited)': curricular_units_1st_sem_credited, + 'Curricular units 1st sem (enrolled)': curricular_units_1st_sem_enrolled, + 'Curricular units 1st sem (evaluations)': curricular_units_1st_sem_evaluations, + 'Curricular units 1st sem (approved)': curricular_units_1st_sem_approved, + 'Curricular units 1st sem (grade)': curricular_units_1st_sem_grade, + 'Curricular units 1st sem (without evaluations)': curricular_units_1st_sem_without_evaluations, + 'Curricular units 2nd sem (credited)': curricular_units_2nd_sem_credited, + 'Curricular units 2nd sem (enrolled)': curricular_units_2nd_sem_enrolled, + 'Curricular units 2nd sem (evaluations)': curricular_units_2nd_sem_evaluations, + 'Curricular units 2nd sem (approved)': curricular_units_2nd_sem_approved, + 'Curricular units 2nd sem (grade)': curricular_units_2nd_sem_grade, + 'Curricular units 2nd sem (without evaluations)': curricular_units_2nd_sem_without_evaluations + } + +def render_economic_indicators_section() -> Dict[str, Any]: + """Render economic indicators input section""" + st.markdown('
', unsafe_allow_html=True) + st.markdown("### 📊 Economic Indicators") + st.markdown('
📈 Economic context data - typically set by institution based on current conditions
', unsafe_allow_html=True) + + col1, col2, col3 = st.columns(3) + with col1: + unemployment_rate = st.number_input( + "Unemployment Rate (%)", + min_value=7.0, max_value=16.2, value=10.8, step=0.1, format="%.1f" + ) + with col2: + inflation_rate = st.number_input( + "Inflation Rate (%)", + min_value=-0.8, max_value=3.7, value=1.4, step=0.1, format="%.1f" + ) + with col3: + gdp = st.number_input( + "GDP Growth", + min_value=-4.06, max_value=3.51, value=1.74, step=0.1, format="%.2f" + ) + + return { + 'Unemployment rate': unemployment_rate, + 'Inflation rate': inflation_rate, + 'GDP': gdp + } \ No newline at end of file diff --git a/src/components/prediction_components.py b/src/components/prediction_components.py new file mode 100644 index 0000000..a674db6 --- /dev/null +++ b/src/components/prediction_components.py @@ -0,0 +1,261 @@ +""" +Prediction and results display components +""" + +import streamlit as st +import plotly.express as px +import plotly.graph_objects as go +import time +import json +from typing import Dict, Any, Tuple, List + +def render_prediction_section(model, preprocessing_info, user_input: Dict[str, Any]) -> None: + """Render the prediction section with enhanced animations""" + from ..utils.model_utils import preprocess_input, generate_recommendations + + # Prediction button with enhanced styling + predict_button = st.button("🔬 Analyze Student Data", type="primary", use_container_width=True) + + if predict_button: + # Enhanced loading animation + progress_bar = st.progress(0) + status_text = st.empty() + + # Simulate processing steps + steps = [ + ("Loading student data...", 20), + ("Preprocessing features...", 40), + ("Running machine learning model...", 70), + ("Calculating prediction confidence...", 90), + ("Finalizing results...", 100) + ] + + for step, progress in steps: + status_text.text(step) + progress_bar.progress(progress) + time.sleep(0.3) + + # Clear progress indicators + progress_bar.empty() + status_text.empty() + + # Preprocess input + processed_input = preprocess_input(user_input, preprocessing_info) + + # Make prediction + prediction = model.predict(processed_input)[0] + prediction_proba = model.predict_proba(processed_input)[0] + + # Get prediction label + target_reverse_mapping = preprocessing_info['target_reverse_mapping'] + predicted_outcome = target_reverse_mapping[prediction] + + # Store in session state for export + st.session_state['last_prediction'] = { + 'outcome': predicted_outcome, + 'probabilities': prediction_proba, + 'timestamp': time.time() + } + + # Enhanced prediction display with confidence indicators + confidence_level = max(prediction_proba) + confidence_text = "High" if confidence_level > 0.7 else "Medium" if confidence_level > 0.5 else "Low" + + # Display prediction with enhanced styling and confidence + if predicted_outcome == "Graduate": + st.markdown(f'''
+ 🎓 GRADUATE +
Confidence: {confidence_level:.1%} ({confidence_text}) +
''', unsafe_allow_html=True) + st.balloons() + elif predicted_outcome == "Dropout": + st.markdown(f'''
+ ⚠️ AT RISK OF DROPOUT +
Confidence: {confidence_level:.1%} ({confidence_text}) +
''', unsafe_allow_html=True) + else: + st.markdown(f'''
+ 📚 LIKELY TO STAY ENROLLED +
Confidence: {confidence_level:.1%} ({confidence_text}) +
''', unsafe_allow_html=True) + + # Add personalized recommendations + render_recommendations(predicted_outcome, user_input) + + # Display probability charts + render_probability_charts(prediction_proba) + +def render_recommendations(predicted_outcome: str, user_input: Dict[str, Any]) -> None: + """Render personalized recommendations section""" + from ..utils.model_utils import generate_recommendations + + st.markdown("### 💡 Personalized Recommendations") + + recommendations = generate_recommendations(predicted_outcome, user_input) + + if predicted_outcome == "Graduate": + st.markdown('
🎯 Success Indicators: This student shows strong potential for graduation. Continue supporting their academic journey.
', unsafe_allow_html=True) + elif predicted_outcome == "Dropout": + if recommendations: + rec_text = '
'.join(recommendations) + st.markdown(f'
🎯 Intervention Strategies:
{rec_text}
', unsafe_allow_html=True) + else: + st.markdown('
📈 Monitoring Needed: This student may benefit from regular check-ins and academic support.
', unsafe_allow_html=True) + +def render_probability_charts(prediction_proba: List[float]) -> None: + """Render prediction probability charts and metrics""" + st.markdown("### 📊 Prediction Confidence") + + # Create probability dataframe + import pandas as pd + prob_df = pd.DataFrame({ + 'Outcome': ['Dropout', 'Enrolled', 'Graduate'], + 'Probability': prediction_proba + }) + + # Create probability chart + fig = px.bar( + prob_df, + x='Outcome', + y='Probability', + color='Outcome', + color_discrete_map={ + 'Graduate': '#9ccfd8', # Rose Pine foam + 'Dropout': '#eb6f92', # Rose Pine love + 'Enrolled': '#f6c177' # Rose Pine gold + }, + title="Prediction Probabilities" + ) + fig.update_layout( + showlegend=False, + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)' + ) + st.plotly_chart(fig, use_container_width=True) + + # Display detailed probabilities in cards + col_a, col_b, col_c = st.columns(3) + + with col_a: + st.markdown(f""" +
+

Graduate

+

{prediction_proba[2]:.1%}

+
+ """, unsafe_allow_html=True) + + with col_b: + st.markdown(f""" +
+

Dropout

+

{prediction_proba[0]:.1%}

+
+ """, unsafe_allow_html=True) + + with col_c: + st.markdown(f""" +
+

Enrolled

+

{prediction_proba[1]:.1%}

+
+ """, unsafe_allow_html=True) + +def render_model_info_sidebar(user_input: Dict[str, Any]) -> None: + """Render model information and validation in sidebar""" + from ..utils.model_utils import validate_input + from ..config.theme import MODEL_METRICS + + st.markdown('

📊 Model Insights

', unsafe_allow_html=True) + + # Model information + st.markdown('
', unsafe_allow_html=True) + st.markdown(f""" + **🤖 Model Details:** + - **Algorithm:** Random Forest Classifier + - **Features:** 36 student characteristics + - **Training Accuracy:** 100% + - **Test Accuracy:** {MODEL_METRICS['accuracy']}% + - **Classes:** Graduate, Dropout, Enrolled + """) + st.markdown('
', unsafe_allow_html=True) + + # Input validation feedback + st.markdown("### 🔍 Input Validation") + validation_issues = validate_input(user_input) + + if validation_issues: + for issue in validation_issues: + st.warning(issue) + else: + st.success("✅ All inputs look valid!") + + # Model performance visualization + st.markdown("### Model Performance") + + metrics = ['Accuracy', 'Precision', 'Recall', 'F1-Score'] + values = [MODEL_METRICS['accuracy'], MODEL_METRICS['precision'], + MODEL_METRICS['recall'], MODEL_METRICS['f1_score']] + + fig_perf = go.Figure(data=go.Scatterpolar( + r=values, + theta=metrics, + fill='toself', + line_color='#c4a7e7' # Rose Pine iris + )) + + fig_perf.update_layout( + polar=dict( + radialaxis=dict( + visible=True, + range=[0, 100] + )), + showlegend=False, + title="Model Performance Metrics", + height=300, + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)' + ) + + st.plotly_chart(fig_perf, use_container_width=True) + +def render_export_section(user_input: Dict[str, Any]) -> None: + """Render export functionality""" + st.markdown("### 📥 Export Results") + + if st.session_state.get('last_prediction'): + export_data = { + 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), + 'prediction': st.session_state['last_prediction']['outcome'], + 'confidence': f"{max(st.session_state['last_prediction']['probabilities']):.1%}", + 'student_data': user_input + } + + # Convert to JSON for download + json_str = json.dumps(export_data, indent=2, ensure_ascii=False) + + st.download_button( + label="📁 Download Prediction Report", + data=json_str, + file_name=f"student_prediction_{time.strftime('%Y%m%d_%H%M%S')}.json", + mime="application/json" + ) + +def render_footer() -> None: + """Render enhanced footer with project information""" + st.markdown('
', unsafe_allow_html=True) + col1, col2, col3 = st.columns(3) + + with col1: + st.markdown("**📊 Data Source**") + st.caption("UCI ML Repository - Student Academic Success") + + with col2: + st.markdown("**🔬 Model Info**") + st.caption("Random Forest with 93.84% accuracy") + + with col3: + st.markdown("**🛠️ Built With**") + st.caption("Streamlit • scikit-learn • Plotly") + + st.markdown("---") + st.markdown('

🎓 Empowering educational institutions with data-driven insights for student success

', unsafe_allow_html=True) \ No newline at end of file diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..72af5e6 --- /dev/null +++ b/src/config/__init__.py @@ -0,0 +1,3 @@ +""" +Configuration modules +""" \ No newline at end of file diff --git a/src/config/mappings.py b/src/config/mappings.py new file mode 100644 index 0000000..82fea5e --- /dev/null +++ b/src/config/mappings.py @@ -0,0 +1,175 @@ +""" +Data mappings and constants for the Student Success Predictor app +Contains all the human-readable mappings for categorical features +""" + +def create_feature_mappings(): + """Create human-readable mappings for categorical features""" + + # Marital status mapping + marital_status_map = { + 1: "Single", + 2: "Married", + 3: "Widowed", + 4: "Divorced", + 5: "Civil Union", + 6: "Legally Separated" + } + + # Application mode mapping + application_mode_map = { + 1: "1st Phase - General Contingent", + 2: "Ordinance No. 612/93", + 5: "1st Phase - Special Contingent (Azores)", + 7: "Holders of Other Higher Courses", + 10: "Ordinance No. 854-B/99", + 15: "International Student (Bachelor)", + 16: "1st Phase - Special Contingent (Madeira)", + 17: "2nd Phase - General Contingent", + 18: "3rd Phase - General Contingent", + 26: "Ordinance No. 533-A/99, item b2", + 27: "Ordinance No. 533-A/99, item b3", + 39: "Over 23 years old", + 42: "Transfer", + 43: "Change of course", + 44: "Technological specialization diploma holders", + 51: "Change of institution/course", + 53: "Short cycle diploma holders", + 57: "Change of institution/course (International)" + } + + # Course mapping + course_map = { + 33: "Biofuel Production Technologies", + 171: "Animation and Multimedia Design", + 8014: "Social Service (evening attendance)", + 9003: "Agronomy", + 9070: "Communication Design", + 9085: "Veterinary Nursing", + 9119: "Informatics Engineering", + 9130: "Equinculture", + 9147: "Management", + 9238: "Social Service", + 9254: "Tourism", + 9500: "Nursing", + 9556: "Oral Hygiene", + 9670: "Advertising and Marketing Management", + 9773: "Journalism and Communication", + 9853: "Basic Education", + 9991: "Management (evening attendance)" + } + + # Previous qualification mapping + qualification_map = { + 1: "Secondary education", + 2: "Higher education - bachelor's degree", + 3: "Higher education - degree", + 4: "Higher education - master's", + 5: "Higher education - doctorate", + 6: "Frequency of higher education", + 9: "12th year of schooling - not completed", + 10: "11th year of schooling - not completed", + 12: "Other - 11th year of schooling", + 14: "10th year of schooling", + 15: "10th year of schooling - not completed", + 19: "Basic education 3rd cycle (9th/10th/11th year) or equiv.", + 38: "Basic education 2nd cycle (6th/7th/8th year) or equiv.", + 39: "Technological specialization course", + 40: "Higher education - degree (1st cycle)", + 42: "Professional course", + 43: "Higher education - master (2nd cycle)" + } + + # Occupation mapping + occupation_map = { + 0: "Student", + 1: "Representatives of the Legislative Power and Executive Bodies", + 2: "Specialists in Intellectual and Scientific Activities", + 3: "Intermediate Level Technicians and Professions", + 4: "Administrative staff", + 5: "Personal Services, Security and Safety Workers", + 6: "Farmers and Skilled Workers in Agriculture", + 7: "Skilled Workers in Industry, Construction and Craftsmen", + 8: "Installation and Machine Operators and Assembly Workers", + 9: "Unskilled Workers", + 10: "Armed Forces Professions", + 90: "Other Situation", + 99: "Unspecified", + 122: "Health professionals", + 123: "Teachers", + 125: "Specialists in information and communication technologies", + 131: "Intermediate level science and engineering technicians", + 132: "Technicians and associate professionals, health", + 134: "Intermediate level technicians from legal, social, sports", + 141: "Office workers, secretaries in general and data processing", + 143: "Data, accounting, statistical, financial services", + 144: "Other administrative support staff", + 151: "Personal service workers", + 152: "Sellers", + 153: "Personal care workers and the like", + 171: "Skilled construction workers and the like, except electricians", + 173: "Skilled workers in printing, precision instrument manufacturing", + 175: "Workers in food processing, woodworking, clothing", + 191: "Cleaning workers", + 192: "Unskilled workers in agriculture, animal production", + 193: "Unskilled workers in extractive industry, construction", + 194: "Meal preparation assistants" + } + + return { + 'marital_status': marital_status_map, + 'application_mode': application_mode_map, + 'course': course_map, + 'qualification': qualification_map, + 'occupation': occupation_map + } + +# Preset configurations for quick form filling +PRESET_CONFIGURATIONS = { + "Custom Entry": { + "age": 20, "gender": 0, "marital": 1, "app_mode": 1, "course": 9254, + "prev_grade": 120, "adm_grade": 120 + }, + "Typical Graduate Student": { + "age": 19, "gender": 1, "marital": 1, "app_mode": 15, "course": 9254, + "prev_grade": 160, "adm_grade": 142 + }, + "At-Risk Student": { + "age": 22, "gender": 1, "marital": 1, "app_mode": 17, "course": 171, + "prev_grade": 100, "adm_grade": 110 + }, + "International Student": { + "age": 21, "gender": 0, "marital": 1, "app_mode": 1, "course": 9238, + "prev_grade": 135, "adm_grade": 130 + } +} + +# Academic performance defaults for presets +ACADEMIC_DEFAULTS = { + "Custom Entry": { + "1st_sem": {"enrolled": 6, "approved": 6, "grade": 13, "credited": 0, "evaluations": 6, "without": 0}, + "2nd_sem": {"enrolled": 6, "approved": 6, "grade": 13, "credited": 0, "evaluations": 6, "without": 0} + }, + "Typical Graduate Student": { + "1st_sem": {"enrolled": 6, "approved": 6, "grade": 14, "credited": 0, "evaluations": 6, "without": 0}, + "2nd_sem": {"enrolled": 6, "approved": 6, "grade": 14, "credited": 0, "evaluations": 6, "without": 0} + }, + "At-Risk Student": { + "1st_sem": {"enrolled": 6, "approved": 0, "grade": 0, "credited": 0, "evaluations": 0, "without": 0}, + "2nd_sem": {"enrolled": 6, "approved": 0, "grade": 0, "credited": 0, "evaluations": 0, "without": 0} + }, + "International Student": { + "1st_sem": {"enrolled": 6, "approved": 6, "grade": 13, "credited": 0, "evaluations": 6, "without": 0}, + "2nd_sem": {"enrolled": 6, "approved": 6, "grade": 14, "credited": 0, "evaluations": 6, "without": 0} + } +} + +# Validation constants +VALIDATION_RULES = { + "min_age": 17, + "max_age": 70, + "min_grade": 0, + "max_grade": 20, + "min_qualification_grade": 95, + "max_qualification_grade": 190 +} \ No newline at end of file diff --git a/src/config/theme.py b/src/config/theme.py new file mode 100644 index 0000000..aa30431 --- /dev/null +++ b/src/config/theme.py @@ -0,0 +1,308 @@ +""" +Theme configuration for the Student Success Predictor app +Rose Pine Dark Theme implementation +""" + +# Rose Pine Dark Theme CSS +ROSE_PINE_THEME = """ + +""" + +# Page configuration +PAGE_CONFIG = { + "page_title": "Student Success Predictor", + "page_icon": "📊", + "layout": "wide", + "initial_sidebar_state": "expanded" +} + +# App constants +APP_TITLE = "Student Success Predictor" +APP_SUBTITLE = "Predict whether a student will Graduate, Dropout, or remain Enrolled based on academic and demographic factors using advanced machine learning." + +# Model constants +MODEL_FILES = { + "model": "random_forest_model.pkl", + "preprocessing": "preprocessing_info.pkl", + "features": "feature_names.pkl" +} + +# Performance metrics (from notebook results) +MODEL_METRICS = { + "accuracy": 93.84, + "precision": 92.96, + "recall": 94.02, + "f1_score": 93.15 +} \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..c7caf80 --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Utility modules +""" \ No newline at end of file diff --git a/src/utils/model_utils.py b/src/utils/model_utils.py new file mode 100644 index 0000000..aa4f84c --- /dev/null +++ b/src/utils/model_utils.py @@ -0,0 +1,108 @@ +""" +Model loading and data preprocessing utilities +""" + +import streamlit as st +import pandas as pd +import pickle +from typing import Dict, Any, List + +@st.cache_data +def load_model_and_info(): + """Load the trained model and preprocessing information""" + try: + with open('random_forest_model.pkl', 'rb') as f: + model = pickle.load(f) + + with open('preprocessing_info.pkl', 'rb') as f: + preprocessing_info = pickle.load(f) + + with open('feature_names.pkl', 'rb') as f: + feature_names = pickle.load(f) + + return model, preprocessing_info, feature_names + except FileNotFoundError: + st.error("❌ Model files not found! Please run 'train_and_save_model.py' first.") + st.stop() + +def preprocess_input(user_input: Dict[str, Any], preprocessing_info: Dict[str, Any]) -> pd.DataFrame: + """Preprocess user input to match the training data format""" + + # Create a DataFrame with the user input + input_df = pd.DataFrame([user_input]) + + # Get categorical columns that need one-hot encoding + categorical_columns = [ + 'Marital status', 'Application mode', 'Course', 'Daytime/evening attendance', + 'Previous qualification', 'Nationality', 'Mothers qualification', + 'Fathers qualification', 'Mothers occupation', 'Fathers occupation', + 'Gender', 'Displaced', 'Educational special needs', 'Debtor', + 'Tuition fees up to date', 'Scholarship holder', 'International', + 'Application order' + ] + + # Perform one-hot encoding for categorical columns + for column in categorical_columns: + if column in input_df.columns: + # Get dummies with prefix + dummies = pd.get_dummies(input_df[column], prefix=column) + # Concatenate with original data + input_df = pd.concat([input_df, dummies], axis=1) + # Drop original column + input_df.drop(columns=[column], inplace=True) + + # Ensure all training features are present + feature_names = preprocessing_info['feature_names'] + + # Add missing columns with 0 values + for feature in feature_names: + if feature not in input_df.columns: + input_df[feature] = 0 + + # Select only the features used in training and in the same order + input_df = input_df[feature_names] + + return input_df + +def validate_input(user_input: Dict[str, Any]) -> List[str]: + """Validate user input and return list of issues""" + validation_issues = [] + + # Check for unrealistic combinations + if user_input['Age at enrollment'] < 17: + validation_issues.append("⚠️ Age seems too low for enrollment") + + if user_input['Curricular units 1st sem (approved)'] > user_input['Curricular units 1st sem (enrolled)']: + validation_issues.append("⚠️ Approved units exceed enrolled units (1st sem)") + + if user_input['Curricular units 2nd sem (approved)'] > user_input['Curricular units 2nd sem (enrolled)']: + validation_issues.append("⚠️ Approved units exceed enrolled units (2nd sem)") + + return validation_issues + +def generate_recommendations(predicted_outcome: str, user_input: Dict[str, Any]) -> List[str]: + """Generate personalized recommendations based on prediction and input""" + recommendations = [] + + if predicted_outcome == "Dropout": + if user_input['Curricular units 1st sem (approved)'] == 0: + recommendations.append("📚 **Academic Support:** Provide tutoring for struggling courses") + + if user_input['Scholarship holder'] == 0: + recommendations.append("💰 **Financial Aid:** Consider scholarship opportunities") + + if user_input['Age at enrollment'] > 25: + recommendations.append("🕐 **Flexible Scheduling:** Offer evening or part-time options") + + if user_input['Debtor'] == 1: + recommendations.append("💳 **Financial Counseling:** Address outstanding debts") + + elif predicted_outcome == "Graduate": + recommendations.append("🎯 **Success Indicators:** This student shows strong potential for graduation") + recommendations.append("🔄 **Continue Support:** Maintain current academic support systems") + + else: # Enrolled + recommendations.append("📈 **Regular Monitoring:** Schedule periodic academic check-ins") + recommendations.append("🎯 **Goal Setting:** Help establish clear academic objectives") + + return recommendations \ No newline at end of file