Project Overview
This project demonstrates how institutional enrollment, demographic, geographic, academic, athletic, and financial-aid data can be transformed into a machine-learning workflow for student-retention analysis. The workflow includes target-variable construction, feature engineering, data integration, class balancing, model training, prediction, evaluation, and model interpretation.
- Data Cleaning
- Feature Engineering
- Geographic Data
- Financial-Aid Data
- SMOTE
- ADASYN
- Random Forest
- Classification Metrics
- Permutation Importance
- Streamlit Deployment
Analytical Workflow
Load and clean institutional data
Construct the retention outcome
Engineer student-level features
Integrate ZIP-code and financial-aid data
Prepare training and testing samples
Address outcome imbalance
Train and evaluate the model
Interpret and deploy predictions
Section 01
Loading and Cleaning Institutional Data
The workflow begins by loading the required libraries and institutional dataset. Variable names are standardized, blank strings are converted to missing values, and the sample is restricted to the relevant student-entry statuses and cohort records.
# Import the libraries used throughout the workflow
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
# Load the institutional enrollment dataset
df = pd.read_csv(
"institutional_enrollment.csv",
low_memory=False
)
# Standardize column names
df.columns = (
df.columns
.str.strip()
.str.lower()
.str.replace(
" ",
"_",
regex=False
)
)
# Convert blank strings into missing values
df.replace(
"",
np.nan,
inplace=True
)
# Standardize student-entry status
df["student_orig_entry_status"] = (
df["student_orig_entry_status"]
.str.strip()
.str.upper()
)
# Keep selected entry-status groups
eligible_statuses = [
"FF",
"AO",
"CP",
"GP",
"ON"
]
df = df[
df["student_orig_entry_status"]
.isin(eligible_statuses)
]
# Retain records with a valid cohort semester
df = df[
df["period_tbl_cohort_semester"]
.notna()
]
Section 02
Constructing the Retention Target
Cohort labels are converted into machine-readable semester values. A retention outcome is then constructed by comparing the student’s initial semester with enrollment information in the relevant subsequent period.
from functions import convert_cohort_to_semester
# Convert a cohort label such as "02/FA"
# into a semester identifier such as 200208
df["beginning_semester"] = (
df["student_cohort"]
.apply(convert_cohort_to_semester)
)
# Illustrative retention construction
df["retention"] = np.where(
df["observed_semester"]
> df["beginning_semester"],
1,
0
)
# Remove records without a valid target value
df = df.dropna(
subset=["retention"]
)
# Review outcome distribution
print(
df["retention"]
.value_counts()
)
Section 03
Feature Engineering
Raw institutional fields are transformed into features that can be used by a machine-learning model. Examples include degree status, age at entry, athletic participation, football participation, cumulative GPA, credits, and demographic indicators.
# Degree-seeking indicator
df["degree_nondegree"] = (
df["degree_nondegree"]
.map({
"D": 1,
"N": 0
})
)
# Convert birth date to a datetime value
df["student_dob"] = pd.to_datetime(
df["student_dob"],
errors="coerce"
)
# Calculate approximate age at entry
df["entry_date"] = pd.to_datetime(
df["entry_date"],
errors="coerce"
)
df["age_at_entry"] = (
(
df["entry_date"]
- df["student_dob"]
).dt.days
/ 365.25
)
# Combine multiple varsity-sport indicators
sport_columns = [
"varsity_sports_a",
"varsity_sports_b",
"varsity_sports_c",
"varsity_sports_d"
]
df["athlete"] = (
df[sport_columns]
.fillna(0)
.max(axis=1)
.astype(int)
)
# Selected variables for the retention model
model_columns = [
"student_key",
"retention",
"female",
"age_at_entry",
"cum_gpa",
"total_credit_hours",
"athlete",
"football",
"degree_nondegree"
]
df = df[model_columns].copy()
Section 04
Integrating Geographic and Financial-Aid Data
Student ZIP-code information is cleaned and merged with a geographic reference file. Financial-aid records are also categorized and merged into the student-semester dataset to provide additional predictors of retention.
import re
# Standardize permanent ZIP codes
df["perm_zip_code"] = (
df["perm_zip_code"]
.astype(str)
.str.strip()
)
# Keep the first five numeric characters
df["ir_zip"] = (
df["perm_zip_code"]
.str.extract(
r"(\d{5})",
expand=False
)
)
# Load ZIP-code coordinates and distance measures
zip_reference = pd.read_csv(
"us_zip_codes.csv",
dtype={"ZIP": str}
)
# Merge geographic information
df = df.merge(
zip_reference[
[
"ZIP",
"LAT",
"LNG",
"distance"
]
],
left_on="ir_zip",
right_on="ZIP",
how="left"
)
# Use a fixed value for international locations
international_mask = (
df["perm_zip_code"]
.str.contains(
r"[A-Za-z]",
na=False
)
)
df.loc[
international_mask,
"distance"
] = 3000
def categorize_description(description):
"""
Convert financial-aid descriptions into broad categories.
"""
description = str(description).strip().lower()
if "grant" in description:
return 1
if "scholarship" in description:
return 2
if "loan" in description:
return 3
return 0
financial_aid = pd.read_csv(
"student_financial_aid.csv",
low_memory=False
)
financial_aid["financial_categories"] = (
financial_aid["aid_source_name"]
.apply(categorize_description)
)
# Build student-semester merge keys
df["student_semester_key"] = (
df["observed_semester"].astype(str)
+ "_"
+ df["student_key"].astype(str)
)
financial_aid["student_semester_key"] = (
financial_aid["semester"].astype(str)
+ "_"
+ financial_aid["student_key"].astype(str)
)
# Merge financial-aid categories
df = df.merge(
financial_aid[
[
"student_semester_key",
"financial_categories"
]
],
on="student_semester_key",
how="left"
)
df["financial_categories"] = (
df["financial_categories"]
.fillna(0)
)
df = pd.get_dummies(
df,
columns=["financial_categories"],
dtype=int
)
Section 05
Preparing Training and Testing Samples
A recent cohort is separated as a prospective prediction group. Earlier observations are used to train and evaluate the model through a stratified train-test split.
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
# Hold out the Fall 2023 cohort for prospective prediction
prediction_cohort = df[
df["beginning_semester"] == 202308
].copy()
# Use prior cohorts for model development
model_df = df[
df["beginning_semester"] != 202308
].copy()
# Remove identifiers and variables unavailable at prediction time
X = model_df.drop(
columns=[
"retention",
"student_key",
"beginning_semester"
]
)
y = model_df["retention"]
# Create stratified training and testing samples
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.20,
random_state=42,
stratify=y
)
# Scale features using training data only
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
Section 06
Handling Imbalanced Retention Outcomes
Retention datasets often contain substantially more retained students than non-retained students. Synthetic oversampling methods can help the model learn patterns associated with the less common outcome.
from imblearn.over_sampling import (
SMOTE,
BorderlineSMOTE,
ADASYN
)
# Review the original outcome distribution
print(
y_train.value_counts()
)
# Create a balanced training sample with ADASYN
adasyn = ADASYN(
random_state=42
)
X_train_balanced, y_train_balanced = (
adasyn.fit_resample(
X_train_scaled,
y_train
)
)
# Confirm the new class distribution
print(
pd.Series(
y_train_balanced
).value_counts()
)
Section 07
Random Forest Retention Model
A Random Forest classifier combines many decision trees to capture nonlinear patterns and interactions among academic, demographic, geographic, athletic, and financial predictors.
Nonlinear Relationships
Trees can identify threshold effects and nonlinear associations without requiring a prespecified functional form.
Variable Interactions
The model can learn interactions between academic, financial, geographic, and demographic characteristics.
Ensemble Prediction
Predictions are aggregated across many trees, typically improving stability relative to a single decision tree.
from sklearn.ensemble import RandomForestClassifier
# Configure the classifier
random_forest = RandomForestClassifier(
n_estimators=500,
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
class_weight="balanced",
random_state=42,
n_jobs=-1
)
# Fit the model to the balanced training sample
random_forest.fit(
X_train_balanced,
y_train_balanced
)
# Generate class and probability predictions
train_predictions = random_forest.predict(
X_train_balanced
)
test_predictions = random_forest.predict(
X_test_scaled
)
test_probabilities = random_forest.predict_proba(
X_test_scaled
)[:, 1]
Section 08
Model Evaluation
Accuracy alone can be misleading when retention outcomes are imbalanced. The evaluation therefore includes precision, recall, F1 scores, a confusion matrix, and the area under the receiver operating characteristic curve.
from sklearn.metrics import (
classification_report,
confusion_matrix,
accuracy_score,
balanced_accuracy_score,
roc_auc_score
)
# Classification report
print(
classification_report(
y_test,
test_predictions,
digits=3
)
)
# Confusion matrix
confusion = confusion_matrix(
y_test,
test_predictions
)
print(
"Confusion matrix:\n",
confusion
)
# Summary metrics
accuracy = accuracy_score(
y_test,
test_predictions
)
balanced_accuracy = balanced_accuracy_score(
y_test,
test_predictions
)
roc_auc = roc_auc_score(
y_test,
test_probabilities
)
print(
f"Accuracy: {accuracy:.3f}"
)
print(
f"Balanced accuracy: {balanced_accuracy:.3f}"
)
print(
f"ROC-AUC: {roc_auc:.3f}"
)
Section 09
Model Interpretability
Permutation importance measures how much predictive performance declines when the values of one feature are randomly rearranged. It provides an intuitive way to identify variables that contribute meaningfully to out-of-sample prediction.
from sklearn.inspection import permutation_importance
# Estimate importance using the untouched testing sample
importance_result = permutation_importance(
random_forest,
X_test_scaled,
y_test,
n_repeats=20,
random_state=42,
scoring="balanced_accuracy",
n_jobs=-1
)
# Organize results into a readable table
importance_table = pd.DataFrame({
"feature": X.columns,
"importance_mean": importance_result.importances_mean,
"importance_sd": importance_result.importances_std
})
importance_table = (
importance_table
.sort_values(
"importance_mean",
ascending=False
)
.reset_index(drop=True)
)
# Display the most influential predictors
print(
importance_table.head(15)
)
Section 10
Prospective Prediction and Deployment
After model evaluation, the trained preprocessing objects and classifier can be applied to an incoming cohort. The model and scaler may also be saved for integration into a Streamlit application or another institutional analytics workflow.
from joblib import dump
# Retain student identifiers before transformation
prediction_student_ids = (
prediction_cohort["student_key"]
.astype(str)
.copy()
)
# Match the training feature structure
prediction_features = (
prediction_cohort
.reindex(
columns=X.columns,
fill_value=0
)
)
# Apply training-sample scaling
prediction_features_scaled = scaler.transform(
prediction_features
)
# Generate predicted retention probabilities
predicted_probability = random_forest.predict_proba(
prediction_features_scaled
)[:, 1]
# Create a restricted internal output table
prediction_output = pd.DataFrame({
"student_key": prediction_student_ids,
"predicted_retention_probability":
predicted_probability
})
# Save model components for deployment
dump(
random_forest,
"retention_random_forest.joblib"
)
dump(
scaler,
"retention_scaler.joblib"
)
Responsible Use and Data Privacy
The code displayed on this page has been simplified and reformatted for portfolio and educational purposes. Student-level institutional data should remain confidential and should not be distributed through a public website or public code repository. Any operational use of a retention model should include privacy safeguards, fairness evaluation, documentation, human review, and careful consideration of how predictions may affect students. Predictive scores should support—not replace—professional judgment and direct student engagement.