Levimichael4 commited on
Commit
ebb512a
·
verified ·
1 Parent(s): ac022f0

Upload 2 files

Browse files
Files changed (2) hide show
  1. app_new.py +286 -0
  2. trims_map.json +382 -0
app_new.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # app_new.py — RideSearch (brand-correct trims, cross-brand, smart fallbacks, optional photos)
3
+ # Drop into your Hugging Face Space and set as the app file.
4
+
5
+ import os, glob, urllib.parse
6
+ import numpy as np
7
+ import pandas as pd
8
+ from sklearn.metrics.pairwise import cosine_similarity
9
+ from sklearn.preprocessing import StandardScaler
10
+ import gradio as gr
11
+
12
+ def load_df():
13
+ if os.path.exists('RideSearch_dataset.csv'):
14
+ return pd.read_csv('RideSearch_dataset.csv')
15
+ parts = sorted(glob.glob('RideSearch_part*_small.csv'))
16
+ if not parts:
17
+ raise FileNotFoundError("Upload RideSearch_dataset.csv OR the 10 parts RideSearch_part*_small.csv.")
18
+ df = pd.concat([pd.read_csv(p) for p in parts], ignore_index=True)
19
+ df.to_csv('RideSearch_dataset.csv', index=False)
20
+ return df
21
+
22
+ DF = load_df()
23
+
24
+ NUM_COLS = [
25
+ 'horsepower','zero_to_100_kmh_s','seats','cargo_liters','price_usd',
26
+ 'popularity_score','comfort_score','reliability_score','tech_score',
27
+ 'ownership_cost_score','safety_rating'
28
+ ]
29
+
30
+ def ensure_emb():
31
+ txt_ok = os.path.exists('emb_text.npy')
32
+ num_ok = os.path.exists('emb_num.npy')
33
+ if txt_ok and num_ok:
34
+ return np.load('emb_text.npy'), np.load('emb_num.npy')
35
+ from sentence_transformers import SentenceTransformer
36
+ m = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
37
+ texts = DF['text_record'].astype(str).tolist()
38
+ Etext = m.encode(texts, batch_size=256, show_progress_bar=True, normalize_embeddings=True)
39
+ Etext = np.asarray(Etext, dtype='float32'); np.save('emb_text.npy', Etext)
40
+ X = DF[NUM_COLS].copy()
41
+ if 'zero_to_100_kmh_s' in X.columns:
42
+ # lower time is better -> invert so higher is better
43
+ X['zero_to_100_kmh_s'] = -X['zero_to_100_kmh_s'].astype('float32')
44
+ Xs = StandardScaler().fit_transform(X.values.astype('float32'))
45
+ Enum = Xs.astype('float32'); np.save('emb_num.npy', Enum)
46
+ return Etext, Enum
47
+
48
+ # ---- trims mapping (loaded from trims_map.json if present) ----
49
+ import json
50
+ TRIM_CHOICES = {}
51
+ TRIM_ALIAS_TO_GENERIC = {}
52
+ if os.path.exists('trims_map.json'):
53
+ with open('trims_map.json','r',encoding='utf-8') as f:
54
+ data = json.load(f)
55
+ TRIM_CHOICES = {tuple(k.split('||')): v['display'] for k, v in data.items()}
56
+ TRIM_ALIAS_TO_GENERIC = {}
57
+ for k, v in data.items():
58
+ for alias, generic in v['alias_to_generic'].items():
59
+ TRIM_ALIAS_TO_GENERIC[alias] = generic
60
+ else:
61
+ # minimal fallback so the app still runs
62
+ TRIM_CHOICES = {("BMW","3 Series"): ["320i","330i","330e","340i","M3"]}
63
+ TRIM_ALIAS_TO_GENERIC = {"320i":"Base","330i":"Sport","330e":"Sport","340i":"Premium","M3":"Performance"}
64
+
65
+ def generic_to_display(make, model, generic_trim):
66
+ if not generic_trim:
67
+ return ""
68
+ if (make, model) not in TRIM_CHOICES:
69
+ return str(generic_trim)
70
+ for alias in TRIM_CHOICES[(make, model)]:
71
+ if TRIM_ALIAS_TO_GENERIC.get(alias) == generic_trim:
72
+ return alias
73
+ return str(generic_trim)
74
+
75
+ def models_for(make):
76
+ if not make:
77
+ return gr.update(choices=[], value=None)
78
+ opts = sorted(DF.loc[DF['make'].eq(make), 'model'].dropna().unique().tolist())
79
+ return gr.update(choices=opts, value=None)
80
+
81
+ def trim_year(make, model):
82
+ if make and model and (make, model) in TRIM_CHOICES:
83
+ trims = TRIM_CHOICES[(make, model)]
84
+ else:
85
+ sub = DF
86
+ if make: sub = sub[sub['make'] == make]
87
+ if model: sub = sub[sub['model'] == model]
88
+ trims = sorted(sub['trim'].astype(str).dropna().unique().tolist())[:20]
89
+ if make and model:
90
+ years = sorted(
91
+ DF.loc[(DF['make'].eq(make)) & (DF['model'].eq(model)), 'year']
92
+ .dropna().astype(int).unique().tolist()
93
+ )
94
+ else:
95
+ years = []
96
+ return trims, years
97
+
98
+ def on_model_change(make, model):
99
+ trims, years = trim_year(make, model)
100
+ return gr.update(choices=trims, value=None), gr.update(choices=years, value=None)
101
+
102
+ def apply_filters(df, body, fuel, y_min, y_max, p_min, p_max, safety, reliab):
103
+ out = df.copy()
104
+ if body != 'Any': out = out[out['body_type'] == body]
105
+ if fuel != 'Any': out = out[out['fuel'] == fuel]
106
+ out = out[(out['year'] >= y_min) & (out['year'] <= y_max)]
107
+ out = out[(out['price_usd'] >= p_min) & (out['price_usd'] <= p_max)]
108
+ out = out[(out['safety_rating'] >= safety) & (out['reliability_score'] >= reliab)]
109
+ return out
110
+
111
+ def placeholder_svg_data_uri(title):
112
+ svg = f\"\"\"<svg xmlns='http://www.w3.org/2000/svg' width='480' height='320'>
113
+ <rect width='100%' height='100%' fill='#e8eef7'/>
114
+ <text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle'
115
+ font-family='Arial' font-size='26' fill='#223'>
116
+ {title}
117
+ </text>
118
+ </svg>\"\"\"
119
+ return "data:image/svg+xml;utf8," + urllib.parse.quote(svg)
120
+
121
+ def build_gallery_html(df_rows):
122
+ cards = []
123
+ for _, r in df_rows.iterrows():
124
+ label = f"{r['make']} {r['model']} {generic_to_display(r['make'], r['model'], r['trim'])}"
125
+ img_src = ""
126
+ if 'image_url' in r and isinstance(r['image_url'], str) and r['image_url'].strip():
127
+ img_src = r['image_url'].strip()
128
+ else:
129
+ img_src = placeholder_svg_data_uri(f"{r['make']} {r['model']}")
130
+ cards.append(f\"\"\"
131
+ <div style="width:240px;margin:6px;border:1px solid #ddd;border-radius:12px;overflow:hidden;background:#fff;">
132
+ <img src="{img_src}" style="width:240px;height:160px;object-fit:cover;display:block" />
133
+ <div style="padding:8px 10px;font:14px/1.3 Arial,sans-serif;color:#111">{label}</div>
134
+ </div>
135
+ \"\"\")
136
+ return f"<div style='display:flex;flex-wrap:wrap'>{''.join(cards)}</div>"
137
+
138
+ def find_anchor(make, model, trim_display, year):
139
+ # Map display trim to dataset generic using alias mapping if present
140
+ def norm(make, model, t):
141
+ if not t: return None
142
+ # if the alias exists globally, use its generic
143
+ return TRIM_ALIAS_TO_GENERIC.get(t, t)
144
+
145
+ trim_generic = norm(make, model, trim_display)
146
+
147
+ sub = DF.copy()
148
+ if make: sub = sub[sub['make'] == make]
149
+ if model: sub = sub[sub['model'] == model]
150
+
151
+ def pick(df_):
152
+ if df_.empty: return None
153
+ return df_.sort_values('popularity_score', ascending=False).iloc[0]
154
+
155
+ exact = sub.copy()
156
+ if trim_generic: exact = exact[exact['trim'] == trim_generic]
157
+ if year: exact = exact[exact['year'] == year]
158
+ if not exact.empty:
159
+ return pick(exact)
160
+
161
+ if year:
162
+ y_only = sub[sub['year'] == year]
163
+ if not y_only.empty: return pick(y_only)
164
+
165
+ if trim_generic:
166
+ t_only = sub[sub['trim'] == trim_generic]
167
+ if not t_only.empty: return pick(t_only)
168
+
169
+ return pick(sub)
170
+
171
+ def recommend(make, model, trim_display, year, topk, alpha,
172
+ body, fuel, y_min, y_max, p_min, p_max, safety, reliab,
173
+ cross_brand_only=True, exclude_same_model=True):
174
+
175
+ a = find_anchor(make, model, trim_display, year)
176
+ if a is None:
177
+ return "No match for that combo.", None, "", None
178
+
179
+ pool = DF.copy()
180
+ if cross_brand_only:
181
+ pool = pool[pool['make'] != a['make']]
182
+ if exclude_same_model:
183
+ pool = pool[~((pool['make'] == a['make']) & (pool['model'] == a['model']))]
184
+
185
+ pool = apply_filters(pool, body, fuel, int(y_min), int(y_max), int(p_min), int(p_max), int(safety), int(reliab))
186
+ if pool.empty:
187
+ return "No cars after filters. Try widening year/price/safety.", None, "", None
188
+
189
+ Etext, Enum = ensure_emb()
190
+ idx_anchor = int(a.name)
191
+ cand_idx = pool.index.values
192
+
193
+ st = cosine_similarity(Etext[idx_anchor:idx_anchor+1], Etext[cand_idx])[0]
194
+ sn = cosine_similarity(Enum[idx_anchor:idx_anchor+1], Enum[cand_idx])[0]
195
+ s = float(alpha)*st + (1-float(alpha))*sn
196
+
197
+ order = np.argsort(-s)
198
+ seen = set(); chosen = []
199
+ for j in order:
200
+ r = DF.loc[cand_idx[j]]
201
+ key = (r['make'], r['model'])
202
+ if key in seen: continue # enforce cross-brand & unique model
203
+ seen.add(key)
204
+ chosen.append(cand_idx[j])
205
+ if len(chosen) >= int(topk): break
206
+
207
+ if not chosen:
208
+ return "No recommendations found after constraints.", None, "", None
209
+
210
+ sel = DF.loc[chosen].copy()
211
+ sel['trim_display'] = sel.apply(lambda r: generic_to_display(r['make'], r['model'], r['trim']), axis=1)
212
+
213
+ sim_lookup = {cand_idx[j]: round(float(s[j])*100, 1) for j in order}
214
+ sel['similarity_%'] = sel.index.map(lambda k: sim_lookup.get(k, 0.0))
215
+
216
+ cols = ['name','make','model','trim_display','year','body_type','fuel','engine_type',
217
+ 'price_usd','horsepower','zero_to_100_kmh_s','popularity_score','comfort_score',
218
+ 'reliability_score','tech_score','ownership_cost_score','safety_rating','similarity_%']
219
+
220
+ anchor_text = (f"**{a['make']} {a['model']} {generic_to_display(a['make'], a['model'], a['trim'])} "
221
+ f"{int(a['year'])}** \\n"
222
+ f"Body: {a['body_type']} • Fuel: {a['fuel']} • Engine: {a['engine_type']} \\n"
223
+ f"HP: {int(a['horsepower'])} • 0–100: {a['zero_to_100_kmh_s']}s • Price: ${int(a['price_usd']):,} \\n"
224
+ f"Popularity {int(a['popularity_score'])}/10 • Comfort {int(a['comfort_score'])}/10 • "
225
+ f"Reliability {int(a['reliability_score'])}/100 • Safety {int(a['safety_rating'])}★")
226
+
227
+ note = (f"α = {float(alpha):.2f} (text ↔ numeric) • Cross-brand only = {cross_brand_only} "
228
+ f"• Exclude same model = {exclude_same_model}")
229
+
230
+ gallery = build_gallery_html(sel)
231
+ return anchor_text, sel[cols], note, gallery
232
+
233
+ def build_ui():
234
+ y_lo, y_hi = int(DF['year'].min()), int(DF['year'].max())
235
+ p_lo, p_hi = int(DF['price_usd'].min()), int(DF['price_usd'].max())
236
+
237
+ with gr.Blocks() as demo:
238
+ gr.Markdown("# RideSearch — cross-brand recommendations with real trims")
239
+
240
+ with gr.Tab("Pick & Recommend"):
241
+ with gr.Row():
242
+ mk = gr.Dropdown(sorted(DF['make'].dropna().unique().tolist()), label="Make", value=None)
243
+ md = gr.Dropdown([], label="Model", value=None)
244
+ tr = gr.Dropdown([], label="Trim (optional)", value=None)
245
+ yr = gr.Dropdown([], label="Year (optional)", value=None)
246
+ mk.change(models_for, mk, md)
247
+ md.change(lambda a,b: on_model_change(a,b), [mk, md], [tr, yr])
248
+
249
+ with gr.Row():
250
+ body = gr.Dropdown(['Any'] + sorted(DF['body_type'].dropna().unique().tolist()), value='Any', label='Body')
251
+ fuel = gr.Dropdown(['Any'] + sorted(DF['fuel'].dropna().unique().tolist()), value='Any', label='Fuel')
252
+ with gr.Row():
253
+ y_min = gr.Slider(y_lo, y_hi, value=y_lo, step=1, label='Year min')
254
+ y_max = gr.Slider(y_lo, y_hi, value=y_hi, step=1, label='Year max')
255
+ with gr.Row():
256
+ p_min = gr.Slider(p_lo, p_hi, value=p_lo, step=500, label='Price min (USD)')
257
+ p_max = gr.Slider(p_lo, p_hi, value=min(p_hi, 80000), step=500, label='Price max (USD)')
258
+ with gr.Row():
259
+ safety = gr.Slider(3, 5, value=4, step=1, label='Min Safety ★')
260
+ reliab = gr.Slider(55, 99, value=70, step=1, label='Min Reliability')
261
+ with gr.Row():
262
+ topk = gr.Slider(1, 10, value=5, step=1, label='Recommendations')
263
+ alpha = gr.Slider(0, 1, value=0.7, step=0.05, label='α — Text vs Numeric')
264
+ with gr.Row():
265
+ cross = gr.Checkbox(label="Cross-brand only", value=True)
266
+ xmodel = gr.Checkbox(label="Exclude same model family", value=True)
267
+
268
+ go = gr.Button("Recommend")
269
+ anchor_md = gr.Markdown()
270
+ table = gr.Dataframe(interactive=False)
271
+ note = gr.Markdown()
272
+ gallery = gr.HTML()
273
+
274
+ go.click(
275
+ recommend,
276
+ [mk, md, tr, yr, topk, alpha, body, fuel, y_min, y_max, p_min, p_max, safety, reliab, cross, xmodel],
277
+ [anchor_md, table, note, gallery]
278
+ )
279
+ gr.Markdown("Tip: Add an 'image_url' column in the CSV for real photos.")
280
+
281
+ return demo
282
+
283
+ demo = build_ui()
284
+
285
+ if __name__ == "__main__":
286
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860)
trims_map.json ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "BMW||3 Series": {
3
+ "display": [
4
+ "318i",
5
+ "320i",
6
+ "330i",
7
+ "330e",
8
+ "340i",
9
+ "M3"
10
+ ],
11
+ "alias_to_generic": {
12
+ "318i": "Base",
13
+ "320i": "Base",
14
+ "330i": "Sport",
15
+ "330e": "Sport",
16
+ "340i": "Premium",
17
+ "M3": "Performance"
18
+ }
19
+ },
20
+ "Audi||A3": {
21
+ "display": [
22
+ "30 TFSI",
23
+ "35 TFSI",
24
+ "40 TFSI",
25
+ "45 TFSI",
26
+ "S3",
27
+ "RS3"
28
+ ],
29
+ "alias_to_generic": {
30
+ "30 TFSI": "Base",
31
+ "35 TFSI": "Base",
32
+ "40 TFSI": "Sport",
33
+ "45 TFSI": "Premium",
34
+ "S3": "Performance",
35
+ "RS3": "Performance"
36
+ }
37
+ },
38
+ "Audi||A4": {
39
+ "display": [
40
+ "35 TFSI",
41
+ "40 TFSI",
42
+ "45 TFSI",
43
+ "S4",
44
+ "RS4"
45
+ ],
46
+ "alias_to_generic": {
47
+ "35 TFSI": "Base",
48
+ "40 TFSI": "Sport",
49
+ "45 TFSI": "Premium",
50
+ "S4": "Performance",
51
+ "RS4": "Performance"
52
+ }
53
+ },
54
+ "Mercedes-Benz||C-Class": {
55
+ "display": [
56
+ "C180",
57
+ "C200",
58
+ "C220d",
59
+ "C300",
60
+ "AMG C43",
61
+ "AMG C63"
62
+ ],
63
+ "alias_to_generic": {
64
+ "C180": "Base",
65
+ "C200": "Base",
66
+ "C220d": "Base",
67
+ "C300": "Premium",
68
+ "AMG C43": "Performance",
69
+ "AMG C63": "Performance"
70
+ }
71
+ },
72
+ "Lexus||IS": {
73
+ "display": [
74
+ "IS 300",
75
+ "IS 350",
76
+ "IS 500 F SPORT"
77
+ ],
78
+ "alias_to_generic": {
79
+ "IS 300": "Base",
80
+ "IS 350": "Premium",
81
+ "IS 500 F SPORT": "Performance"
82
+ }
83
+ },
84
+ "Toyota||Corolla": {
85
+ "display": [
86
+ "L",
87
+ "LE",
88
+ "SE",
89
+ "XSE",
90
+ "GR"
91
+ ],
92
+ "alias_to_generic": {
93
+ "L": "Base",
94
+ "LE": "Base",
95
+ "SE": "Sport",
96
+ "XSE": "Premium",
97
+ "GR": "Performance"
98
+ }
99
+ },
100
+ "Mini||Cooper": {
101
+ "display": [
102
+ "Classic",
103
+ "Signature",
104
+ "Iconic",
105
+ "John Cooper Works"
106
+ ],
107
+ "alias_to_generic": {
108
+ "Classic": "Base",
109
+ "Signature": "Premium",
110
+ "Iconic": "Premium",
111
+ "John Cooper Works": "Performance"
112
+ }
113
+ },
114
+ "Jeep||Wrangler": {
115
+ "display": [
116
+ "Sport",
117
+ "Willys",
118
+ "Sahara",
119
+ "Rubicon",
120
+ "392"
121
+ ],
122
+ "alias_to_generic": {
123
+ "Sport": "Base",
124
+ "Willys": "Sport",
125
+ "Sahara": "Premium",
126
+ "Rubicon": "Performance",
127
+ "392": "Performance"
128
+ }
129
+ },
130
+ "Kia||Sportage": {
131
+ "display": [
132
+ "LX",
133
+ "EX",
134
+ "SX",
135
+ "X-Line",
136
+ "X-Pro"
137
+ ],
138
+ "alias_to_generic": {
139
+ "LX": "Base",
140
+ "EX": "Premium",
141
+ "SX": "Premium",
142
+ "X-Line": "Sport",
143
+ "X-Pro": "Performance"
144
+ }
145
+ },
146
+ "Land Rover||Range Rover Evoque": {
147
+ "display": [
148
+ "S",
149
+ "SE",
150
+ "R-Dynamic S",
151
+ "R-Dynamic SE",
152
+ "Autobiography"
153
+ ],
154
+ "alias_to_generic": {
155
+ "S": "Base",
156
+ "SE": "Premium",
157
+ "R-Dynamic S": "Sport",
158
+ "R-Dynamic SE": "Premium",
159
+ "Autobiography": "Premium"
160
+ }
161
+ },
162
+ "Lexus||RX": {
163
+ "display": [
164
+ "RX 350",
165
+ "RX 350h",
166
+ "RX 500h F SPORT"
167
+ ],
168
+ "alias_to_generic": {
169
+ "RX 350": "Premium",
170
+ "RX 350h": "Premium",
171
+ "RX 500h F SPORT": "Performance"
172
+ }
173
+ },
174
+ "Mazda||Mazda3": {
175
+ "display": [
176
+ "S",
177
+ "Select",
178
+ "Preferred",
179
+ "Premium",
180
+ "Turbo"
181
+ ],
182
+ "alias_to_generic": {
183
+ "S": "Base",
184
+ "Select": "Base",
185
+ "Preferred": "Premium",
186
+ "Premium": "Premium",
187
+ "Turbo": "Performance"
188
+ }
189
+ },
190
+ "Mitsubishi||Outlander": {
191
+ "display": [
192
+ "ES",
193
+ "SE",
194
+ "SEL",
195
+ "Black Edition",
196
+ "PHEV"
197
+ ],
198
+ "alias_to_generic": {
199
+ "ES": "Base",
200
+ "SE": "Sport",
201
+ "SEL": "Premium",
202
+ "Black Edition": "Premium",
203
+ "PHEV": "Premium"
204
+ }
205
+ },
206
+ "Nissan||X-Trail": {
207
+ "display": [
208
+ "Visia",
209
+ "Acenta",
210
+ "N-Connecta",
211
+ "Tekna",
212
+ "Tekna+"
213
+ ],
214
+ "alias_to_generic": {
215
+ "Visia": "Base",
216
+ "Acenta": "Base",
217
+ "N-Connecta": "Premium",
218
+ "Tekna": "Premium",
219
+ "Tekna+": "Premium"
220
+ }
221
+ },
222
+ "Peugeot||3008": {
223
+ "display": [
224
+ "Active",
225
+ "Allure",
226
+ "GT",
227
+ "GT Pack"
228
+ ],
229
+ "alias_to_generic": {
230
+ "Active": "Base",
231
+ "Allure": "Premium",
232
+ "GT": "Premium",
233
+ "GT Pack": "Premium"
234
+ }
235
+ },
236
+ "Porsche||911": {
237
+ "display": [
238
+ "Carrera",
239
+ "Carrera S",
240
+ "GTS",
241
+ "Turbo",
242
+ "GT3"
243
+ ],
244
+ "alias_to_generic": {
245
+ "Carrera": "Base",
246
+ "Carrera S": "Premium",
247
+ "GTS": "Premium",
248
+ "Turbo": "Performance",
249
+ "GT3": "Performance"
250
+ }
251
+ },
252
+ "Ram||1500": {
253
+ "display": [
254
+ "Tradesman",
255
+ "Big Horn",
256
+ "Laramie",
257
+ "Rebel",
258
+ "Limited"
259
+ ],
260
+ "alias_to_generic": {
261
+ "Tradesman": "Base",
262
+ "Big Horn": "Sport",
263
+ "Laramie": "Premium",
264
+ "Rebel": "Sport",
265
+ "Limited": "Premium"
266
+ }
267
+ },
268
+ "Renault||Clio": {
269
+ "display": [
270
+ "Authentique",
271
+ "Expression",
272
+ "Dynamique",
273
+ "RS Line"
274
+ ],
275
+ "alias_to_generic": {
276
+ "Authentique": "Base",
277
+ "Expression": "Sport",
278
+ "Dynamique": "Premium",
279
+ "RS Line": "Performance"
280
+ }
281
+ },
282
+ "Seat||Leon": {
283
+ "display": [
284
+ "Reference",
285
+ "Style",
286
+ "FR",
287
+ "Cupra"
288
+ ],
289
+ "alias_to_generic": {
290
+ "Reference": "Base",
291
+ "Style": "Sport",
292
+ "FR": "Sport",
293
+ "Cupra": "Performance"
294
+ }
295
+ },
296
+ "Skoda||Octavia": {
297
+ "display": [
298
+ "Active",
299
+ "Ambition",
300
+ "Style",
301
+ "RS"
302
+ ],
303
+ "alias_to_generic": {
304
+ "Active": "Base",
305
+ "Ambition": "Sport",
306
+ "Style": "Premium",
307
+ "RS": "Performance"
308
+ }
309
+ },
310
+ "Subaru||Outback": {
311
+ "display": [
312
+ "Base",
313
+ "Premium",
314
+ "Limited",
315
+ "Wilderness",
316
+ "Touring"
317
+ ],
318
+ "alias_to_generic": {
319
+ "Base": "Base",
320
+ "Premium": "Premium",
321
+ "Limited": "Premium",
322
+ "Wilderness": "Sport",
323
+ "Touring": "Premium"
324
+ }
325
+ },
326
+ "Tesla||Model 3": {
327
+ "display": [
328
+ "RWD",
329
+ "Long Range",
330
+ "Performance"
331
+ ],
332
+ "alias_to_generic": {
333
+ "RWD": "Base",
334
+ "Long Range": "Premium",
335
+ "Performance": "Performance"
336
+ }
337
+ },
338
+ "Volkswagen||Golf": {
339
+ "display": [
340
+ "Trendline",
341
+ "Comfortline",
342
+ "Highline",
343
+ "GTI",
344
+ "R"
345
+ ],
346
+ "alias_to_generic": {
347
+ "Trendline": "Base",
348
+ "Comfortline": "Base",
349
+ "Highline": "Premium",
350
+ "GTI": "Performance",
351
+ "R": "Performance"
352
+ }
353
+ },
354
+ "Volkswagen||Tiguan": {
355
+ "display": [
356
+ "S",
357
+ "SE",
358
+ "SEL",
359
+ "R-Line"
360
+ ],
361
+ "alias_to_generic": {
362
+ "S": "Base",
363
+ "SE": "Sport",
364
+ "SEL": "Premium",
365
+ "R-Line": "Performance"
366
+ }
367
+ },
368
+ "Volvo||XC60": {
369
+ "display": [
370
+ "Core",
371
+ "Plus",
372
+ "Ultimate",
373
+ "Polestar Engineered"
374
+ ],
375
+ "alias_to_generic": {
376
+ "Core": "Base",
377
+ "Plus": "Premium",
378
+ "Ultimate": "Premium",
379
+ "Polestar Engineered": "Performance"
380
+ }
381
+ }
382
+ }