diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..bbaca11 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e1b5bf3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/student-retention.iml b/.idea/student-retention.iml new file mode 100644 index 0000000..42b6342 --- /dev/null +++ b/.idea/student-retention.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index a4c29ab..c072543 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# student retention \ No newline at end of file +# student retention + +A beautiful Streamlit application that predicts student academic outcomes (Graduate, Dropout, or Enrolled) using a Random Forest model trained on 36 features including academic performance, demographics, and economic indicators. \ No newline at end of file diff --git a/models/train_and_save_model.py b/models/train_and_save_model.py index e69de29..8edcfbc 100644 --- a/models/train_and_save_model.py +++ b/models/train_and_save_model.py @@ -0,0 +1,166 @@ +""" +Model Training and Pickle Export Script +This script recreates the Random Forest model training process and saves the model and preprocessing pipeline. +""" + +import pickle + +import pandas as pd +from sklearn.ensemble import RandomForestClassifier +from sklearn.model_selection import train_test_split + + +def load_and_preprocess_data(): + """Load and preprocess the data exactly as done in the notebook""" + # Load data + data_file_name_path = "./data/data.csv" + master_raw_data = pd.read_csv(data_file_name_path) + data = master_raw_data.copy() + + # Fix column name typo + data.rename(columns={'Nacionality': 'Nationality'}, inplace=True) + + # Remove apostrophes from column names + for column_name in data.columns: + if "'" in column_name: + new_column_name = column_name.replace("'", "") + data.rename(columns={column_name: new_column_name}, inplace=True) + + return data + + +def encode_target_variable(data): + """Encode the target variable as done in the notebook""" + # Label mapping from the notebook analysis + target_mapping = {'Dropout': 0, 'Enrolled': 1, 'Graduate': 2} + data['Target_encoded'] = data['Target'].map(target_mapping) + return data + + +def prepare_features_for_modeling(data): + """Prepare features exactly as done in the notebook""" + # Get all 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' + ] + + # Additional columns that might be categorical (from notebook analysis) + for col in data.columns: + if col not in categorical_columns and col not in ['Target', 'Target_encoded']: + unique_vals = data[col].nunique() + # If low unique values, likely categorical + if unique_vals <= 20 and col not in ['Age at enrollment']: + categorical_columns.append(col) + + # Create a copy for processing + processed_data = data.copy() + + # Perform one-hot encoding for categorical columns + for column in categorical_columns: + if column in processed_data.columns: + # Get dummies with prefix + dummies = pd.get_dummies(processed_data[column], prefix=column) + # Concatenate with original data + processed_data = pd.concat([processed_data, dummies], axis=1) + # Drop original column + processed_data.drop(columns=[column], inplace=True) + + # Drop the original Target column, keep Target_encoded + processed_data.drop(columns=['Target'], inplace=True) + + return processed_data + + +def train_and_save_model(): + """Train the Random Forest model and save both model and preprocessing info""" + + # Load and preprocess data + print("Loading and preprocessing data...") + data = load_and_preprocess_data() + + # Encode target variable + data = encode_target_variable(data) + + # Prepare features + processed_data = prepare_features_for_modeling(data) + + # Separate features and target + X = processed_data.drop('Target_encoded', axis=1) + y = processed_data['Target_encoded'] + + print(f"Final dataset shape: {X.shape}") + print(f"Features: {X.columns.tolist()}") + + # Split data (using 30% test size as in the notebook) + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=42) + + # Train Random Forest model + print("Training Random Forest model...") + rf_classifier = RandomForestClassifier(n_estimators=100, random_state=42) + rf_classifier.fit(X_train, y_train) + + # Evaluate model + train_accuracy = rf_classifier.score(X_train, y_train) + test_accuracy = rf_classifier.score(X_test, y_test) + + print(f"Training Accuracy: {train_accuracy:.4f}") + print(f"Test Accuracy: {test_accuracy:.4f}") + + # Save the trained model + with open('random_forest_model.pkl', 'wb') as f: + pickle.dump(rf_classifier, f) + + # Save feature names for consistency + feature_names = X.columns.tolist() + with open('feature_names.pkl', 'wb') as f: + pickle.dump(feature_names, f) + + # Save preprocessing information + preprocessing_info = { + 'target_mapping': {'Dropout': 0, 'Enrolled': 1, 'Graduate': 2}, + 'target_reverse_mapping': {0: 'Dropout', 1: 'Enrolled', 2: 'Graduate'}, + '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' # Adding this based on notebook analysis + ], + 'feature_names': feature_names, + 'original_columns': [ + 'Marital status', 'Application mode', 'Application order', 'Course', + 'Daytime/evening attendance', 'Previous qualification', + 'Previous qualification (grade)', 'Nationality', 'Mothers qualification', + 'Fathers qualification', 'Mothers occupation', 'Fathers occupation', + 'Admission grade', 'Displaced', 'Educational special needs', 'Debtor', + 'Tuition fees up to date', 'Gender', 'Scholarship holder', + 'Age at enrollment', 'International', 'Curricular units 1st sem (credited)', + 'Curricular units 1st sem (enrolled)', 'Curricular units 1st sem (evaluations)', + 'Curricular units 1st sem (approved)', 'Curricular units 1st sem (grade)', + 'Curricular units 1st sem (without evaluations)', + 'Curricular units 2nd sem (credited)', 'Curricular units 2nd sem (enrolled)', + 'Curricular units 2nd sem (evaluations)', 'Curricular units 2nd sem (approved)', + 'Curricular units 2nd sem (grade)', 'Curricular units 2nd sem (without evaluations)', + 'Unemployment rate', 'Inflation rate', 'GDP' + ] + } + + with open('preprocessing_info.pkl', 'wb') as f: + pickle.dump(preprocessing_info, f) + + print("Model and preprocessing info saved successfully!") + print("Files created:") + print("- random_forest_model.pkl") + print("- feature_names.pkl") + print("- preprocessing_info.pkl") + + return rf_classifier, preprocessing_info + + +if __name__ == "__main__": + train_and_save_model() \ No newline at end of file