City-level ad performance scorer for quick commerce marketers.
Score cities by ROAS, CPA, conversion rate, and market size. Get a ranked list with recommended budget splits. Stop allocating ad spend by gut feel.
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ City Ad Performance Rankings │
├──┬──────────────┬───────┬────────┬────────────┬────────┬─────────┬───────┬─────────────┤
│ #│ City │ Score │ Status │ Quality │ ROAS │ CPA(₹) │ Conv% │ Budget% │
├──┼──────────────┼───────┼────────┼────────────┼────────┼─────────┼───────┼─────────────┤
│ 1│ Mumbai │ 82.4 │ SCALE │ SUFFICIENT │ 4.20x │ ₹95 │ 2.02% │ 18.3% │
│ 2│ Bengaluru │ 74.1 │ SCALE │ SUFFICIENT │ 3.80x │ ₹110 │ 1.77% │ 16.5% │
│ 3│ Hyderabad │ 68.9 │ HOLD │ SUFFICIENT │ 3.50x │ ₹125 │ 1.82% │ 15.3% │
│ 4│ Delhi │ 61.2 │ HOLD │ SUFFICIENT │ 3.10x │ ₹145 │ 1.54% │ 13.6% │
│ 5│ Pune │ 54.7 │ HOLD │ SUFFICIENT │ 2.90x │ ₹160 │ 1.91% │ 12.2% │
│ 6│ Chennai │ 43.8 │ REDUCE │ SUFFICIENT │ 2.70x │ ₹175 │ 1.58% │ 9.7% │
│ 7│ Ahmedabad │ 37.1 │ REDUCE │ SUFFICIENT │ 2.40x │ ₹190 │ 1.61% │ 8.3% │
│ 8│ Jaipur │ 28.6 │ LOW │ SUFFICIENT │ 1.90x │ ₹240 │ 1.32% │ 6.1% │
└──┴──────────────┴───────┴────────┴────────────┴────────┴─────────┴───────┴─────────────┘
Quick commerce performance varies dramatically by city — demand density, dark store coverage, and user adoption differ 5-10x between Mumbai and a Tier-2 city. But most qcomm marketers allocate budgets platform-first (Meta vs Google), not city-first. The result: high-performing cities are under-invested, low-performing cities eat budget.
This tool is the missing layer between knowing city-first allocation is correct and actually doing it.
pip install spendmapRequires Python 3.9+.
Step 1: Export your ad data to CSV
Export from Meta Ads Manager (or any ad platform) with city-level breakdown. Save as a CSV with these columns:
city, roas, cpa, conversions, spend, clicks
| Column | Description | Example |
|---|---|---|
city |
City name (must be consistent across rows) | Mumbai |
roas |
Return on ad spend | 4.2 |
cpa |
Cost per acquisition in INR (Indian Rupee) | 95 |
conversions |
Total conversions in the date window | 850 |
spend |
Total spend in the date window (INR) | 380500 |
clicks |
Total clicks (used to derive conv rate) | 42000 |
Your column names don't have to match exactly — rename them before running:
# Example: your export has "Cost per Result" instead of "cpa"
# Rename the column in your CSV before passing to spendmapStep 2: Run
# Terminal table (default)
spendmap --input city_data.csv
# Save to CSV
spendmap --input city_data.csv --format csv --output report.csv
# Save to JSON (machine-readable)
spendmap --input city_data.csv --format json --output report.json
# With budget allocation (total budget in INR)
spendmap --input city_data.csv --total-budget 5000000
# Per-signal breakdown (for debugging / trust-building)
spendmap --input city_data.csv --verbose
# State-level (if city data is unavailable)
spendmap --input state_data.csv --granularity stateEach city gets a score from 0 to 100. Higher is better.
Signals and weights (configurable via config.yaml):
| Signal | Weight | Direction | Notes |
|---|---|---|---|
| ROAS | 35% | Higher is better | Return on ad spend |
| CPA | 30% | Lower is better | Inverted in scoring so lower CPA → higher score |
| Conversion rate | 20% | Higher is better | Conversions / clicks |
| Spend vs TAM share | 15% | Match is better | How well your spend share matches the city's Total Addressable Market size |
All signals are min-max normalised across all cities in your dataset before weighting. This means scores are relative to your data, not absolute benchmarks.
Tier multiplier (applied after weighted sum):
| City tier | Multiplier | Cities |
|---|---|---|
| Metro | 1.00 | Mumbai, Delhi, Bengaluru, Hyderabad, etc |
| T1 | 0.95 | Pune, Surat, Jaipur, Lucknow, etc |
| T2 | 0.90 | Dehradun, Kochi, Coimbatore, etc |
| T3 | 0.85 | All other cities |
Multipliers are arbitrary starting defaults. Override in config.yaml.
Status thresholds:
| Status | Score range | Recommendation |
|---|---|---|
| SCALE | ≥ 70 | Increase budget |
| HOLD | 50–69 | Maintain current budget |
| REDUCE | 30–49 | Reduce budget, monitor |
| LOW | < 30 | Pause or move budget to SCALE cities |
Data quality flags (independent of status):
| Flag | Conversions | Meaning |
|---|---|---|
| SUFFICIENT | ≥ 100 | Reliable signal |
| LOW_SIGNAL | 10–99 | Included in ranking, but interpret with caution |
| INSUFFICIENT | < 10 | Excluded from ranking, listed separately |
If a city has LOW_SIGNAL and LOW status, the tool warns you to run a test budget before reducing — there's not enough data to confirm the city is actually low-performing.
Budget share = city_score / sum(all_city_scores) × 100%
Pass --total-budget in INR to see absolute amounts:
spendmap --input data.csv --total-budget 5000000Known v1 limitation: single total budget input. Multi-campaign / multi-objective allocation (e.g., split by funnel stage) is not yet supported.
If your ad platform only reports state-level data (common when pixel/CAPI — Conversions
API — is not configured for city-level signals), use --granularity state:
spendmap --input state_data.csv --granularity stateState-level CSV uses state instead of city as the geo column. All other columns
are the same. Tier multipliers are not applied in state mode.
Create config.yaml in your working directory to override defaults:
scoring:
weights:
roas: 0.40 # Increase ROAS weight
cpa: 0.30
conversion_rate: 0.20
tam_share: 0.10 # Reduce TAM weight
tier_multipliers:
Metro: 1.00
T1: 0.97 # Custom T1 multiplier
T2: 0.93
T3: 0.88
status_thresholds:
SCALE: 75 # Raise the SCALE bar
HOLD: 55
REDUCE: 35git clone https://github.com/your-org/spendmap
cd spendmap
pip install -e ".[dev]"
pytestBefore trusting the rankings for budget decisions, run the model against 90 days of
your actual ad data and check whether the top 5 cities match your team's manual
ranking from experience. If correlation is < 0.7, adjust signal weights in
config.yaml before acting on the output.
| Feature | Version | Status |
|---|---|---|
| CSV-in scoring (this) | v1.0 | Shipped |
| Meta Ads API connector | v1.1 | Planned |
| Google Ads API connector | v1.1 | Planned |
| Movement tracking (--compare) | v1.1 | Planned |
| Slack webhook integration | v1.1 | Planned |
| Multi-platform score fusion | v2 | Planned |
| Real-time reallocation | v2 | Planned |
| Term | Meaning |
|---|---|
| ROAS | Return on Ad Spend — revenue divided by ad spend |
| CPA | Cost per Acquisition — spend divided by conversions |
| TAM | Total Addressable Market — the estimated market size for a city, used to gauge spend share |
| INR | Indian Rupee (₹) — all monetary values are in INR |
| CAPI | Conversions API — server-side event integration for ad platforms |
| CSV | Comma-Separated Values — the input/output file format used by this tool |
| JSON | JavaScript Object Notation — machine-readable output format (--format json) |
| qcomm | Quick commerce — same-day/rapid delivery e-commerce |
Apache 2.0