110 lines
3.5 KiB
Python
110 lines
3.5 KiB
Python
|
|
"""Halal meat sales opportunity scoring for export market entities."""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import re
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
# Land-basis: vraag naar halal vlees / moslim markt (0–100 proxy)
|
|||
|
|
COUNTRY_HALAL_MARKET: dict[str, float] = {
|
|||
|
|
"SA": 98, "AE": 97, "QA": 97, "KW": 96, "BH": 96, "OM": 95,
|
|||
|
|
"EG": 92, "MA": 90, "DZ": 90, "TN": 89, "LY": 88, "SD": 91, "SO": 93,
|
|||
|
|
"TR": 85, "ID": 94, "MY": 93, "PK": 95, "BD": 94, "AF": 96, "IR": 94,
|
|||
|
|
"IQ": 93, "JO": 90, "LB": 88, "SY": 89, "YE": 95, "PS": 91,
|
|||
|
|
"NG": 82, "SN": 92, "ML": 93, "NE": 95, "BF": 90, "CI": 85, "GH": 78,
|
|||
|
|
"KE": 72, "TZ": 78, "UG": 70, "ET": 68, "ZA": 55, "GB": 52, "FR": 54,
|
|||
|
|
"DE": 50, "NL": 58, "BE": 56, "SE": 42, "NO": 38, "DK": 40, "FI": 35,
|
|||
|
|
"AT": 48, "CH": 45, "IT": 46, "ES": 48, "PT": 42, "GR": 45, "CY": 55,
|
|||
|
|
"BA": 82, "AL": 85, "XK": 88, "MK": 75, "RS": 72, "BG": 58, "RO": 55,
|
|||
|
|
"US": 45, "CA": 48, "AU": 38, "NZ": 35, "MX": 40, "BR": 42, "AR": 38,
|
|||
|
|
"SG": 72, "TH": 55, "PH": 65, "IN": 70, "CN": 35, "JP": 30, "KR": 32,
|
|||
|
|
"RU": 48, "KZ": 62, "UZ": 75, "AZ": 80, "GE": 55,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TYPE_BASE: dict[str, float] = {
|
|||
|
|
"butcher": 92,
|
|||
|
|
"doner_shoarma": 90,
|
|||
|
|
"wholesaler": 86,
|
|||
|
|
"distributor": 85,
|
|||
|
|
"importer": 84,
|
|||
|
|
"contract_caterer": 78,
|
|||
|
|
"restaurant": 72,
|
|||
|
|
"foodservice": 65,
|
|||
|
|
"logistics": 45,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
HALAL_KEYWORDS = re.compile(
|
|||
|
|
r"halal|helal|kebab|döner|doner|shoarma|shawarma|moslim|muslim|islamic|"
|
|||
|
|
r"middle\s*east|turkish|türk|arab|pakistani|moroccan|marok|syrian|lebanese|"
|
|||
|
|
r"peri\s*peri|grill|bbq|meat|vlees|slager|butcher|wholesale|groothandel|"
|
|||
|
|
r"cash\s*&\s*carry|horeca|ethnic|kosher",
|
|||
|
|
re.I,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
EXPLICIT_HALAL = re.compile(r"\bhalal\b|\bhelal\b", re.I)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _product_interest_text(val: Any) -> str:
|
|||
|
|
if not val:
|
|||
|
|
return ""
|
|||
|
|
if isinstance(val, str):
|
|||
|
|
try:
|
|||
|
|
val = json.loads(val)
|
|||
|
|
except json.JSONDecodeError:
|
|||
|
|
return val
|
|||
|
|
if isinstance(val, list):
|
|||
|
|
return " ".join(str(x) for x in val)
|
|||
|
|
return str(val)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def score_entity(row: dict[str, Any]) -> tuple[float, list[str]]:
|
|||
|
|
"""Return (score 0–100, reason tags)."""
|
|||
|
|
reasons: list[str] = []
|
|||
|
|
country = (row.get("country_iso2") or "").upper()
|
|||
|
|
etype = row.get("entity_type") or ""
|
|||
|
|
name = row.get("name") or ""
|
|||
|
|
notes = row.get("halal_cert_notes") or ""
|
|||
|
|
interest = _product_interest_text(row.get("product_interest"))
|
|||
|
|
|
|||
|
|
score = TYPE_BASE.get(etype, 50)
|
|||
|
|
reasons.append(f"type:{etype}")
|
|||
|
|
|
|||
|
|
market = COUNTRY_HALAL_MARKET.get(country, 40)
|
|||
|
|
score = score * 0.55 + market * 0.45
|
|||
|
|
if market >= 80:
|
|||
|
|
reasons.append("sterke_halal_markt")
|
|||
|
|
|
|||
|
|
blob = f"{name} {notes} {interest}".lower()
|
|||
|
|
if EXPLICIT_HALAL.search(blob):
|
|||
|
|
score += 18
|
|||
|
|
reasons.append("halal_in_naam")
|
|||
|
|
elif HALAL_KEYWORDS.search(blob):
|
|||
|
|
score += 10
|
|||
|
|
reasons.append("halal_signaal")
|
|||
|
|
|
|||
|
|
if etype in ("butcher", "doner_shoarma"):
|
|||
|
|
reasons.append("direct_vlees_kanaal")
|
|||
|
|
elif etype in ("wholesaler", "distributor", "importer"):
|
|||
|
|
reasons.append("B2B_kanaal")
|
|||
|
|
|
|||
|
|
conf = int(row.get("confidence") or 50)
|
|||
|
|
score += (conf - 50) * 0.08
|
|||
|
|
|
|||
|
|
if row.get("email") or row.get("contact_email"):
|
|||
|
|
score += 3
|
|||
|
|
if int(row.get("contact_count") or 0) > 0:
|
|||
|
|
score += 4
|
|||
|
|
reasons.append("heeft_contact")
|
|||
|
|
|
|||
|
|
return round(min(100, max(0, score)), 1), reasons
|
|||
|
|
|
|||
|
|
|
|||
|
|
def halal_pin_tier(score: float) -> str:
|
|||
|
|
if score >= 75:
|
|||
|
|
return "hot"
|
|||
|
|
if score >= 55:
|
|||
|
|
return "warm"
|
|||
|
|
if score >= 40:
|
|||
|
|
return "mild"
|
|||
|
|
return "low"
|