A machine-learning credit-decisioning project that predicts whether a loan application should be approved, with a feature set and evaluation approach designed around affordability checks used in microfinance and cooperative lending (the kind of underwriting a SACCO, Ajo/Esusu group, or microfinance institution does manually) rather than a generic Kaggle notebook.
The goal isn't just "get a good F1 score" - it's to build a small, honest, end-to-end credit-risk pipeline: clean data → engineered affordability features → multiple models compared fairly → the winner tuned and explained → shipped behind a CLI and a small interactive app.
Live demo · Results · Why these features
Across many African markets, formal credit-history data is thin, but informal and cooperative savings/lending groups (Ajo, Esusu, Chama, SACCOs) have been assessing creditworthiness for generations using a handful of simple, explainable signals: can this person actually afford the repayment, given their income and existing obligations? This project takes a classic loan-approval dataset and rebuilds the pipeline around that lens - explicit affordability features (EMI burden, balance income after repayment, income-to-loan ratio) instead of throwing raw income/loan columns at a model and hoping for the best.
Given an applicant's demographic and financial profile, predict whether
Dream Housing Finance approves the loan (Loan_Status = Y/N).
- Source: Loan Prediction Problem Dataset (Analytics Vidhya / Kaggle, public domain for ML practice)
- Samples: 614 applications - 422 approved (68.7%), 192 denied (31.3%)
- Raw features: 11 inputs (demographic + financial) + 1 target
- Missingness: 7 of 12 raw columns have missing values (
Credit_Historyalone is missing for 50 rows)
| Feature | Description |
|---|---|
| Gender, Married, Dependents | Household demographics |
| Education, Self_Employed | Employment profile |
| ApplicantIncome, CoapplicantIncome | Monthly income streams |
| LoanAmount, Loan_Amount_Term | Requested loan (thousands) and term (months) |
| Credit_History | 1 = clean, 0 = adverse/no history |
| Property_Area | Urban / Semiurban / Rural |
| Loan_Status | Target: Y (approved) / N (denied) |
On top of cleaning (mode/median imputation, Dependents string→int, an
explicit Credit_History_Missing flag since a missing credit history is
itself a signal - a first-time or informal-sector borrower with no prior
formal credit trail), this project adds affordability features that mirror
manual loan-committee checks:
| Engineered feature | What it represents |
|---|---|
TotalIncome |
Household income (applicant + co-applicant) |
EMI |
Estimated monthly repayment (LoanAmount / Term) |
BalanceIncome |
Income left over after the EMI - the real affordability signal |
IncomeToLoanRatio |
How large the loan is relative to income |
LogTotalIncome, LogLoanAmount |
Log-transforms to tame right-skew |
HasCoapplicant |
Whether a co-applicant shares repayment responsibility |
Seven baseline classifiers were trained on an 80/20 stratified split, then the
best baseline was tuned with 5-fold GridSearchCV (scoring = F1). All numbers
below are from an actual run of src/train.py on this repo's code - not
copied from elsewhere.
| Model | Accuracy | Precision | Recall | F1 | ROC-AUC |
|---|---|---|---|---|---|
| Random Forest | 0.8862 | 0.8989 | 0.9412 | 0.9195 | 0.8475 |
| Logistic Regression | 0.8618 | 0.8400 | 0.9882 | 0.9081 | 0.8508 |
| SVM (RBF) | 0.8537 | 0.8317 | 0.9882 | 0.9032 | 0.8517 |
| Naive Bayes | 0.8455 | 0.8367 | 0.9647 | 0.8962 | 0.8387 |
| KNN (K=7) | 0.8293 | 0.8200 | 0.9647 | 0.8865 | 0.7364 |
| Gradient Boosting | 0.8293 | 0.8478 | 0.9176 | 0.8814 | 0.8207 |
| Decision Tree | 0.7154 | 0.8205 | 0.7529 | 0.7853 | 0.6923 |
| Random Forest (Tuned, deployed) | 0.8618 | 0.8469 | 0.9765 | 0.9071 | 0.8635 |
Why the tuned model is deployed even though it isn't the single highest F1
on this test split: GridSearchCV selects parameters by cross-validated F1
on the training folds, not by test-set F1 - that's the honest way to tune
without leaking test data. The tuned Random Forest (max_depth=4, n_estimators=200) trades a hair of test-set F1 for a better ROC-AUC and a
shallower, less overfit tree, which generalizes better in practice. Both
numbers are reported here deliberately, instead of only showing the flattering
one.
Credit_Historydominates (44% of Random Forest's feature importance on its own): ~79.6% approval rate when clean vs. ~7.9% when adverse. This matches how loan committees actually behave - almost everything else is a tiebreaker.IncomeToLoanRatioandBalanceIncome(engineered, not raw) rank second and third in importance - evidence that the affordability framing adds real signal beyond the raw income/loan columns.- Semiurban applicants have the highest approval rate (76.8%) - higher than Urban (65.8%) or Rural (61.5%), likely reflecting a lender sweet spot between rural income volatility and urban competition/risk.
- Raw income is only mildly predictive on its own - approved applicants'
raw incomes aren't meaningfully higher than denied applicants', which is
exactly why
BalanceIncome(income after obligations) andIncomeToLoanRatioare more informative than income alone.
src/explain.py generates a feature-importance chart (models/feature_importance.png)
so every prediction can be traced back to why - critical for any
credit-decisioning tool that affects real people's access to a loan.
loan-approval-predictor/
├── app.py # Streamlit demo app
├── data/
│ └── loan.csv # Raw dataset (614 rows)
├── models/ # Generated: trained pipeline, results, chart
├── notebooks/
│ └── 01_eda.ipynb # Exploratory data analysis
├── src/
│ ├── data_prep.py # Cleaning + feature engineering
│ ├── train.py # Model comparison, tuning, persistence
│ ├── predict.py # CLI single-applicant inference
│ └── explain.py # Feature importance chart
├── tests/
│ └── test_pipeline.py # Unit tests (data prep + pipeline shape)
├── .github/workflows/ci.yml # CI: tests + training smoke test on every push
├── requirements.txt
├── LICENSE
└── README.md
git clone https://github.com/<your-username>/loan-approval-predictor.git
cd loan-approval-predictor
pip install -r requirements.txt
# Train the model (reproduces the results table above)
python -m src.train
# Generate the explainability chart
python -m src.explain
# Predict for a single applicant via CLI
python -m src.predict --applicant_income 4500 --coapplicant_income 1500 \
--loan_amount 120 --credit_history 1 --property_area Semiurban \
--married Yes --dependents 1
# Run the interactive demo app
streamlit run app.pyRun the test suite:
pytest tests/ -vpandas, numpy, scikit-learn, matplotlib/seaborn, joblib, Streamlit, pytest, GitHub Actions.
- Trained on a small (614-row), single-lender, home-loan dataset - not
production-ready credit scoring, and not validated for fairness/bias across
protected attributes (an important next step before any real deployment,
especially given
GenderandMarriedare in the feature set). - Next steps: SHAP-based per-prediction explanations (not just global feature importance), fairness auditing across demographic slices, and a synthetic extension modeling group-liability cooperative loans (where a member's repayment is influenced by their savings group's track record) - a closer analogue to Ajo/Esusu/SACCO-style lending than an individual home loan.
Dataset: Dream Housing Finance / Analytics Vidhya loan prediction problem (public, via Kaggle). Project structure was originally inspired by the Beginner-Data-Science-Projects collection by Taimour Karim (MIT licensed); this repository's feature engineering, model pipeline, explainability tooling, CLI, app, tests, and CI are an original build on top of that starting point.
MIT - see LICENSE.