Mobile_Reccomendation / src /scoring_engine.py
smhs16's picture
Upload 8 files
f651dd8 verified
"""
PriceOye Phone Recommendation AI
=================================
Scoring Engine β€” Professional ML Benchmark System
Benchmarks (hidden from user, used internally only):
- Android: Samsung Galaxy S26 Ultra
- iOS: iPhone 17 Pro Max
Each phone is scored across 10 categories Γ— multiple sub-dimensions.
Final score = weighted sum of category averages per user priority profile.
"""
from dataclasses import dataclass, field
from typing import Optional
import math
# ─────────────────────────────────────────────
# DATA STRUCTURES
# ─────────────────────────────────────────────
@dataclass
class SubDimension:
score: float # 0.0 – 10.0
note: str # Human-readable justification
@dataclass
class PhoneDimensions:
"""All benchmark sub-dimensions for a phone."""
# Camera (9 sub-dims)
camera_main_sensor: SubDimension
camera_aperture: SubDimension
camera_optical_zoom: SubDimension
camera_ultrawide: SubDimension
camera_video: SubDimension
camera_night_mode: SubDimension
camera_front: SubDimension
camera_lens_quality: SubDimension
camera_ois: SubDimension
# Performance (5 sub-dims)
perf_cpu: SubDimension
perf_gpu: SubDimension
perf_ram_type: SubDimension
perf_thermal: SubDimension
perf_ai_chip: SubDimension
# Display (6 sub-dims)
disp_resolution: SubDimension
disp_brightness: SubDimension
disp_color_accuracy: SubDimension
disp_refresh_rate: SubDimension
disp_technology: SubDimension
disp_touch_sampling: SubDimension
# Battery (4 sub-dims)
batt_capacity: SubDimension
batt_real_world_sot: SubDimension
batt_efficiency: SubDimension
batt_wireless: SubDimension
# Charging (4 sub-dims)
charg_wired_speed: SubDimension
charg_wireless_speed: SubDimension
charg_reverse: SubDimension
charg_inbox_charger: SubDimension
# RAM (3 sub-dims)
ram_capacity: SubDimension
ram_type: SubDimension
ram_os_management: SubDimension
# Storage (3 sub-dims)
stor_capacity: SubDimension
stor_speed: SubDimension
stor_expandable: SubDimension
# Build (4 sub-dims)
build_frame: SubDimension
build_ip_rating: SubDimension
build_front_glass: SubDimension
build_form_factor: SubDimension
# Software (4 sub-dims)
soft_update_policy: SubDimension
soft_bloatware: SubDimension
soft_ai_features: SubDimension
soft_ecosystem: SubDimension
# Audio (3 sub-dims)
audio_speakers: SubDimension
audio_headphone_jack: SubDimension
audio_bt_codecs: SubDimension
@dataclass
class Phone:
id: str
name: str
brand: str
os: str # 'android' | 'ios'
price_pkr: int
price_label: str
priceoye_url: str
whatmobile_url: str
emoji: str
tags: list[str]
highlights: dict[str, str]
dims: PhoneDimensions
available_on_priceoye: bool = True
@dataclass
class UserPreferences:
budget: Optional[int] = None
os_preference: Optional[str] = None # 'android' | 'ios' | 'any'
priority: Optional[str] = None # see WEIGHT_PROFILES keys
brand_preference: Optional[str] = None # e.g. 'Samsung', 'Apple'
brand_avoid: list[str] = field(default_factory=list)
session_memory: dict = field(default_factory=dict) # cross-turn memory
# ─────────────────────────────────────────────
# CATEGORY AVERAGES
# ─────────────────────────────────────────────
CATEGORY_GROUPS = {
"camera": [
"camera_main_sensor", "camera_aperture", "camera_optical_zoom",
"camera_ultrawide", "camera_video", "camera_night_mode",
"camera_front", "camera_lens_quality", "camera_ois",
],
"performance": [
"perf_cpu", "perf_gpu", "perf_ram_type",
"perf_thermal", "perf_ai_chip",
],
"display": [
"disp_resolution", "disp_brightness", "disp_color_accuracy",
"disp_refresh_rate", "disp_technology", "disp_touch_sampling",
],
"battery": [
"batt_capacity", "batt_real_world_sot",
"batt_efficiency", "batt_wireless",
],
"charging": [
"charg_wired_speed", "charg_wireless_speed",
"charg_reverse", "charg_inbox_charger",
],
"ram": ["ram_capacity", "ram_type", "ram_os_management"],
"storage": ["stor_capacity", "stor_speed", "stor_expandable"],
"build": [
"build_frame", "build_ip_rating",
"build_front_glass", "build_form_factor",
],
"software": [
"soft_update_policy", "soft_bloatware",
"soft_ai_features", "soft_ecosystem",
],
"audio": [
"audio_speakers", "audio_headphone_jack", "audio_bt_codecs",
],
}
def get_category_scores(phone: Phone) -> dict[str, float]:
"""Return average score per category."""
scores = {}
for cat, fields in CATEGORY_GROUPS.items():
vals = [getattr(phone.dims, f).score for f in fields]
scores[cat] = round(sum(vals) / len(vals), 2)
return scores
def get_sub_scores(phone: Phone, category: str) -> list[dict]:
"""Return detailed sub-dimension breakdown for a category."""
fields = CATEGORY_GROUPS.get(category, [])
result = []
for f in fields:
dim: SubDimension = getattr(phone.dims, f)
label = f.replace(f.split("_")[0] + "_", "").replace("_", " ").title()
result.append({
"label": label,
"score": dim.score,
"note": dim.note,
})
return result
# ─────────────────────────────────────────────
# WEIGHT PROFILES
# Must sum to 1.0 per profile
# ─────────────────────────────────────────────
WEIGHT_PROFILES: dict[str, dict[str, float]] = {
"photography": {
"camera": 0.45, "display": 0.15, "storage": 0.10,
"performance": 0.10, "battery": 0.06, "build": 0.05,
"software": 0.04, "charging": 0.02, "ram": 0.02, "audio": 0.01,
},
"gaming": {
"performance": 0.30, "display": 0.20, "ram": 0.15,
"battery": 0.12, "charging": 0.08, "audio": 0.05,
"build": 0.05, "camera": 0.03, "software": 0.01, "storage": 0.01,
},
"battery": {
"battery": 0.30, "charging": 0.25, "performance": 0.15,
"display": 0.10, "ram": 0.07, "camera": 0.06,
"build": 0.04, "software": 0.02, "storage": 0.01, "audio": 0.00,
},
"value": {
"camera": 0.18, "performance": 0.18, "battery": 0.15,
"display": 0.12, "charging": 0.10, "storage": 0.10,
"build": 0.07, "software": 0.05, "ram": 0.03, "audio": 0.02,
},
"business": {
"software": 0.25, "performance": 0.20, "display": 0.15,
"build": 0.12, "camera": 0.10, "battery": 0.08,
"ram": 0.05, "charging": 0.03, "storage": 0.01, "audio": 0.01,
},
"balanced": {
"camera": 0.18, "performance": 0.17, "battery": 0.14,
"display": 0.13, "charging": 0.10, "build": 0.09,
"software": 0.08, "ram": 0.05, "storage": 0.04, "audio": 0.02,
},
"ios": {
"software": 0.25, "camera": 0.22, "performance": 0.18,
"build": 0.12, "display": 0.10, "battery": 0.07,
"ram": 0.03, "charging": 0.02, "storage": 0.01, "audio": 0.00,
},
}
# ─────────────────────────────────────────────
# CORE SCORING FUNCTION
# ─────────────────────────────────────────────
IPHONE_MIN_PRICE = 280000 # Minimum realistic iPhone price in Pakistan (PKR)
def score_phone(
phone: Phone,
prefs: UserPreferences,
phone_db: list["Phone"],
) -> float:
"""
Returns a float score 0–10 for this phone against user preferences.
Returns -1 if the phone is ineligible.
"""
if not phone.available_on_priceoye:
return -1.0
# OS filter
if prefs.os_preference == "ios" and phone.os != "ios":
return -1.0
if prefs.os_preference == "android" and phone.os != "android":
return -1.0
# Budget filter β€” auto-exclude iPhones if budget too low
if prefs.budget:
if phone.os == "ios" and prefs.budget < IPHONE_MIN_PRICE:
return -1.0
if phone.price_pkr > prefs.budget * 1.12: # 12% tolerance
return -1.0
# Brand preference boost
brand_boost = 0.0
if prefs.brand_preference and phone.brand.lower() == prefs.brand_preference.lower():
brand_boost = 0.3
if phone.brand in prefs.brand_avoid:
return -1.0
# Get weights
weights = WEIGHT_PROFILES.get(prefs.priority or "balanced", WEIGHT_PROFILES["balanced"])
# Compute weighted score
cat_scores = get_category_scores(phone)
total = sum(cat_scores[cat] * w for cat, w in weights.items())
# Savings bonus (mild β€” specs should dominate)
if prefs.budget and prefs.budget > 0:
savings_ratio = max(0.0, 1.0 - phone.price_pkr / prefs.budget)
total += savings_ratio * 0.15
return round(min(10.0, total + brand_boost), 2)
def recommend(
prefs: UserPreferences,
phone_db: list["Phone"],
count: int = 1,
) -> list[tuple["Phone", float]]:
"""
Returns top-N phones scored for the given preferences.
Each item is (phone, final_score).
"""
scored = []
for phone in phone_db:
s = score_phone(phone, prefs, phone_db)
if s > 0:
scored.append((phone, s))
scored.sort(key=lambda x: x[1], reverse=True)
return scored[:count]
def find_closest_alternative(
target_phone_name: str,
prefs: UserPreferences,
phone_db: list["Phone"],
) -> Optional["Phone"]:
"""
If a benchmarked phone is not on PriceOye, find the closest
available alternative based on category score similarity.
"""
# Try to find target in DB
target = next(
(p for p in phone_db if target_phone_name.lower() in p.name.lower()), None
)
if target and target.available_on_priceoye:
return target
if not target:
# Fall back to top recommendation
results = recommend(prefs, phone_db, count=1)
return results[0][0] if results else None
# Find most similar by category scores
target_cats = get_category_scores(target)
best_match = None
best_dist = float("inf")
for phone in phone_db:
if not phone.available_on_priceoye:
continue
if phone.id == target.id:
continue
if prefs.os_preference and phone.os != prefs.os_preference:
continue
phone_cats = get_category_scores(phone)
dist = math.sqrt(
sum((target_cats[c] - phone_cats[c]) ** 2 for c in target_cats)
)
if dist < best_dist:
best_dist = dist
best_match = phone
return best_match
# ─────────────────────────────────────────────
# PRIORITY REASON GENERATOR
# ─────────────────────────────────────────────
def get_priority_reason(phone: Phone, priority: str) -> str:
cats = get_category_scores(phone)
reasons = {
"photography": (
f"Camera score {cats['camera']:.1f}/10 β€” "
f"{phone.dims.camera_lens_quality.note}"
),
"gaming": (
f"Gaming performance {cats['performance']:.1f}/10 β€” "
f"{phone.dims.perf_cpu.note}"
),
"battery": (
f"Battery {cats['battery']:.1f}/10 Β· "
f"Charging {cats['charging']:.1f}/10 β€” "
f"{phone.dims.batt_real_world_sot.note}"
),
"value": (
f"{phone.price_label} mein best value β€” "
f"Camera {cats['camera']:.1f} Β· Performance {cats['performance']:.1f}"
),
"business": (
f"Software {cats['software']:.1f}/10 Β· "
f"Build {cats['build']:.1f}/10 β€” "
f"{phone.dims.soft_update_policy.note}"
),
"balanced": (
f"Koi bhi weak point nahi β€” "
f"Camera {cats['camera']:.1f} Β· "
f"Performance {cats['performance']:.1f} Β· "
f"Battery {cats['battery']:.1f}"
),
"ios": (
f"iOS ecosystem {cats['software']:.1f}/10 β€” "
f"{phone.dims.soft_update_policy.note}"
),
}
return reasons.get(priority, f"Tamam categories mein strong performance: {sum(cats.values())/len(cats):.1f}/10 average")