If you scrape or export local business listings (from Google Maps, a directory, or your own CRM), you quickly end up with hundreds of rows and no idea where to start. This tutorial builds a small, transparent scoring function in Python that ranks businesses by likely demand and flags the ones with no website, so the top of your list is where the easy conversations are.
No external services are needed. Everything below runs on the standard library plus csv.
The input
Assume a CSV with one row per business:
name,category,city,reviews,rating,website Amit's Gym,gym,Kolkata,847,4.7, SAS FITNESS,gym,Kolkata,1403,4.6, Kross9 Gym Kasba,gym,Kolkata,551,4.8, Big Chain Fitness,gym,Kolkata,2300,4.2,https://example.com Corner Studio,gym,Kolkata,12,5.0, website is empty when the listing has no site. Ratings near 5.0 with very few reviews are common and not very informative, so we want the score to trust review volume more than the raw rating.
Step 1: a demand score that does not over-reward huge numbers
Review counts are heavily skewed. A business with 1,400 reviews is not 100 times more interesting than one with 14. A log scale keeps big numbers from dominating:
import math def demand_score(reviews: int, rating: float) -> float: """0-100. Log-scaled review volume, lightly adjusted by rating.""" volume = min(math.log10(reviews + 1) / 3.0, 1.0) # 1,000 reviews ~ 1.0 quality = max(min((rating - 3.5) / 1.5, 1.0), 0.0) # 3.5 -> 0, 5.0 -> 1 return round(100 * (0.8 * volume + 0.2 * quality), 1) Volume carries 80% of the weight because it reflects real footfall. Rating only nudges the result.
Step 2: flag the gap
For anyone selling websites or digital services, a business with strong demand and no website is the best lead. Add a gap flag and a combined priority:
def priority(row: dict) -> float: score = demand_score(int(row["reviews"]), float(row["rating"] or 0)) has_site = bool(row["website"].strip()) return score if not has_site else score * 0.3 Businesses that already have a site drop to the bottom instead of disappearing. They can still be leads for redesigns.
Step 3: rank and export
import csv def rank(path_in: str, path_out: str, top: int = 20) -> None: with open(path_in, newline="", encoding="utf-8") as f: rows = list(csv.DictReader(f)) for r in rows: r["priority"] = priority(r) r["gap"] = "no website" if not r["website"].strip() else "" rows.sort(key=lambda r: r["priority"], reverse=True) with open(path_out, "w", newline="", encoding="utf-8") as f: w = csv.DictWriter(f, fieldnames=list(rows[0].keys())) w.writeheader() w.writerows(rows[:top]) rank("listings.csv", "ranked.csv") With the sample input, the order comes out as SAS FITNESS, Amit's Gym, Kross9 Gym Kasba, then Corner Studio (few reviews), with Big Chain Fitness last because it already has a website.
Step 4: sanity-check the weights
Before trusting any score, eyeball the extremes:
- Does a 5.0 rating with 3 reviews outrank a 4.5 with 600? It should not.
- Does every row in the top 10 have a reason you could say out loud to the owner?
- Does the score change sensibly if you double one business's reviews?
If any answer is "no", adjust the 0.8 / 0.2 split or the log divisor. Keep the formula simple enough to explain; that is the point.
What the output looks like in practice
The same idea, applied to live listings, produces lists like this one of gyms with no website in Kolkata, where review counts range from about 370 to 1,400 per gym. For turning a ranked list into conversations, this guide on selling websites to gyms covers the pitch.
Wrap-up
- Log-scale review counts so large businesses do not drown everything else.
- Treat rating as a light adjustment, not the main signal.
- Down-weight, rather than delete, businesses that already have a website.
- Keep the formula explainable.
Disclosure: I work on Mantis, which builds lead lists like the one above.
Originally published on Mantis Leads Blog.
