Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,419 +1,661 @@
|
|
| 1 |
-
# app.py — RideSearch (
|
| 2 |
|
| 3 |
-
import os, glob,
|
| 4 |
import numpy as np
|
| 5 |
import pandas as pd
|
| 6 |
-
from sklearn.metrics.pairwise import cosine_similarity
|
| 7 |
-
from sklearn.preprocessing import StandardScaler
|
| 8 |
import gradio as gr
|
| 9 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def load_df():
|
| 16 |
-
if os.path.exists(
|
| 17 |
-
return pd.read_csv(
|
| 18 |
-
parts = sorted(glob.glob(
|
| 19 |
if not parts:
|
| 20 |
raise FileNotFoundError("Upload RideSearch_dataset.csv OR the 10 parts RideSearch_part*_small.csv.")
|
| 21 |
df = pd.concat([pd.read_csv(p) for p in parts], ignore_index=True)
|
| 22 |
-
df.to_csv(
|
| 23 |
return df
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
]
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
if txt_ok and num_ok:
|
| 47 |
-
return np.load(
|
| 48 |
|
|
|
|
| 49 |
from sentence_transformers import SentenceTransformer
|
| 50 |
-
m = SentenceTransformer(
|
| 51 |
-
texts =
|
| 52 |
-
Etext = m.encode(texts, batch_size=256, show_progress_bar=
|
| 53 |
-
Etext = np.asarray(Etext, dtype=
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
# Handle missing values
|
| 62 |
-
X = X.fillna(X.mean())
|
| 63 |
-
|
| 64 |
-
Xs = StandardScaler().fit_transform(X.values.astype('float32'))
|
| 65 |
-
Enum = Xs.astype('float32'); np.save('emb_num.npy', Enum)
|
| 66 |
return Etext, Enum
|
| 67 |
|
| 68 |
-
#
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
df = df[df['make'] == make]
|
| 77 |
-
if model:
|
| 78 |
-
df = df[df['model'] == model]
|
| 79 |
-
|
| 80 |
-
# Get unique trims and clean them up
|
| 81 |
-
trims = df['trim'].dropna().astype(str).unique()
|
| 82 |
-
# Remove any obviously bad data
|
| 83 |
-
trims = [t for t in trims if t not in ['nan', 'None', '', 'null']]
|
| 84 |
-
return sorted(trims)
|
| 85 |
-
|
| 86 |
-
def get_available_years(make=None, model=None):
|
| 87 |
-
"""Get available years from actual data"""
|
| 88 |
-
df = DF.copy()
|
| 89 |
-
if make:
|
| 90 |
-
df = df[df['make'] == make]
|
| 91 |
-
if model:
|
| 92 |
-
df = df[df['model'] == model]
|
| 93 |
-
|
| 94 |
-
years = df['year'].dropna().astype(int).unique()
|
| 95 |
-
return sorted(years)
|
| 96 |
-
|
| 97 |
-
# ==============================
|
| 98 |
-
# Dropdowns, filters, formatting
|
| 99 |
-
# ==============================
|
| 100 |
-
|
| 101 |
-
def models_for(make):
|
| 102 |
-
if not make:
|
| 103 |
-
return gr.update(choices=[], value=None)
|
| 104 |
-
opts = sorted(DF.loc[DF['make'].eq(make), 'model'].dropna().unique().tolist())
|
| 105 |
-
return gr.update(choices=opts, value=None)
|
| 106 |
-
|
| 107 |
-
def on_model_change(make, model):
|
| 108 |
-
trims = get_available_trims(make, model)
|
| 109 |
-
years = get_available_years(make, model)
|
| 110 |
-
return gr.update(choices=trims, value=None), gr.update(choices=years, value=None)
|
| 111 |
-
|
| 112 |
-
def apply_filters(df, body, fuel, y_min, y_max, p_min, p_max, safety, reliab):
|
| 113 |
-
out = df.copy()
|
| 114 |
-
if body != 'Any':
|
| 115 |
-
out = out[out['body_type'] == body]
|
| 116 |
-
if fuel != 'Any':
|
| 117 |
-
out = out[out['fuel'] == fuel]
|
| 118 |
-
out = out[(out['year'] >= y_min) & (out['year'] <= y_max)]
|
| 119 |
-
out = out[(out['price_usd'] >= p_min) & (out['price_usd'] <= p_max)]
|
| 120 |
-
out = out[(out['safety_rating'] >= safety) & (out['reliability_score'] >= reliab)]
|
| 121 |
return out
|
| 122 |
|
| 123 |
-
def
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
trim_str = f" {r['trim']}" if pd.notna(r['trim']) and str(r['trim']) != 'nan' else ""
|
| 153 |
-
label = f"{r['make']} {r['model']}{trim_str}"
|
| 154 |
-
|
| 155 |
-
if 'image_url' in r and isinstance(r['image_url'], str) and r['image_url'].strip():
|
| 156 |
-
img_src = r['image_url'].strip()
|
| 157 |
-
else:
|
| 158 |
-
img_src = placeholder_svg_data_uri(f"{r['make']} {r['model']}")
|
| 159 |
-
|
| 160 |
-
cards.append(f"""
|
| 161 |
-
<div style="width:240px;margin:6px;border:1px solid #ddd;border-radius:12px;overflow:hidden;background:#fff;">
|
| 162 |
-
<img src="{img_src}" style="width:240px;height:160px;object-fit:cover;display:block" />
|
| 163 |
-
<div style="padding:8px 10px;font:14px/1.3 Arial,sans-serif;color:#111">{label}</div>
|
| 164 |
-
</div>
|
| 165 |
-
""")
|
| 166 |
-
return f"<div style='display:flex;flex-wrap:wrap'>{''.join(cards)}</div>"
|
| 167 |
-
|
| 168 |
-
# ===========================
|
| 169 |
-
# Anchor selection & ranking
|
| 170 |
-
# ===========================
|
| 171 |
-
def anchor_row(make, model, trim, year):
|
| 172 |
-
"""Find the best matching anchor car from the dataset"""
|
| 173 |
-
sub = DF.copy()
|
| 174 |
-
|
| 175 |
-
if make:
|
| 176 |
-
sub = sub[sub['make'] == make]
|
| 177 |
-
if model:
|
| 178 |
-
sub = sub[sub['model'] == model]
|
| 179 |
-
|
| 180 |
-
def pick_best(df_):
|
| 181 |
-
if df_.empty:
|
| 182 |
-
return None
|
| 183 |
-
# Sort by popularity score (descending) then by year (descending)
|
| 184 |
-
return df_.sort_values(['popularity_score', 'year'], ascending=[False, False]).iloc[0]
|
| 185 |
-
|
| 186 |
-
# Try exact match first
|
| 187 |
-
exact = sub.copy()
|
| 188 |
-
if trim and str(trim) != 'nan':
|
| 189 |
-
exact = exact[exact['trim'] == trim]
|
| 190 |
-
if year:
|
| 191 |
-
exact = exact[exact['year'] == year]
|
| 192 |
-
if not exact.empty:
|
| 193 |
-
return pick_best(exact)
|
| 194 |
-
|
| 195 |
-
# Try same year, any trim
|
| 196 |
-
if year:
|
| 197 |
-
y_only = sub[sub['year'] == year]
|
| 198 |
-
if not y_only.empty:
|
| 199 |
-
return pick_best(y_only)
|
| 200 |
-
|
| 201 |
-
# Try same trim, any year
|
| 202 |
-
if trim and str(trim) != 'nan':
|
| 203 |
-
t_only = sub[sub['trim'] == trim]
|
| 204 |
-
if not t_only.empty:
|
| 205 |
-
return pick_best(t_only)
|
| 206 |
-
|
| 207 |
-
# Fallback to any match for make/model
|
| 208 |
-
return pick_best(sub)
|
| 209 |
-
|
| 210 |
-
def calculate_custom_similarity(anchor_idx, candidate_indices, text_emb, num_emb, alpha):
|
| 211 |
-
"""Calculate similarity with proper handling of acceleration times"""
|
| 212 |
-
# Text similarity
|
| 213 |
-
text_sim = cosine_similarity(
|
| 214 |
-
text_emb[anchor_idx:anchor_idx+1],
|
| 215 |
-
text_emb[candidate_indices]
|
| 216 |
-
)[0]
|
| 217 |
-
|
| 218 |
-
# Numerical similarity - but we need to handle acceleration specially
|
| 219 |
-
# For acceleration, lower is better, so we need to invert the similarity logic
|
| 220 |
-
anchor_features = num_emb[anchor_idx:anchor_idx+1]
|
| 221 |
-
candidate_features = num_emb[candidate_indices]
|
| 222 |
-
|
| 223 |
-
# Calculate cosine similarity for numerical features
|
| 224 |
-
num_sim = cosine_similarity(anchor_features, candidate_features)[0]
|
| 225 |
-
|
| 226 |
-
# Combine similarities
|
| 227 |
-
combined_sim = alpha * text_sim + (1 - alpha) * num_sim
|
| 228 |
-
|
| 229 |
-
return combined_sim, text_sim, num_sim
|
| 230 |
-
|
| 231 |
-
def recommend(make, model, trim, year, topk, alpha,
|
| 232 |
body, fuel, y_min, y_max, p_min, p_max, safety, reliab,
|
| 233 |
-
cross_brand_only=True, exclude_same_model=True):
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
if cross_brand_only:
|
| 243 |
-
pool = pool[pool[
|
| 244 |
if exclude_same_model:
|
| 245 |
-
pool = pool[~((pool[
|
| 246 |
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
| 249 |
if pool.empty:
|
| 250 |
-
return "No cars
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
)
|
| 260 |
-
|
| 261 |
-
# Rank and select top recommendations
|
| 262 |
-
order = np.argsort(-combined_sim)
|
| 263 |
-
seen_models = set()
|
| 264 |
-
chosen_indices = []
|
| 265 |
-
|
| 266 |
for j in order:
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
if model_key in seen_models:
|
| 272 |
continue
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
if len(chosen_indices) >= int(topk):
|
| 278 |
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
gr.
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
with gr.Tab("Pick & Recommend"):
|
| 330 |
with gr.Row():
|
| 331 |
-
mk = gr.Dropdown(
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
)
|
| 336 |
-
md = gr.Dropdown([], label="Model", value=None)
|
| 337 |
-
tr = gr.Dropdown([], label="Trim (optional)", value=None)
|
| 338 |
-
yr = gr.Dropdown([], label="Year (optional)", value=None)
|
| 339 |
-
|
| 340 |
-
# Set up dropdown dependencies
|
| 341 |
mk.change(models_for, mk, md)
|
| 342 |
-
md.change(
|
| 343 |
|
| 344 |
with gr.Row():
|
| 345 |
-
body = gr.Dropdown(
|
| 346 |
-
|
| 347 |
-
value='Any',
|
| 348 |
-
label='Body Type'
|
| 349 |
-
)
|
| 350 |
-
fuel = gr.Dropdown(
|
| 351 |
-
choices=['Any'] + sorted(DF['fuel'].dropna().unique().tolist()),
|
| 352 |
-
value='Any',
|
| 353 |
-
label='Fuel Type'
|
| 354 |
-
)
|
| 355 |
-
|
| 356 |
with gr.Row():
|
| 357 |
-
y_min = gr.Slider(y_lo, y_hi, value=y_lo, step=1, label=
|
| 358 |
-
y_max = gr.Slider(y_lo, y_hi, value=y_hi, step=1, label=
|
| 359 |
-
|
| 360 |
with gr.Row():
|
| 361 |
-
p_min = gr.Slider(p_lo, p_hi, value=p_lo, step=500, label=
|
| 362 |
-
p_max = gr.Slider(p_lo, p_hi, value=min(p_hi,
|
| 363 |
-
|
| 364 |
with gr.Row():
|
| 365 |
-
safety = gr.Slider(3, 5, value=4, step=1, label=
|
| 366 |
-
reliab = gr.Slider(55, 99, value=70, step=1, label=
|
| 367 |
-
|
| 368 |
with gr.Row():
|
| 369 |
-
topk = gr.Slider(1, 10, value=5, step=1, label=
|
| 370 |
-
alpha = gr.Slider(0, 1, value=0.7, step=0.05, label=
|
| 371 |
-
|
| 372 |
with gr.Row():
|
| 373 |
-
cross = gr.Checkbox(label="Cross
|
| 374 |
-
xmodel = gr.Checkbox(label="Exclude same model family"
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
with gr.Tab("Data Info"):
|
| 392 |
-
gr.Markdown("""
|
| 393 |
-
## Fixes Applied:
|
| 394 |
-
|
| 395 |
-
1. **Acceleration Times**: Fixed the incorrect negation of 0-100 km/h times
|
| 396 |
-
2. **Trim Levels**: Simplified trim handling to use actual data from your CSV files
|
| 397 |
-
3. **Data Validation**: Added proper handling of missing/invalid values
|
| 398 |
-
4. **Similarity Calculation**: Improved the similarity algorithm for better recommendations
|
| 399 |
-
5. **UI Improvements**: Better error handling and user feedback
|
| 400 |
-
|
| 401 |
-
## Dataset Overview:
|
| 402 |
-
- Cars in database: {:,}
|
| 403 |
-
- Years covered: {} - {}
|
| 404 |
-
- Price range: ${:,} - ${:,}
|
| 405 |
-
- Makes available: {}
|
| 406 |
-
""".format(
|
| 407 |
-
len(DF),
|
| 408 |
-
int(DF['year'].min()), int(DF['year'].max()),
|
| 409 |
-
int(DF['price_usd'].min()), int(DF['price_usd'].max()),
|
| 410 |
-
len(DF['make'].unique())
|
| 411 |
-
))
|
| 412 |
|
| 413 |
-
|
|
|
|
|
|
|
| 414 |
|
| 415 |
-
|
| 416 |
-
|
|
|
|
| 417 |
|
| 418 |
if __name__ == "__main__":
|
|
|
|
| 419 |
demo.queue().launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
| 1 |
+
# app.py — RideSearch (real trims, fixed 0–100, cross‑brand, photos)
|
| 2 |
|
| 3 |
+
import os, glob, json, math, requests
|
| 4 |
import numpy as np
|
| 5 |
import pandas as pd
|
|
|
|
|
|
|
| 6 |
import gradio as gr
|
| 7 |
+
from sklearn.preprocessing import StandardScaler
|
| 8 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 9 |
+
|
| 10 |
+
# --------- Files / constants ----------
|
| 11 |
+
CSV_MAIN = "RideSearch_dataset.csv"
|
| 12 |
+
CSV_PARTS_GLOB = "RideSearch_part*_small.csv"
|
| 13 |
+
EMB_TEXT_NPY = "emb_text.npy"
|
| 14 |
+
EMB_NUM_NPY = "emb_num.npy"
|
| 15 |
+
EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
| 16 |
|
| 17 |
+
NUM_COLS = [
|
| 18 |
+
"horsepower","zero_to_100_kmh_s","seats","cargo_liters","price_usd",
|
| 19 |
+
"popularity_score","comfort_score","reliability_score","tech_score",
|
| 20 |
+
"ownership_cost_score","safety_rating"
|
| 21 |
+
]
|
| 22 |
|
| 23 |
+
# --------- Trim database (your long list, deduped) ----------
|
| 24 |
+
REAL_WORLD_TRIMS = {
|
| 25 |
+
# BMW
|
| 26 |
+
("BMW", "1 Series"): ["116i","118i","120i","125i","M135i"],
|
| 27 |
+
("BMW", "2 Series"): ["218i","220i","225i","230i","M235i","M240i"],
|
| 28 |
+
("BMW", "3 Series"): ["318i","320i","325i","330i","330e","335i","340i","M340i","M3"],
|
| 29 |
+
("BMW", "4 Series"): ["420i","430i","435i","440i","M440i","M4"],
|
| 30 |
+
("BMW", "5 Series"): ["520i","525i","530i","530e","535i","540i","550i","M550i","M5"],
|
| 31 |
+
("BMW", "6 Series"): ["630i","640i","650i","M6"],
|
| 32 |
+
("BMW", "7 Series"): ["730i","740i","750i","760i","M760i"],
|
| 33 |
+
("BMW", "8 Series"): ["840i","850i","M850i","M8"],
|
| 34 |
+
("BMW", "X1"): ["sDrive18i","sDrive20i","xDrive20i","xDrive25i"],
|
| 35 |
+
("BMW", "X2"): ["sDrive18i","sDrive20i","xDrive20i","xDrive25i","M35i"],
|
| 36 |
+
("BMW", "X3"): ["sDrive20i","xDrive20i","xDrive30i","M40i","X3 M"],
|
| 37 |
+
("BMW", "X4"): ["xDrive20i","xDrive30i","M40i","X4 M"],
|
| 38 |
+
("BMW", "X5"): ["sDrive40i","xDrive40i","xDrive45e","M50i","X5 M"],
|
| 39 |
+
("BMW", "X6"): ["sDrive40i","xDrive40i","M50i","X6 M"],
|
| 40 |
+
("BMW", "X7"): ["xDrive40i","xDrive50i","M50i"],
|
| 41 |
+
("BMW", "Z4"): ["sDrive20i","sDrive30i","M40i"],
|
| 42 |
+
|
| 43 |
+
# Audi
|
| 44 |
+
("Audi", "A1"): ["25 TFSI","30 TFSI","35 TFSI","S1"],
|
| 45 |
+
("Audi", "A3"): ["30 TFSI","35 TFSI","40 TFSI","45 TFSI e","S3","RS3"],
|
| 46 |
+
("Audi", "A4"): ["35 TFSI","40 TFSI","45 TFSI","45 TFSI e","S4","RS4"],
|
| 47 |
+
("Audi", "A5"): ["35 TFSI","40 TFSI","45 TFSI","S5","RS5"],
|
| 48 |
+
("Audi", "A6"): ["40 TFSI","45 TFSI","50 TFSI","55 TFSI e","S6","RS6"],
|
| 49 |
+
("Audi", "A7"): ["45 TFSI","50 TFSI","55 TFSI e","S7","RS7"],
|
| 50 |
+
("Audi", "A8"): ["50 TFSI","55 TFSI","60 TFSI e","S8"],
|
| 51 |
+
("Audi", "Q2"): ["30 TFSI","35 TFSI","40 TFSI"],
|
| 52 |
+
("Audi", "Q3"): ["35 TFSI","40 TFSI","45 TFSI e","RS Q3"],
|
| 53 |
+
("Audi", "Q5"): ["40 TFSI","45 TFSI e","SQ5"],
|
| 54 |
+
("Audi", "Q7"): ["45 TFSI","50 TDI","55 TFSI e","SQ7"],
|
| 55 |
+
("Audi", "Q8"): ["50 TDI","55 TFSI","SQ8","RS Q8"],
|
| 56 |
+
("Audi", "TT"): ["40 TFSI","45 TFSI","TTS","TT RS"],
|
| 57 |
+
("Audi", "R8"): ["V10","V10 Performance","V10 Plus"],
|
| 58 |
+
|
| 59 |
+
# Mercedes-Benz
|
| 60 |
+
("Mercedes-Benz", "A-Class"): ["A180","A200","A220","A250","A250e","AMG A35","AMG A45"],
|
| 61 |
+
("Mercedes-Benz", "B-Class"): ["B180","B200","B220","B250e"],
|
| 62 |
+
("Mercedes-Benz", "C-Class"): ["C180","C200","C220d","C250","C300","C350e","AMG C43","AMG C63"],
|
| 63 |
+
("Mercedes-Benz", "E-Class"): ["E200","E220d","E300","E350","E400","E450","AMG E53","AMG E63"],
|
| 64 |
+
("Mercedes-Benz", "S-Class"): ["S450","S500","S580","AMG S63","AMG S65"],
|
| 65 |
+
("Mercedes-Benz", "CLA"): ["CLA180","CLA200","CLA220","CLA250","AMG CLA35","AMG CLA45"],
|
| 66 |
+
("Mercedes-Benz", "CLS"): ["CLS350","CLS450","AMG CLS53","AMG CLS63"],
|
| 67 |
+
("Mercedes-Benz", "GLA"): ["GLA180","GLA200","GLA220","GLA250","AMG GLA35","AMG GLA45"],
|
| 68 |
+
("Mercedes-Benz", "GLB"): ["GLB200","GLB220","GLB250","AMG GLB35"],
|
| 69 |
+
("Mercedes-Benz", "GLC"): ["GLC200","GLC220d","GLC300","GLC350e","AMG GLC43","AMG GLC63"],
|
| 70 |
+
("Mercedes-Benz", "GLE"): ["GLE350","GLE450","GLE580","AMG GLE53","AMG GLE63"],
|
| 71 |
+
("Mercedes-Benz", "GLS"): ["GLS450","GLS580","AMG GLS63"],
|
| 72 |
+
("Mercedes-Benz", "G-Class"): ["G500","G550","AMG G63"],
|
| 73 |
+
|
| 74 |
+
# Toyota (sedans/SUVs)
|
| 75 |
+
("Toyota", "Corolla"): ["L","LE","XLE","SE","XSE","GR Corolla"],
|
| 76 |
+
("Toyota", "Camry"): ["LE","SE","XLE","XSE","TRD"],
|
| 77 |
+
("Toyota", "Avalon"): ["XLE","XSE","Limited","TRD"],
|
| 78 |
+
("Toyota", "Prius"): ["LE","XLE","Limited","Prime"],
|
| 79 |
+
("Toyota", "RAV4"): ["LE","XLE","XLE Premium","Adventure","TRD Off-Road","Limited","Prime"],
|
| 80 |
+
("Toyota", "Highlander"): ["L","LE","XLE","Limited","Platinum","Hybrid"],
|
| 81 |
+
("Toyota", "4Runner"): ["SR5","TRD Off-Road","TRD Pro","Limited"],
|
| 82 |
+
("Toyota", "Tacoma"): ["SR","SR5","TRD Sport","TRD Off-Road","TRD Pro","Limited"],
|
| 83 |
+
("Toyota", "Tundra"): ["SR","SR5","Limited","Platinum","1794","TRD Pro"],
|
| 84 |
+
("Toyota", "Land Cruiser"): ["Base","Heritage Edition"],
|
| 85 |
+
("Toyota", "Supra"): ["2.0","3.0","3.0 Premium"],
|
| 86 |
+
("Toyota", "Yaris"): ["L","LE","XLE","GRMN"],
|
| 87 |
+
("Toyota", "C-HR"): ["LE","XLE","Nightshade","Limited"],
|
| 88 |
+
("Toyota", "Sequoia"): ["SR5","Limited","Platinum","TRD Pro","Capstone"],
|
| 89 |
+
("Toyota", "Sienna"): ["LE","XLE","XSE","Limited","Platinum"],
|
| 90 |
+
|
| 91 |
+
# Honda
|
| 92 |
+
("Honda", "Civic"): ["LX","Sport","EX","EX-L","Sport Touring","Touring","Si","Type R"],
|
| 93 |
+
("Honda", "Accord"): ["LX","Sport","EX","EX-L","Touring","Sport-L"],
|
| 94 |
+
("Honda", "CR-V"): ["LX","EX","EX-L","Touring","Hybrid"],
|
| 95 |
+
("Honda", "HR-V"): ["LX","Sport","EX","EX-L"],
|
| 96 |
+
("Honda", "Pilot"): ["LX","EX","EX-L","Touring","Elite","TrailSport"],
|
| 97 |
+
("Honda", "Passport"): ["Sport","EX-L","Touring","Elite","TrailSport"],
|
| 98 |
+
("Honda", "Ridgeline"): ["Sport","RTL","RTL-E","Black Edition"],
|
| 99 |
+
("Honda", "Insight"): ["LX","EX","Touring"],
|
| 100 |
+
("Honda", "Fit / Jazz"): ["LX","Sport","EX","EX-L"],
|
| 101 |
+
("Honda", "Odyssey"): ["EX","EX-L","Touring","Elite"],
|
| 102 |
+
|
| 103 |
+
# Volkswagen
|
| 104 |
+
("Volkswagen", "Golf"): ["S","SE","SEL","Autobahn","GTI","R"],
|
| 105 |
+
("Volkswagen", "Jetta"): ["S","SE","SEL","SEL Premium","GLI"],
|
| 106 |
+
("Volkswagen", "Passat"): ["S","SE","SEL Premium","R-Line"],
|
| 107 |
+
("Volkswagen", "Tiguan"): ["S","SE","SEL","SEL Premium","R-Line"],
|
| 108 |
+
("Volkswagen", "Atlas"): ["S","SE","SEL","SEL Premium","Cross Sport"],
|
| 109 |
+
("Volkswagen", "Arteon"): ["SE","SEL","SEL Premium","R-Line"],
|
| 110 |
+
("Volkswagen", "ID.4"): ["Pro","Pro S","1st Edition"],
|
| 111 |
+
("Volkswagen", "Tiguan (alt)"): ["S","SE","SEL","R-Line"],
|
| 112 |
+
|
| 113 |
+
# Nissan
|
| 114 |
+
("Nissan", "Sentra"): ["S","SV","SR"],
|
| 115 |
+
("Nissan", "Altima"): ["S","SV","SL","SR","Platinum"],
|
| 116 |
+
("Nissan", "Maxima"): ["S","SV","SL","SR","Platinum"],
|
| 117 |
+
("Nissan", "Versa"): ["S","SV","SR"],
|
| 118 |
+
("Nissan", "Rogue"): ["S","SV","SL","Platinum"],
|
| 119 |
+
("Nissan", "Murano"): ["S","SV","SL","Platinum"],
|
| 120 |
+
("Nissan", "Pathfinder"): ["S","SV","SL","Platinum"],
|
| 121 |
+
("Nissan", "Armada"): ["SV","SL","Platinum"],
|
| 122 |
+
("Nissan", "Frontier"): ["S","SV","PRO-4X"],
|
| 123 |
+
("Nissan", "Titan"): ["S","SV","PRO-4X","Platinum Reserve"],
|
| 124 |
+
("Nissan", "Z"): ["Sport","Performance"],
|
| 125 |
+
("Nissan", "GT-R"): ["Premium","NISMO","Track Edition"],
|
| 126 |
+
("Nissan", "Qashqai / Rogue Sport"): ["S","SV","SL"],
|
| 127 |
+
("Nissan", "X-Trail"): ["Visia","Acenta","N-Connecta","Tekna"],
|
| 128 |
+
|
| 129 |
+
# Hyundai
|
| 130 |
+
("Hyundai", "Elantra"): ["SE","SEL","Limited","N Line","N"],
|
| 131 |
+
("Hyundai", "Sonata"): ["SE","SEL","Limited","N Line"],
|
| 132 |
+
("Hyundai", "Accent"): ["SE","SEL"],
|
| 133 |
+
("Hyundai", "Tucson"): ["SE","SEL","Limited","N Line"],
|
| 134 |
+
("Hyundai", "Santa Fe"): ["SE","SEL","Limited","Calligraphy"],
|
| 135 |
+
("Hyundai", "Palisade"): ["SE","SEL","Limited","Calligraphy"],
|
| 136 |
+
("Hyundai", "Kona"): ["SE","SEL","Limited","N Line","N"],
|
| 137 |
+
("Hyundai", "Venue"): ["SE","SEL"],
|
| 138 |
+
("Hyundai", "Ioniq 5"): ["SE","SEL","Limited"],
|
| 139 |
+
("Hyundai", "Ioniq 6"): ["SE","SEL","Limited"],
|
| 140 |
+
("Hyundai", "Santa Cruz"): ["SE","SEL","Night","Limited"],
|
| 141 |
+
|
| 142 |
+
# Kia
|
| 143 |
+
("Kia", "Rio"): ["LX","S"],
|
| 144 |
+
("Kia", "Forte"): ["LX","S","EX","GT-Line","GT"],
|
| 145 |
+
("Kia", "K5"): ["LX","S","EX","GT-Line"],
|
| 146 |
+
("Kia", "Stinger"): ["GT-Line","GT1","GT2"],
|
| 147 |
+
("Kia", "Soul"): ["LX","S","EX","GT-Line"],
|
| 148 |
+
("Kia", "Seltos"): ["LX","S","EX","SX Turbo"],
|
| 149 |
+
("Kia", "Sportage"): ["LX","S","EX","SX Turbo"],
|
| 150 |
+
("Kia", "Sorento"): ["LX","S","EX","SX","SX Turbo"],
|
| 151 |
+
("Kia", "Telluride"): ["LX","S","EX","SX"],
|
| 152 |
+
("Kia", "Carnival"): ["LX","S","EX","SX"],
|
| 153 |
+
("Kia", "EV6"): ["Light","Wind","GT-Line"],
|
| 154 |
+
("Kia", "Niro"): ["LX","EX","SX Touring"],
|
| 155 |
+
("Kia", "EV9"): ["Light","Wind","Land","GT-Line"],
|
| 156 |
+
|
| 157 |
+
# Ford
|
| 158 |
+
("Ford", "Fiesta"): ["S","SE","Titanium","ST"],
|
| 159 |
+
("Ford", "Focus"): ["S","SE","SEL","Titanium","ST","RS"],
|
| 160 |
+
("Ford", "Fusion"): ["S","SE","SEL","Titanium","Sport"],
|
| 161 |
+
("Ford", "Mustang"): ["EcoBoost","GT","Mach 1","Shelby GT350","Shelby GT500"],
|
| 162 |
+
("Ford", "Escape"): ["S","SE","SEL","Titanium"],
|
| 163 |
+
("Ford", "Edge"): ["SE","SEL","Titanium","ST"],
|
| 164 |
+
("Ford", "Explorer"): ["Base","XLT","Limited","King Ranch","Platinum","ST"],
|
| 165 |
+
("Ford", "Expedition"): ["XLT","Limited","King Ranch","Platinum"],
|
| 166 |
+
("Ford", "F-150"): ["Regular Cab","SuperCab","SuperCrew","Raptor","Lightning"],
|
| 167 |
+
("Ford", "Ranger"): ["XL","XLT","Lariat"],
|
| 168 |
+
("Ford", "Bronco"): ["Base","Big Bend","Black Diamond","Outer Banks","Badlands","Wildtrak","Raptor"],
|
| 169 |
+
("Ford", "Maverick"): ["XL","XLT","Lariat","Tremor"],
|
| 170 |
+
("Ford", "Bronco Sport"): ["Base","Big Bend","Outer Banks","Badlands"],
|
| 171 |
+
|
| 172 |
+
# Chevrolet
|
| 173 |
+
("Chevrolet", "Spark"): ["LS","LT","Premier"],
|
| 174 |
+
("Chevrolet", "Sonic"): ["LS","LT","Premier"],
|
| 175 |
+
("Chevrolet", "Cruze"): ["L","LS","LT","Premier"],
|
| 176 |
+
("Chevrolet", "Malibu"): ["L","LS","LT","Premier"],
|
| 177 |
+
("Chevrolet", "Impala"): ["LS","LT","Premier"],
|
| 178 |
+
("Chevrolet", "Camaro"): ["1LS","1LT","2LT","1SS","2SS","ZL1"],
|
| 179 |
+
("Chevrolet", "Corvette"): ["1LT","2LT","3LT","Z06","ZR1"],
|
| 180 |
+
("Chevrolet", "Trax"): ["L","LS","LT","Premier"],
|
| 181 |
+
("Chevrolet", "Equinox"): ["L","LS","LT","Premier"],
|
| 182 |
+
("Chevrolet", "Traverse"): ["L","LS","LT","Premier","High Country"],
|
| 183 |
+
("Chevrolet", "Tahoe"): ["LS","LT","RST","Premier","High Country"],
|
| 184 |
+
("Chevrolet", "Suburban"): ["LS","LT","RST","Premier","High Country"],
|
| 185 |
+
("Chevrolet", "Blazer"): ["LT","RS","Premier"],
|
| 186 |
+
("Chevrolet", "Trailblazer"): ["LS","LT","ACTIV","RS"],
|
| 187 |
+
("Chevrolet", "Bolt EV"): ["1LT","2LT"],
|
| 188 |
+
("Chevrolet", "Silverado 1500"): ["Work Truck","Custom","LT","RST","LTZ","High Country"],
|
| 189 |
+
|
| 190 |
+
# Lexus
|
| 191 |
+
("Lexus", "IS"): ["300","350","500 F SPORT Performance"],
|
| 192 |
+
("Lexus", "ES"): ["250","300h","350"],
|
| 193 |
+
("Lexus", "GS"): ["300","350","450h","F"],
|
| 194 |
+
("Lexus", "LS"): ["500","500h"],
|
| 195 |
+
("Lexus", "LC"): ["500","500h"],
|
| 196 |
+
("Lexus", "RC"): ["300","350","F"],
|
| 197 |
+
("Lexus", "UX"): ["200","250h"],
|
| 198 |
+
("Lexus", "NX"): ["250","350","350h","450h+"],
|
| 199 |
+
("Lexus", "RX"): ["350","350h","500h F SPORT Performance"],
|
| 200 |
+
("Lexus", "GX"): ["460"],
|
| 201 |
+
("Lexus", "LX"): ["570","600"],
|
| 202 |
+
|
| 203 |
+
# Infiniti
|
| 204 |
+
("Infiniti", "Q50"): ["Pure","Luxe","Sensory","Red Sport 400"],
|
| 205 |
+
("Infiniti", "Q60"): ["Pure","Luxe","Sensory","Red Sport 400"],
|
| 206 |
+
("Infiniti", "Q70"): ["Base","Sport"],
|
| 207 |
+
("Infiniti", "QX50"): ["Pure","Luxe","Sensory","Autograph"],
|
| 208 |
+
("Infiniti", "QX55"): ["Luxe","Essential","Sensory"],
|
| 209 |
+
("Infiniti", "QX60"): ["Pure","Luxe","Sensory","Autograph"],
|
| 210 |
+
("Infiniti", "QX80"): ["Pure","Luxe","Sensory","Autograph"],
|
| 211 |
+
|
| 212 |
+
# Acura
|
| 213 |
+
("Acura", "ILX"): ["Base","Premium","A-Spec"],
|
| 214 |
+
("Acura", "TLX"): ["Base","Technology","A-Spec","Advance","Type S"],
|
| 215 |
+
("Acura", "RLX"): ["Base","Technology","Advance"],
|
| 216 |
+
("Acura", "NSX"): ["Base","Type S"],
|
| 217 |
+
("Acura", "RDX"): ["Base","Technology","A-Spec","Advance"],
|
| 218 |
+
("Acura", "MDX"): ["Base","Technology","A-Spec","Advance","Type S"],
|
| 219 |
+
|
| 220 |
+
# Cadillac
|
| 221 |
+
("Cadillac", "ATS"): ["Base","Luxury","Premium Luxury","V-Sport","V"],
|
| 222 |
+
("Cadillac", "CTS"): ["Base","Luxury","Premium Luxury","V-Sport","V"],
|
| 223 |
+
("Cadillac", "CT4"): ["Luxury","Premium Luxury","Sport","V-Series","V-Series Blackwing"],
|
| 224 |
+
("Cadillac", "CT5"): ["Luxury","Premium Luxury","Sport","V-Series","V-Series Blackwing"],
|
| 225 |
+
("Cadillac", "XTS"): ["Base","Luxury","Premium Luxury","Platinum"],
|
| 226 |
+
("Cadillac", "XT4"): ["Luxury","Premium Luxury","Sport"],
|
| 227 |
+
("Cadillac", "XT5"): ["Luxury","Premium Luxury","Sport"],
|
| 228 |
+
("Cadillac", "XT6"): ["Luxury","Premium Luxury","Sport"],
|
| 229 |
+
("Cadillac", "Escalade"): ["Luxury","Premium Luxury","Sport","V-Series"],
|
| 230 |
+
|
| 231 |
+
# Lincoln
|
| 232 |
+
("Lincoln", "MKZ"): ["Premiere","Select","Reserve"],
|
| 233 |
+
("Lincoln", "Continental"): ["Premiere","Select","Reserve","Coach Door Edition"],
|
| 234 |
+
("Lincoln", "Corsair"): ["Base","Reserve"],
|
| 235 |
+
("Lincoln", "Nautilus"): ["Base","Reserve"],
|
| 236 |
+
("Lincoln", "Aviator"): ["Premiere","Reserve","Grand Touring","Black Label"],
|
| 237 |
+
("Lincoln", "Navigator"): ["Premiere","Select","Reserve","Black Label"],
|
| 238 |
+
|
| 239 |
+
# Buick
|
| 240 |
+
("Buick", "Verano"): ["Base","Convenience","Leather","Premium"],
|
| 241 |
+
("Buick", "Regal"): ["1SV","Preferred","Essence","GS"],
|
| 242 |
+
("Buick", "LaCrosse"): ["Base","Preferred","Essence","Premium","Avenir"],
|
| 243 |
+
("Buick", "Encore"): ["Base","Convenience","Leather","Premium"],
|
| 244 |
+
("Buick", "Envision"): ["Preferred","Essence","Premium","Avenir"],
|
| 245 |
+
("Buick", "Enclave"): ["Base","Essence","Premium","Avenir"],
|
| 246 |
+
|
| 247 |
+
# Genesis
|
| 248 |
+
("Genesis", "G70"): ["2.0T","2.0T Advanced","3.3T Sport","3.3T Sport Prestige"],
|
| 249 |
+
("Genesis", "G80"): ["2.5T","2.5T Advanced","3.5T Sport","3.5T Sport Prestige"],
|
| 250 |
+
("Genesis", "G90"): ["3.3T Premium","3.3T Prestige","5.0 Ultimate"],
|
| 251 |
+
("Genesis", "GV70"): ["2.5T","2.5T Advanced","3.5T Sport","3.5T Sport Prestige"],
|
| 252 |
+
("Genesis", "GV80"): ["2.5T","2.5T Advanced","3.5T","3.5T Prestige"],
|
| 253 |
+
|
| 254 |
+
# Tesla
|
| 255 |
+
("Tesla", "Model S"): ["Standard Range","Long Range","Plaid"],
|
| 256 |
+
("Tesla", "Model 3"): ["Standard Range Plus","Long Range","Performance"],
|
| 257 |
+
("Tesla", "Model X"): ["Standard Range","Long Range","Plaid"],
|
| 258 |
+
("Tesla", "Model Y"): ["Standard Range","Long Range","Performance"],
|
| 259 |
+
|
| 260 |
+
# Porsche
|
| 261 |
+
("Porsche", "911"): ["Carrera","Carrera S","Carrera 4","Carrera 4S","Turbo","Turbo S","GT3","GT3 RS","GT2 RS"],
|
| 262 |
+
("Porsche", "Boxster"): ["Base","S","GTS","Spyder"],
|
| 263 |
+
("Porsche", "Cayman"): ["Base","S","GTS","GT4"],
|
| 264 |
+
("Porsche", "Panamera"): ["Base","4","S","4S","GTS","Turbo","Turbo S"],
|
| 265 |
+
("Porsche", "Macan"): ["Base","S","GTS","Turbo"],
|
| 266 |
+
("Porsche", "Cayenne"): ["Base","S","GTS","Turbo","Turbo S","E-Hybrid"],
|
| 267 |
+
("Porsche", "Taycan"): ["Base","4S","Turbo","Turbo S"],
|
| 268 |
+
|
| 269 |
+
# Jaguar
|
| 270 |
+
("Jaguar", "XE"): ["Base","Premium","Prestige","R-Sport","S"],
|
| 271 |
+
("Jaguar", "XF"): ["Premium","Prestige","R-Sport","S"],
|
| 272 |
+
("Jaguar", "XJ"): ["Premium Luxury","Portfolio","Autobiography","XJR575"],
|
| 273 |
+
("Jaguar", "F-TYPE"): ["Base","Premium","R-Dynamic","R","SVR"],
|
| 274 |
+
("Jaguar", "E-PACE"): ["Base","S","SE","HSE","R-Dynamic"],
|
| 275 |
+
("Jaguar", "F-PACE"): ["Premium","Prestige","R-Sport","S","SVR"],
|
| 276 |
+
("Jaguar", "I-PACE"): ["S","SE","HSE","First Edition"],
|
| 277 |
+
|
| 278 |
+
# Land Rover
|
| 279 |
+
("Land Rover", "Range Rover Evoque"): ["S","SE","HSE","HSE Dynamic","Autobiography"],
|
| 280 |
+
("Land Rover", "Range Rover Velar"): ["S","SE","HSE","R-Dynamic","P380","P550"],
|
| 281 |
+
("Land Rover", "Range Rover Sport"): ["HSE","HSE Dynamic","Autobiography","SVR"],
|
| 282 |
+
("Land Rover", "Range Rover"): ["Base","HSE","Autobiography","SV","SVAutobiography"],
|
| 283 |
+
("Land Rover", "Discovery Sport"): ["S","SE","HSE","HSE Luxury"],
|
| 284 |
+
("Land Rover", "Discovery"): ["S","SE","HSE","HSE Luxury"],
|
| 285 |
+
("Land Rover", "Defender"): ["90","110","130","X","X-Dynamic","First Edition"],
|
| 286 |
+
|
| 287 |
+
# Volvo
|
| 288 |
+
("Volvo", "S60"): ["T5 Momentum","T5 R-Design","T6 R-Design","T8 Polestar"],
|
| 289 |
+
("Volvo", "S90"): ["T5 Momentum","T6 Momentum","T6 R-Design","T8 Inscription"],
|
| 290 |
+
("Volvo", "V60"): ["T5 Momentum","T5 R-Design","T6 R-Design","T8 Polestar"],
|
| 291 |
+
("Volvo", "V90"): ["T5 Momentum","T6 Momentum","T6 R-Design","T8 Inscription"],
|
| 292 |
+
("Volvo", "XC40"): ["T4 Momentum","T5 Momentum","T5 R-Design","Recharge"],
|
| 293 |
+
("Volvo", "XC60"): ["Core","Plus","Ultimate","Polestar Engineered"],
|
| 294 |
+
("Volvo", "XC90"): ["Momentum","R-Design","Inscription","Recharge"],
|
| 295 |
+
|
| 296 |
+
# MINI
|
| 297 |
+
("Mini", "Cooper"): ["Base","Classic","Signature","Iconic","John Cooper Works"],
|
| 298 |
+
("Mini", "Countryman"): ["Classic","Signature","Iconic","John Cooper Works"],
|
| 299 |
+
("Mini", "Clubman"): ["Classic","Signature","Iconic","John Cooper Works"],
|
| 300 |
+
|
| 301 |
+
# Mazda
|
| 302 |
+
("Mazda", "Mazda3"): ["Base","Select","Preferred","Premium","Turbo"],
|
| 303 |
+
("Mazda", "Mazda6"): ["Sport","Touring","Grand Touring","Grand Touring Reserve","Signature"],
|
| 304 |
+
("Mazda", "CX-3"): ["Sport","Touring","Grand Touring"],
|
| 305 |
+
("Mazda", "CX-30"): ["S","Select","Preferred","Premium","Turbo","Turbo Premium Plus"],
|
| 306 |
+
("Mazda", "CX-5"): ["S","Select","Preferred","Premium","Turbo","Turbo Signature"],
|
| 307 |
+
("Mazda", "CX-50"): ["2.5 S","2.5 S Select","2.5 S Preferred","2.5 Turbo","2.5 Turbo Premium"],
|
| 308 |
+
("Mazda", "CX-9"): ["Sport","Touring","Grand Touring","Signature","Carbon Edition"],
|
| 309 |
+
("Mazda", "MX-5 Miata"): ["Sport","Club","Grand Touring"],
|
| 310 |
+
("Mazda", "MX-30"): ["EV","EV Premium Plus"],
|
| 311 |
+
|
| 312 |
+
# Peugeot
|
| 313 |
+
("Peugeot", "208"): ["Active","Allure","GT"],
|
| 314 |
+
("Peugeot", "2008"): ["Active","Allure","GT"],
|
| 315 |
+
("Peugeot", "308"): ["Active","Allure","GT"],
|
| 316 |
+
("Peugeot", "3008"): ["Active","Allure","GT","GT Pack"],
|
| 317 |
+
("Peugeot", "5008"): ["Active","Allure","GT"],
|
| 318 |
+
("Peugeot", "508"): ["Active","Allure","GT","PSE"],
|
| 319 |
+
|
| 320 |
+
# Renault
|
| 321 |
+
("Renault", "Clio"): ["Play","Iconic","S Edition","RS Line"],
|
| 322 |
+
("Renault", "Megane"): ["Play","Iconic","RS Line","RS Trophy"],
|
| 323 |
+
("Renault", "Captur"): ["Play","Iconic","S Edition","RS Line"],
|
| 324 |
+
("Renault", "Kadjar"): ["Play","Iconic","S Edition","GT Line"],
|
| 325 |
+
("Renault", "Arkana"): ["Iconic","S Edition","RS Line"],
|
| 326 |
+
("Renault", "Austral"): ["Equilibre","Techno","Esprit Alpine"],
|
| 327 |
+
|
| 328 |
+
# Škoda / Skoda
|
| 329 |
+
("Skoda", "Fabia"): ["S","SE","SE L","Monte Carlo"],
|
| 330 |
+
("Skoda", "Octavia"): ["S","SE","SE L","SportLine","vRS"],
|
| 331 |
+
("Skoda", "Superb"): ["SE","SE L","SportLine","L&K"],
|
| 332 |
+
("Skoda", "Karoq"): ["SE Drive","SE L","SportLine"],
|
| 333 |
+
("Skoda", "Kodiaq"): ["SE","SE L","SportLine","vRS"],
|
| 334 |
+
|
| 335 |
+
# SEAT
|
| 336 |
+
("Seat", "Ibiza"): ["Reference","Style","Xcellence","FR"],
|
| 337 |
+
("Seat", "Leon"): ["Reference","Style","Xcellence","FR","Cupra"],
|
| 338 |
+
("Seat", "Arona"): ["Reference","Style","Xcellence","FR"],
|
| 339 |
+
("Seat", "Ateca"): ["Reference","Style","Xcellence","FR"],
|
| 340 |
+
("Seat", "Tarraco"): ["SE","SE Technology","Xcellence","FR"],
|
| 341 |
+
|
| 342 |
+
# Alfa Romeo
|
| 343 |
+
("Alfa Romeo", "Giulia"): ["Sprint","Ti","Veloce","Quadrifoglio"],
|
| 344 |
+
("Alfa Romeo", "Stelvio"): ["Sprint","Ti","Veloce","Quadrifoglio"],
|
| 345 |
+
("Alfa Romeo", "Tonale"): ["Ti","Veloce","Edizione Speciale"],
|
| 346 |
+
|
| 347 |
+
# Citroën
|
| 348 |
+
("Citroen", "C3"): ["Live","Feel","Shine"],
|
| 349 |
+
("Citroen", "C4"): ["Live","Feel","Shine"],
|
| 350 |
+
("Citroen", "C5 Aircross"): ["Feel","Shine","C-Series","Max"],
|
| 351 |
+
|
| 352 |
+
# Dodge
|
| 353 |
+
("Dodge", "Charger"): ["SXT","GT","R/T","Scat Pack","SRT Hellcat"],
|
| 354 |
+
("Dodge", "Challenger"): ["SXT","GT","R/T","Scat Pack","SRT Hellcat"],
|
| 355 |
+
("Dodge", "Durango"): ["SXT","GT","R/T","Citadel","SRT"],
|
| 356 |
+
|
| 357 |
+
# Ram
|
| 358 |
+
("Ram", "1500"): ["Tradesman","Big Horn","Laramie","Rebel","Limited"],
|
| 359 |
+
|
| 360 |
+
# GMC
|
| 361 |
+
("GMC", "Terrain"): ["SLE","SLT","AT4","Denali"],
|
| 362 |
+
("GMC", "Acadia"): ["SLE","SLT","AT4","Denali"],
|
| 363 |
+
("GMC", "Yukon"): ["SLE","SLT","AT4","Denali"],
|
| 364 |
+
("GMC", "Sierra 1500"): ["Pro","SLE","Elevation","SLT","AT4","Denali"],
|
| 365 |
+
|
| 366 |
+
# Chrysler
|
| 367 |
+
("Chrysler", "300"): ["Touring","Touring L","S"],
|
| 368 |
+
("Chrysler", "Pacifica"): ["Touring","Touring L","Limited","Pinnacle"],
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
# --------- Load + unify ----------
|
| 372 |
def load_df():
|
| 373 |
+
if os.path.exists(CSV_MAIN):
|
| 374 |
+
return pd.read_csv(CSV_MAIN)
|
| 375 |
+
parts = sorted(glob.glob(CSV_PARTS_GLOB))
|
| 376 |
if not parts:
|
| 377 |
raise FileNotFoundError("Upload RideSearch_dataset.csv OR the 10 parts RideSearch_part*_small.csv.")
|
| 378 |
df = pd.concat([pd.read_csv(p) for p in parts], ignore_index=True)
|
| 379 |
+
df.to_csv(CSV_MAIN, index=False)
|
| 380 |
return df
|
| 381 |
|
| 382 |
+
def unify_columns(df: pd.DataFrame) -> pd.DataFrame:
|
| 383 |
+
df = df.copy()
|
| 384 |
+
# create missing columns
|
| 385 |
+
for c in ["make","model","trim_display","year","body_type","fuel","engine_type","name"]:
|
| 386 |
+
if c not in df.columns:
|
| 387 |
+
df[c] = ""
|
| 388 |
+
if df["trim_display"].isna().all() and "trim" in df.columns:
|
| 389 |
+
df["trim_display"] = df["trim"].fillna("")
|
| 390 |
+
|
| 391 |
+
# alias numeric score names
|
| 392 |
+
if "popularity" in df.columns and "popularity_score" not in df.columns:
|
| 393 |
+
df["popularity_score"] = df["popularity"]
|
| 394 |
+
if "comfort" in df.columns and "comfort_score" not in df.columns:
|
| 395 |
+
df["comfort_score"] = df["comfort"]
|
| 396 |
+
if "reliability" in df.columns and "reliability_score" not in df.columns:
|
| 397 |
+
df["reliability_score"] = df["reliability"]
|
| 398 |
+
if "safety" in df.columns and "safety_rating" not in df.columns:
|
| 399 |
+
df["safety_rating"] = df["safety"]
|
| 400 |
+
|
| 401 |
+
for c in NUM_COLS:
|
| 402 |
+
if c not in df.columns: df[c] = np.nan
|
| 403 |
+
df[c] = pd.to_numeric(df[c], errors="coerce")
|
| 404 |
+
|
| 405 |
+
# build text_record if missing
|
| 406 |
+
if "text_record" not in df.columns:
|
| 407 |
+
cols = ["make","model","trim_display","body_type","fuel","engine_type"]
|
| 408 |
+
df["text_record"] = df[cols].fillna("").astype(str).agg(" ".join, axis=1)
|
| 409 |
|
| 410 |
+
return df
|
| 411 |
|
| 412 |
+
# --------- Fix 0–100 if broken ----------
|
| 413 |
+
def suspicious_zero_to_100(series: pd.Series) -> bool:
|
| 414 |
+
s = series.dropna()
|
| 415 |
+
if len(s) < 30: # need some mass to judge
|
| 416 |
+
return False
|
| 417 |
+
return (s.nunique() <= max(2, int(0.03*len(s)))) or (s.std() < 0.18)
|
| 418 |
+
|
| 419 |
+
def estimate_0_100(row):
|
| 420 |
+
# Heuristic depending on hp, body, fuel, trim hints. Clamped 2.8..14.5
|
| 421 |
+
try: hp = float(row.get("horsepower", 150) or 150)
|
| 422 |
+
except Exception: hp = 150.0
|
| 423 |
+
body = str(row.get("body_type","")).lower()
|
| 424 |
+
fuel = str(row.get("fuel","")).lower()
|
| 425 |
+
trim = f"{row.get('model','')} {row.get('trim_display','')}".lower()
|
| 426 |
+
|
| 427 |
+
base = 26.0 - 3.2*np.log(max(hp, 60.0)) # ~10s @100hp; ~6s @300hp
|
| 428 |
+
if any(k in body for k in ["suv","crossover","pickup","truck","van"]): base += 0.7
|
| 429 |
+
if any(k in body for k in ["coupe","roadster","hatch"]): base -= 0.3
|
| 430 |
+
if "electric" in fuel or "ev" in fuel: base -= 0.8
|
| 431 |
+
if "hybrid" in fuel: base -= 0.3
|
| 432 |
+
if "diesel" in fuel: base += 0.2
|
| 433 |
+
if any(x in trim for x in [" m ","amg","rs","type r","sti","gts","gt3","hellcat","svr","cupra","john cooper works"]):
|
| 434 |
+
base -= 0.6
|
| 435 |
+
return float(np.clip(round(base,2), 2.8, 14.5))
|
| 436 |
+
|
| 437 |
+
# --------- Embeddings (text + numeric) ----------
|
| 438 |
+
def ensure_embeddings(df):
|
| 439 |
+
txt_ok = os.path.exists(EMB_TEXT_NPY)
|
| 440 |
+
num_ok = os.path.exists(EMB_NUM_NPY)
|
| 441 |
if txt_ok and num_ok:
|
| 442 |
+
return np.load(EMB_TEXT_NPY), np.load(EMB_NUM_NPY)
|
| 443 |
|
| 444 |
+
# Text embeddings
|
| 445 |
from sentence_transformers import SentenceTransformer
|
| 446 |
+
m = SentenceTransformer(EMBED_MODEL)
|
| 447 |
+
texts = df["text_record"].astype(str).tolist()
|
| 448 |
+
Etext = m.encode(texts, batch_size=256, show_progress_bar=False, normalize_embeddings=True)
|
| 449 |
+
Etext = np.asarray(Etext, dtype="float32"); np.save(EMB_TEXT_NPY, Etext)
|
| 450 |
+
|
| 451 |
+
# Numeric embeddings
|
| 452 |
+
X = df[NUM_COLS].copy()
|
| 453 |
+
X["zero_to_100_kmh_s"] = X["zero_to_100_kmh_s"].astype("float32")
|
| 454 |
+
X = X.fillna(X.mean(numeric_only=True))
|
| 455 |
+
Xs = StandardScaler().fit_transform(X.values.astype("float32"))
|
| 456 |
+
Enum = Xs.astype("float32"); np.save(EMB_NUM_NPY, Enum)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
return Etext, Enum
|
| 458 |
|
| 459 |
+
# --------- Trims (curated + learned from CSV) ----------
|
| 460 |
+
def learned_trims_from_csv(df):
|
| 461 |
+
out = {}
|
| 462 |
+
grp = df.groupby(["make","model"])
|
| 463 |
+
for (mk, md), sub in grp:
|
| 464 |
+
vals = sub["trim_display"].dropna().astype(str).value_counts().index.tolist()
|
| 465 |
+
vals = [v for v in vals if v.lower() not in ("", "nan", "none")]
|
| 466 |
+
if vals: out[(mk, md)] = vals[:20]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
return out
|
| 468 |
|
| 469 |
+
def trims_for(make, model, learned):
|
| 470 |
+
key = (make, model)
|
| 471 |
+
if key in REAL_WORLD_TRIMS: return REAL_WORLD_TRIMS[key]
|
| 472 |
+
if key in learned: return learned[key]
|
| 473 |
+
return ["Base"]
|
| 474 |
+
|
| 475 |
+
# --------- Images via Wikipedia ----------
|
| 476 |
+
def wiki_image(make, model, year=None):
|
| 477 |
+
q = f"{year} {make} {model}" if year else f"{make} {model}"
|
| 478 |
+
try:
|
| 479 |
+
r = requests.get(
|
| 480 |
+
"https://en.wikipedia.org/w/api.php",
|
| 481 |
+
params={
|
| 482 |
+
"action":"query","format":"json","prop":"pageimages",
|
| 483 |
+
"piprop":"thumbnail","pithumbsize":600,
|
| 484 |
+
"generator":"search","gsrsearch":q,"gsrlimit":1,"gsrnamespace":0
|
| 485 |
+
},
|
| 486 |
+
timeout=6
|
| 487 |
+
).json()
|
| 488 |
+
pages = r.get("query",{}).get("pages",{})
|
| 489 |
+
if pages:
|
| 490 |
+
page = next(iter(pages.values()))
|
| 491 |
+
return page.get("thumbnail",{}).get("source")
|
| 492 |
+
except Exception:
|
| 493 |
+
pass
|
| 494 |
+
return None
|
| 495 |
+
|
| 496 |
+
# --------- Recommend ----------
|
| 497 |
+
def recommend(df, Etext, Enum, make, model, trim, year, topk, alpha,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
body, fuel, y_min, y_max, p_min, p_max, safety, reliab,
|
| 499 |
+
cross_brand_only=True, exclude_same_model=True, unique_brand=True):
|
| 500 |
+
|
| 501 |
+
if not make or not model:
|
| 502 |
+
return "Pick a make & model first.", None, "", []
|
| 503 |
+
|
| 504 |
+
# anchor candidates
|
| 505 |
+
sub = df[(df["make"]==make) & (df["model"]==model)].copy()
|
| 506 |
+
if trim:
|
| 507 |
+
sub = sub[sub["trim_display"].astype(str).str.lower() == str(trim).lower()]
|
| 508 |
+
if year:
|
| 509 |
+
sub = sub[pd.to_numeric(sub["year"], errors="coerce")==int(year)]
|
| 510 |
+
if sub.empty:
|
| 511 |
+
sub = df[(df["make"]==make) & (df["model"]==model)].copy()
|
| 512 |
+
if sub.empty:
|
| 513 |
+
return "No such make/model in dataset.", None, "", []
|
| 514 |
+
|
| 515 |
+
a = sub.sort_values("popularity_score", ascending=False).iloc[0]
|
| 516 |
+
a_idx = int(a.name)
|
| 517 |
+
|
| 518 |
+
# candidate pool
|
| 519 |
+
pool = df.copy()
|
| 520 |
if cross_brand_only:
|
| 521 |
+
pool = pool[pool["make"] != a["make"]]
|
| 522 |
if exclude_same_model:
|
| 523 |
+
pool = pool[~((pool["make"] == a["make"]) & (pool["model"] == a["model"]))]
|
| 524 |
|
| 525 |
+
if body and body != "Any": pool = pool[pool["body_type"] == body]
|
| 526 |
+
if fuel and fuel != "Any": pool = pool[pool["fuel"] == fuel]
|
| 527 |
+
pool = pool[(pool["year"] >= y_min) & (pool["year"] <= y_max)]
|
| 528 |
+
pool = pool[(pool["price_usd"] >= p_min) & (pool["price_usd"] <= p_max)]
|
| 529 |
+
pool = pool[(pool["safety_rating"] >= safety) & (pool["reliability_score"] >= reliab)]
|
| 530 |
if pool.empty:
|
| 531 |
+
return "No cars after your filters. Loosen price/year/safety.", None, "", []
|
| 532 |
+
|
| 533 |
+
cand_idx = pool.index.values
|
| 534 |
+
st = cosine_similarity(Etext[cand_idx], Etext[a_idx].reshape(1,-1)).ravel()
|
| 535 |
+
sn = cosine_similarity(Enum[cand_idx], Enum[a_idx].reshape(1,-1)).ravel()
|
| 536 |
+
s = float(alpha)*st + (1-float(alpha))*sn
|
| 537 |
+
|
| 538 |
+
order = np.argsort(-s)
|
| 539 |
+
chosen, seen = [], set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
for j in order:
|
| 541 |
+
k = cand_idx[j]
|
| 542 |
+
brand = str(df.loc[k,"make"]).lower()
|
| 543 |
+
if unique_brand and brand in seen:
|
|
|
|
|
|
|
| 544 |
continue
|
| 545 |
+
seen.add(brand)
|
| 546 |
+
chosen.append(k)
|
| 547 |
+
if len(chosen) >= int(topk):
|
|
|
|
|
|
|
| 548 |
break
|
| 549 |
+
if not chosen:
|
| 550 |
+
return "No recommendations after constraints.", None, "", []
|
| 551 |
+
|
| 552 |
+
out = df.loc[chosen].copy()
|
| 553 |
+
out["similarity_%"] = [round(float(s[np.where(cand_idx==k)[0][0]])*100,1) for k in chosen]
|
| 554 |
|
| 555 |
+
cols = ["name","make","model","trim_display","year","body_type","fuel","engine_type",
|
| 556 |
+
"price_usd","horsepower","zero_to_100_kmh_s","popularity_score","comfort_score",
|
| 557 |
+
"reliability_score","tech_score","ownership_cost_score","safety_rating","similarity_%"]
|
| 558 |
+
table = out[[c for c in cols if c in out.columns]].reset_index(drop=True)
|
| 559 |
+
|
| 560 |
+
disp_trim = str(a.get("trim_display","")).strip()
|
| 561 |
+
anchor_md = (f"**{a['make']} {a['model']} {disp_trim} {int(a['year']) if pd.notna(a['year']) else ''}** \n"
|
| 562 |
+
f"Body: {a.get('body_type','')} • Fuel: {a.get('fuel','')} • Engine: {a.get('engine_type','')} \n"
|
| 563 |
+
f"HP: {int(a['horsepower']) if pd.notna(a['horsepower']) else '—'} • 0–100: {a.get('zero_to_100_kmh_s','—')}s "
|
| 564 |
+
f"• Price: ${int(a['price_usd']) if pd.notna(a['price_usd']) else '—'} \n"
|
| 565 |
+
f"Popularity {int(a.get('popularity_score',0))}/10 • Comfort {int(a.get('comfort_score',0))}/10 • "
|
| 566 |
+
f"Reliability {int(a.get('reliability_score',0))}/100 • Safety {int(a.get('safety_rating',0))}★")
|
| 567 |
+
|
| 568 |
+
cards = []
|
| 569 |
+
for _, r in out.head(3).iterrows():
|
| 570 |
+
img = wiki_image(r.get("make",""), r.get("model",""), r.get("year",None))
|
| 571 |
+
title = f"{r.get('make','')} {r.get('model','')} {str(r.get('trim_display','')).strip()}"
|
| 572 |
+
cards.append((img, title))
|
| 573 |
+
return None, table, anchor_md, cards
|
| 574 |
+
|
| 575 |
+
# --------- Build UI ----------
|
| 576 |
+
def build_app():
|
| 577 |
+
df = unify_columns(load_df())
|
| 578 |
+
# repair acceleration if clearly broken
|
| 579 |
+
if suspicious_zero_to_100(df["zero_to_100_kmh_s"]):
|
| 580 |
+
df["zero_to_100_kmh_s"] = df.apply(estimate_0_100, axis=1)
|
| 581 |
+
|
| 582 |
+
Etext, Enum = ensure_embeddings(df)
|
| 583 |
+
|
| 584 |
+
learned = learned_trims_from_csv(df)
|
| 585 |
+
makes = sorted(df["make"].dropna().astype(str).unique().tolist())
|
| 586 |
+
body_choices = ["Any"] + sorted([b for b in df["body_type"].dropna().astype(str).unique().tolist() if b])
|
| 587 |
+
fuel_choices = ["Any"] + sorted([f for f in df["fuel"].dropna().astype(str).unique().tolist() if f])
|
| 588 |
+
|
| 589 |
+
y_lo, y_hi = int(df["year"].min(skipna=True)), int(df["year"].max(skipna=True))
|
| 590 |
+
p_lo, p_hi = int(df["price_usd"].min(skipna=True)), int(df["price_usd"].max(skipna=True))
|
| 591 |
+
|
| 592 |
+
def models_for(make):
|
| 593 |
+
if not make: return gr.update(choices=[], value=None)
|
| 594 |
+
opts = sorted(df.loc[df["make"].eq(make), "model"].dropna().astype(str).unique().tolist())
|
| 595 |
+
return gr.update(choices=opts, value=None)
|
| 596 |
+
|
| 597 |
+
def trims_years_for(make, model):
|
| 598 |
+
if not make or not model:
|
| 599 |
+
return gr.update(choices=[], value=None), gr.update(choices=[], value=None)
|
| 600 |
+
trims = REAL_WORLD_TRIMS.get((make, model)) or learned.get((make, model)) or ["Base"]
|
| 601 |
+
years = sorted(pd.to_numeric(df[(df["make"]==make)&(df["model"]==model)]["year"], errors="coerce").dropna().astype(int).unique().tolist())
|
| 602 |
+
return gr.update(choices=trims, value=None), gr.update(choices=[None]+years, value=None)
|
| 603 |
+
|
| 604 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="RideSearch") as demo:
|
| 605 |
+
gr.Markdown("## RideSearch — cross‑brand recommendations with realistic trims & photos")
|
| 606 |
|
| 607 |
with gr.Tab("Pick & Recommend"):
|
| 608 |
with gr.Row():
|
| 609 |
+
mk = gr.Dropdown(makes, label="Make")
|
| 610 |
+
md = gr.Dropdown([], label="Model")
|
| 611 |
+
tr = gr.Dropdown([], label="Trim (optional)")
|
| 612 |
+
yr = gr.Dropdown([], label="Year (optional)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
mk.change(models_for, mk, md)
|
| 614 |
+
md.change(lambda a,b: trims_years_for(a,b), [mk, md], [tr, yr])
|
| 615 |
|
| 616 |
with gr.Row():
|
| 617 |
+
body = gr.Dropdown(body_choices, value="Any", label="Body")
|
| 618 |
+
fuel = gr.Dropdown(fuel_choices, value="Any", label="Fuel")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
with gr.Row():
|
| 620 |
+
y_min = gr.Slider(y_lo, y_hi, value=max(y_lo, y_lo), step=1, label="Year min")
|
| 621 |
+
y_max = gr.Slider(y_lo, y_hi, value=y_hi, step=1, label="Year max")
|
|
|
|
| 622 |
with gr.Row():
|
| 623 |
+
p_min = gr.Slider(p_lo, p_hi, value=p_lo, step=500, label="Price min (USD)")
|
| 624 |
+
p_max = gr.Slider(p_lo, p_hi, value=min(p_hi, max(p_lo+15000, p_lo+5000)), step=500, label="Price max (USD)")
|
|
|
|
| 625 |
with gr.Row():
|
| 626 |
+
safety = gr.Slider(3, 5, value=4, step=1, label="Min Safety ★")
|
| 627 |
+
reliab = gr.Slider(55, 99, value=70, step=1, label="Min Reliability")
|
|
|
|
| 628 |
with gr.Row():
|
| 629 |
+
topk = gr.Slider(1, 10, value=5, step=1, label="Recommendations")
|
| 630 |
+
alpha = gr.Slider(0, 1, value=0.7, step=0.05, label="α — Text vs Numeric")
|
|
|
|
| 631 |
with gr.Row():
|
| 632 |
+
cross = gr.Checkbox(True, label="Cross‑brand only")
|
| 633 |
+
xmodel = gr.Checkbox(True, label="Exclude same model family")
|
| 634 |
+
uniqb = gr.Checkbox(True, label="Unique brands (no repeats)")
|
| 635 |
+
|
| 636 |
+
run = gr.Button("Recommend", variant="primary")
|
| 637 |
+
err = gr.Markdown()
|
| 638 |
+
anchor_md = gr.Markdown()
|
| 639 |
+
table = gr.Dataframe(interactive=False, wrap=True, label="Recommendations")
|
| 640 |
+
gallery = gr.Gallery(label="Photos", height=220, columns=[3])
|
| 641 |
+
|
| 642 |
+
def on_click(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o):
|
| 643 |
+
msg, tbl, atext, cards = recommend(
|
| 644 |
+
df, Etext, Enum, a,b,c,d,e,f,int(g),int(h),int(i),int(j),int(k),int(l),
|
| 645 |
+
cross_brand_only=m, exclude_same_model=n, unique_brand=o
|
| 646 |
+
)
|
| 647 |
+
if msg:
|
| 648 |
+
return gr.update(value=f"**{msg}**"), gr.update(value=""), pd.DataFrame(), []
|
| 649 |
+
return gr.update(value=""), gr.update(value=atext), tbl, cards
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
|
| 651 |
+
run.click(on_click,
|
| 652 |
+
[mk, md, tr, yr, topk, alpha, body, fuel, y_min, y_max, p_min, p_max, safety, reliab, cross, xmodel, uniqb],
|
| 653 |
+
[err, anchor_md, table, gallery])
|
| 654 |
|
| 655 |
+
gr.Markdown("Tip: First run may be slower (embeddings build). Next runs are fast.")
|
| 656 |
+
|
| 657 |
+
return demo
|
| 658 |
|
| 659 |
if __name__ == "__main__":
|
| 660 |
+
demo = build_app()
|
| 661 |
demo.queue().launch(server_name="0.0.0.0", server_port=7860)
|