An open-data study of the Safia bakery network in Tashkent, prepared by Azizbek Sunnatov as part of an application for the ML Researcher role in Safia’s AI team (September 2026). It uses only public data: no internal Safia data was used.
Deliverables
| One-page report | Uzbek · Russian |
| Interactive map (uz/ru) | Open in the browser (source: outputs/maps/safia_whitespace.html) |
| Findings with all numbers | docs/findings.md |

Safia site (JSON-LD, 172 branches) ─┐
Cake Lab site, OSM competitors ─────┤
OpenStreetMap POIs, land use ───────┼─► H3 grid (res 8, ~0.88 km² in Tashkent, 493 cells, 12 districts)
Official district population 2023 ─┘ │
▼
features: cafes, schools, bus stops, supermarkets, universities, offices,
apartments, metro, malls (cell + neighbours), district density,
distance to centre, non-residential share (25 features)
│
▼
target: "cell has a Safia branch" → logistic regression / LightGBM
20 × spatial GroupKFold over H3 res-6 blocks, 2-ring buffer → mean out-of-fold score per cell
│
▼
candidates: no Safia within 1 km, ≥ 1.5 km apart, reasons from SHAP (per concept),
checked on satellite imagery (7 checked, 1 rejected: cropland)
Model results. Validation is spatial: blocks of about 36 km² are held out together, and training cells within 2 rings (~2 km) of the test block are removed. This matters because features use each cell’s neighbours, so nearby cells share information. The assignment of blocks to folds is repeated 20 times and averaged. 107 of 493 cells have a branch, so a random model scores 0.217 PR-AUC.
| Model | ROC-AUC | PR-AUC |
|---|---|---|
| Logistic regression, no competitor feature (chosen) | 0.865 ± 0.006 | 0.589 ± 0.015 |
| Logistic regression + competitor feature | 0.864 ± 0.007 | 0.589 ± 0.017 |
| LightGBM, no competitor feature | 0.855 ± 0.005 | 0.595 ± 0.015 |
| LightGBM + competitor feature | 0.855 ± 0.004 | 0.596 ± 0.010 |
(± is the spread over the 20 repeats.) The rule is to pick the simplest model within 0.01 PR-AUC of the best. LightGBM is not better: its PR-AUC is +0.005 higher, which is within noise, and its ROC-AUC is lower. The competitor flag adds nothing, and that data is incomplete anyway.
cafe category contained 3 Safia and 23 competitor cafés. They are removed from the
features.data/interim/candidate_checks.csv, and rejected cells are
excluded.Requires Python 3.13. Everything runs from the data committed in data/raw/, so no network access is needed.
git clone https://github.com/ibnMuxiddin/safia-location-study.git
cd safia-location-study
python -m venv .venv
.venv/Scripts/python -m pip install -r requirements.lock.txt # Windows (Linux/macOS: .venv/bin/python)
.venv/Scripts/python src/run_all.py # steps 01 → 12, about 2 minutes
Each step is one script, src/NN_name.py. Scripts that download data (02, 03, 04, 05, 06, 06b) only do so when the
cached file is missing or --refresh is passed. The raw HTML of the Safia and Cake Lab pages is not redistributed.
Steps 02 and 05 use the extracted branch tables in data/interim/safia_branches.csv and
data/raw/cakelab_branches.csv instead. --refresh downloads today’s pages, so results may then differ.
src/ one script per step, config.py, osm_utils.py, map_utils.py, run_all.py
data/raw/ source data as downloaded (Safia/Cake Lab pages, OSM extracts, boundaries)
data/interim/ cleaned tables (branches, competitors, official population, manual checks)
data/processed/ features, model outputs, top-10, coverage list, facts.json
outputs/ maps and figures reports/ one-page PDFs docs/ findings