Sepideh Zamani — Utrecht, Netherlands
Three analyses on small tabular business datasets. Each reaches a negative or corrected result, and each is written that way deliberately.
A portfolio notebook reporting 85% accuracy on a clean dataset shows that its author can call
.fit(). These show something I think matters more in practice: noticing when a result is
wrong, tracing it back to its cause in the data, and reporting what the evidence actually
supports.
01_churn_prediction/notebooks/churn_analysis.ipynb
Built a churn classifier on 300 telecom records. Logistic regression reached ROC-AUC 0.559 against a 0.500 baseline — an apparent improvement that turned out not to be real.
Method: cross-validated pipeline (ColumnTransformer + StratifiedKFold, preprocessing fitted
inside each fold), majority-class baseline, ROC curves on out-of-fold predictions, and a 200-run
permutation test.
Finding: the score falls inside the distribution the same pipeline produces on randomly shuffled targets (p = 0.129). A data-quality audit explains why:
- Service dependencies guaranteed by the schema are violated on nearly every row, in both directions — customers with no phone service carrying multi-line details, customers with no internet carrying active internet add-ons.
TotalChargesequalsMonthlyCharges × tenureexactly on all 300 rows, so it carries no independent information.- 54% of customers are flagged as senior citizens, against ~16% in the reference population.
- Contract type, normally the strongest churn predictor, spans under 6 percentage points.
The columns were generated independently. No amount of tuning would produce a model that generalises, and reporting a tuned figure would have been misleading.
02_customer_segmentation/notebooks/segmentation_analysis.ipynb
An earlier version of this analysis produced a compelling result: a large segment of high-income customers who browse extensively and never purchase, and an obvious recommendation to target them for conversion.
The segment did not exist.
ast.literal_eval returned a tuple for 33 of 50 rows and a dict for one more, because the
source export omitted the enclosing brackets on those rows. A downstream isinstance(x, list)
check then assigned all 34 zero purchases — silently, with no exception raised and no value
missing. K-means grouped them together, and the artifact was interpreted as a customer behaviour.
| Buggy | Corrected | |
|---|---|---|
| Mean purchases per record | 0.68 | 2.22 |
| Records at zero purchases | 34 of 50 | 0 of 50 |
| corr(time on site, purchases) | −0.445 | +0.344 |
Fixing the parser removes the segment and reverses the sign of the headline relationship.
The notebook also uses silhouette scores rather than eyeballing an elbow curve, and reports a detail worth noting: the buggy clustering scored higher (0.475) than the corrected one (0.368), because 34 identical artificial zeros are trivially separable. A quality metric improving is not evidence that the data is right.
03_sql_data_quality/notebooks/sql_audit.ipynb
The auditing half of project 1, done the way it would run against a real source system rather than in pandas. The flat extract is normalised back into the customer, service and billing tables it plausibly came from, so the checks have to join across them.
Uses joins, CTEs, CASE aggregation, HAVING, and window functions (NTILE, RANK, AVG OVER)
to isolate dependency violations in 295 of 300 records, confirm total_charges is fully derived on
every row, and show that churn rate carries no monotonic relationship to tenure decile or contract
type. Same verdict as the Python analysis, reached independently.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
jupyter labEach notebook runs top to bottom from a fresh kernel with no manual steps. Data paths are relative, so the notebooks work from a clone without editing.
Both are small teaching datasets, and the point here is not to criticise their origin. They are what I had, and working with flawed data honestly seemed more useful than swapping in something clean. The checks demonstrated — dependency audits, derived-feature detection, permutation testing, parser-output validation — are the ones I would run before trusting any model, at any scale.