Last checked
Group the ledger by member to get three features: share of months paid, average days late and misses in the last three months. Combine them into a points score you can explain, then fit a logistic regression and compare. Get members' consent first and never let the score decide alone.
McTaba runs coding and data bootcamps in Kenya. This guide is written to be useful whether or not you ever join one.
A chama treasurer already holds a credit file: a notebook or a sheet showing who paid how much, in which month and how many days after the deadline. Before a loan from the pot, the group asks "will she repay?" This tutorial turns that gut feeling into numbers. It is one of the projects on our data science career map.
Credit scoring is also a capstone option in our Data Science to AI Engineering programme, so the method is worth learning properly.
Imagine a ledger for 60 pretend members over 12 months at KES 2,000 a month. Code generates it, so it is made up, and so is the loan outcome later. Real chama data needs the members' agreement, which we cover at the end.
import numpy as np
import pandas as pd
rng = np.random.default_rng(7)
rows = []
for i in range(1, 61):
m = f"M{i:02d}"
reliability = rng.uniform(0.3, 1.0)
for month in range(1, 13):
if rng.random() < reliability:
late = int(rng.poisson((1 - reliability) * 8))
rows.append((m, month, 2000, late))
else:
rows.append((m, month, 0, 0))
pay = pd.DataFrame(
rows, columns=["member", "month", "paid", "days_late"])
| Feature | What it shows |
|---|---|
| paid_share | Share of months with a payment |
| avg_days_late | How late she is when she does pay |
| missed_last3 | Misses in the last three months, a sign of trouble now |
Our Data Science to AI Engineering programme teaches this feature-building step in its machine learning phase.
g = pay.groupby("member")
feat = pd.DataFrame({
"paid_share": g["paid"].apply(lambda s: (s > 0).mean()),
"avg_days_late": pay[pay.paid > 0]
.groupby("member")["days_late"].mean(),
"missed_last3": pay[pay.month > 9]
.groupby("member")["paid"].apply(lambda s: (s == 0).sum()),
}).fillna(0)
The fillna(0) handles a member who never paid and so has no lateness average. That choice flatters her on lateness, so the points score below leans mostly on paid_share.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
feat["score"] = (
60 * feat["paid_share"]
+ 25 * (1 - feat["avg_days_late"].clip(0, 10) / 10)
+ 15 * (1 - feat["missed_last3"] / 3)
).round()
print(feat.sort_values("score").head())
The score runs from 0 to 100. Sixty points reward paying on most months, 25 reward being on time, and 15 reward a clean last quarter. The weights are our judgement, not a law. Change them and see who moves.
Its strength is that a member can read it. "You lost 12 points because you missed two of the last three months" is an answer you can say aloud at a meeting. Start every scoring project here, because a model has to beat this baseline to earn its place.
A model needs a past outcome to learn from: did the member later fail to repay a loan? Here we make that outcome up, tied loosely to paid_share, so you can run the code. With a real chama you would take it from loan records.
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
p = 1 / (1 + np.exp(6 * (feat["paid_share"] - 0.55)))
feat["defaulted"] = (rng.random(len(feat)) < p).astype(int)
X = feat[["paid_share", "avg_days_late", "missed_last3"]]
y = feat["defaulted"]
Xtr, Xte, ytr, yte = train_test_split(
X, y, test_size=0.3, random_state=1, stratify=y)
model = LogisticRegression(max_iter=1000).fit(Xtr, ytr)
print(model.coef_[0].round(2).tolist())
print(roc_auc_score(yte, model.predict_proba(Xte)[:, 1]))
A negative coefficient on paid_share says more months paid means lower default risk. roc_auc_score says how well the model ranks risky members above safe ones: 0.5 is a coin toss, 1.0 is perfect.
Sixty members is a tiny sample, so the test score swings with the random split. Treat it as a way to learn the steps. A real chama with a few dozen members cannot support a confident model, which is a finding too. For the metrics side of imbalance, see catching mobile money fraud with machine learning.
A score about a person is personal data. Kenya's Data Protection Act 2019 is enforced by the Office of the Data Protection Commissioner (checked on 3 November 2026). Before you score real members, do these five things.
To go further, try the same steps on your own records from your M-Pesa statement. If your chama or SACCO wants a scoring tool built, or you want to learn this with mentors, see our Data Science to AI Engineering programme, starting 11 January 2027.
KES 140,000
Data Science to AI Engineering: 5 months full time or 8 months part time, hybrid in Nairobi. Fraud, credit and M-Pesa data from Week 1, and every model deployed. Starts 11 January 2027.
See the Data Science programme