import gradio as gr import joblib import pandas as pd import numpy as np from geopy.distance import geodesic import re from sklearn.preprocessing import MultiLabelBinarizer # Примерные данные для выпадающих списков OPTIONS = { # 'equipment': ['Не указано', 'Dynamic', 'Hit', 'Luxury', 'Burner', 'Invite+', 'Tiptronic', 'Value', 'Flagship', 'Kinetic', 'LTZ2V', 'Prime', 'LT+', 'Cayenne', 'GO!', 'XV', 'Feel', 'Премиум', 'Норма', '21144-40-022', 'Gt-Line', 'Sportline', 'Nomade', '21144-40-021', 'Trophy', 'Premium', 'Спорт', 'Image', 'Sport&Style', 'Терра', 'Базовая', 'XTR', 'Play', 'Basis', 'Design', 'Laurin&Klement', 'Premier', 'SL3', '2M', 'LTD', 'WELL', 'GTI', 'Siv', 'MT3', 'NR', 'Tg-Fl14C', '360', 'Spike', 'Technology', 'Match', "Luxe'24", 'GLX', "Comfort'22", 'Classic’22', 'ES', 'WaY', 'Essentia', 'Avantgarde', 'Tendance', "[BLACK]'22", 'AT7', 'Elegancе', 'Active+', 'Original', 'Vr14C', 'Individual', 'LTZ2F', 'Allstar', 'Lux', 'Travel', 'Hi-Tech', 'Kombi', 'GLE', 'Tg12C', 'Excalibur', 'SVAUTOBIOGRAPHY', 'Tekna', "Comfort'24+Мультимедиа", 'CUP', 'Луна', 'Comfort', 'In14C', 'LT', 'Family', '2.0R', 'GT', 'SE+', 'МТ3', 'PROSAFETY', 'DX', 'MPS', 'First', 'MT1', 'Vogue', 'Black&Brown', '23490-A7-452', 'Respect', 'SDX', 'LTZM', 'Shogun', 'LE', 'Sensory', 'Trust', 'Nismo', 'Экспедиция', 'GXR', '2.0L', 'Avenue', 'Calligraphy', 'Essential', 'Instinct', "Life'24", 'Life', 'Sportium', 'Classique', 'Instyle', 'Sport', 'Трофи', 'Drive', 'TRD', '21104-82', 'L&K', 'Summum', 'Voyage', 'LS', 'XE', 'LEM', 'Jet', 'Limited', 'Bluef.', 'PanAmericana', "Classic'22", 'Autobiography', 'Performance', 'Multispace', 'MID', 'SV', 'SUV', 'Луна+', 'JLX-A', 'AT8', 'Trek', 'Unlimited', 'EX', 'Elite', 'Intense+', 'Comfort+', 'Supercharged', "Comfort'24", 'Pulse', 'VTR+', 'Competition', 'Tech', 'Ghia', 'CX', 'Trailhawk', 'YOU', 'Silverline', 'Confort', '#Club', 'Sochi', 'Standart', 'Core', 'Star', 'R-Design', "[BLACK]'24", 'Suriken', 'Premuim', 'ST', 'WRX', 'Startline', 'MT2', 'Комфорт', 'GR', 'Extreme', 'Air', 'President', 'Rubicon', 'ELEGANCE', 'Black', 'Impulse', '21061', 'Base', 'Optimum', 'Tg12Lx', "KHL'24", 'Touring', 'S', 'Hi-Tech+', 'Cooper', '21144-30-012', 'BM', 'SV2', 'Entry', 'PE+', 'Люкс', 'Status', 'Trend&Fun', 'D-Sign', 'A', 'Elegant', 'GL-X', 'Trendline', 'Максимум', 'Diva', 'Edition25', 'Comfort+Navi', 'Atacama', 'Inform', 'Progressive', 'Fun', 'HIGH', 'Macan', 'Club', 'Mid', 'AT4', "Enjoy'24", 'Cosmo', '#Club+Multimedia', 'xLine', 'JLX', 'BlueEFFICIENCY', 'Joy', "Luxe'22", 'Urban', '[BLACK]', 'Top', 'MX', 'Premium+SV', 'XNN', 'Prestige+', "#Club'23", 'Optima', 'Scout', 'XS', 'Euro2012', 'Promo', 'CLASSIC', 'Triumph', 'Shiro', 'Feline', 'Km12C', 'SiR', 'DE', 'Edition', 'GL', "#Club'24", 'Luna', 'Grande', 'Би-2', 'XR', 'Laredo', 'Ms14Lx', 'City', 'Dreamline', 'High+', 'STD', 'Bluetec', 'Expedition', 'Titanium', 'Лимитед', 'Hi-tech', '21144-22-010', 'Noblesse', 'Vr14Lx', 'Primary', 'High-Tech', 'Allure', 'Exclusive', 'Expression', 'Active', 'Elbrus', "#CLUB'22", 'LTZ3V', 'Diesel', 'F', 'Элеганс', 'LX', 'YV', 'In14B', 'Standard', 'SX', 'Cup', 'L', 'R-line', 'Techno', 'PE', 'HL3', "Comfort'23", 'JLX-E', 'G', '21065', 'S-Limited', '23490-A7-450', 'CS', 'XSE', 'LE-R', 'Lite', 'AVANTGARDE', 'LTZ', 'Action', 'Best', 'Start', 'Passion', 'R-Line', 'Fresh', "Techno'24", 'Ultimate', 'Inspire', 'Adventure', 'Authentique', 'Modern', '23490-A1-011', 'Ambition', "Classic'24", 'Westminster', 'AT5', 'Connect', 'SE', 'M2', 'GLS', 'Noire', 'TOP', 'Trend', 'Anniversary', 'Supreme+', 'Intense', 'X-Line', 'JX-E', 'Luxe', 'Direct', "Classic'23", 'Enjoy', 'Tg-Fl14Lx', 'TOP+', 'Классик', 'Advanced', 'Lounge', 'Prestige', 'Pro', '21144-20-010', 'Executive+', 'Inscription', 'Basic', 'Experience', 'KHL', 'Collection', 'Outdoor', 'Advance', 'Luxe+', 'Km14C', 'Kasten', 'Tg13C', 'Tg13Lx', 'SL', 'Deluxe', 'Access', 'M', 'X', "Urban'24", 'Trend+', 'SXT', 'Fleet', 'In12C', 'Style', 'Lifestyle', 'Business', 'Tour', 'Plus', 'Стандарт', 'Оптима', 'Sahara', 'Luxury+', 'Welcome', 'XT', 'FIFA', 'Bn12C', 'Авангард', 'Reference', 'SiV', 'Luxury+Four', 'Invite', 'Select', 'Luxury+Navi', 'Momentum', 'SE+Perso', 'Ambiente', 'HSE', 'Portfolio', 'JLX-EL', 'LC', 'Premium+', 'DLX', 'Attraction', 'Oxygo', '3D', 'Dynamique', 'Hightech', 'Royal', 'MT5', 'TRX', 'RS', 'SS', 'CrossCaddy', 'MT', 'BX', 'Classic', 'Utility', 'CDX', 'Elegance', 'Norma', 'NAV', "Quest'22", 'Престиж', 'Сол', 'Executive', 'GLCM', 'VF', 'Offroad', 'Юбилейный', 'Privilege', 'Comfortline', 'Tg-Fl13C', 'Bn14C', 'Platinum', 'Trendy', 'S-Edition', 'S/C', 'Conceptline', 'Quest', 'Flagship+', 'High', 'Track&Field', 'Overland', 'Way', 'GT-Line', 'XLT', 'Murano', 'Столица', 'Elegance+', 'Blueef', 'Comfortable', 'HVX', 'VTR', 'Medalist', 'Supreme', 'Экспедиционный', 'AUTOBIOGRAPHY', 'Origin', 'Highline', 'Energy', 'LE+', '6AB', 'Live', 'MT6', 'Max', 'Velour', 'Xline', 'Elegance+Four', 'GLC'], 'body_type': ['Фургон', 'Седан', 'Пикап', 'Внедорожник', 'Кабриолет', 'Лифтбек', 'Минивэн', 'Универсал', 'Микроавтобус', 'Купе', 'Хетчбэк'], 'drive_type': ['Передний', 'Задний', 'Полный'], 'engine_type': ['Электро', 'Бензин', 'Гибрид', 'Дизель', 'Газ'], 'doors_number': ['2', '3', '4', '5'], 'color': ['Белый', 'Голубой', 'Фиолетовый', 'Пурпурный', 'Коричневый', 'Серебряный', 'Зелёный', 'Серый', 'Жёлтый', 'Золотой', 'Оранжевый', 'Бежевый', 'Бордовый', 'Розовый', 'Чёрный', 'Синий', 'Красный'], 'pts': ['Не указано', 'Дубликат', 'Электронный', 'Оригинал'], 'audiosistema': ['Не указано', '4 колонки', '2 колонки', '6 колонок', '8+ колонок'], 'diski': ['Не указано', '24"', '20"', '30"', '12"', '13"', '17"', '19"', '14"', '27"', '25"', '21"', '15"', '29"', '7"', '23"', '8"', '28"', '18"', '10"', '22"', '11"', '26"', '16"'], 'electropodemniki': ['Не указано', 'Передние и задние', 'Только передние'], 'fary': ['Не указано', 'Светодиодные', 'Галогенные', 'Ксеноновые'], 'salon': ['Не указано', 'Комбинированный', 'Кожа', 'Велюр', 'Ткань'], 'upravlenie_klimatom': ['Не указано', 'Кондиционер', 'Климат-контроль двухзонный', 'Климат-контроль однозонный'], 'usilitel_rul': ['Не указано', 'электро-', 'электрогидро-'], 'steering_wheel': ['Левый', 'Правый'], 'crashes_count': ['0', '1', '2', '3+'], 'owners_count': ['1', '2', '3', '4+'] } custom_css = """ /* Настройки выпадающих списков */ .dropdown-menu, .select-wrap, ul[role="listbox"] { background-color: white !important; opacity: 1 !important; z-index: 9999 !important; border: 1px solid #000000 !important; box-shadow: 0px 10px 20px rgba(0,0,0,0.2) !important; } .item, li[role="option"] { background-color: white !important; color: black !important; border-bottom: 1px solid #eee; } .item:hover, li[role="option"]:hover { background-color: #f0f0f0 !important; color: #000 !important; } input[role="combobox"] { background-color: white !important; color: black !important; } /* Настройки слайдера (Черный стиль) */ :root { --slider-color: #000000 !important; } input[type="range"]::-webkit-slider-thumb { background-color: black !important; border-color: black !important; } /* Рамка числового ввода у слайдера */ input[type="number"] { border: 1px solid #000000 !important; } """ # --- Пользовательский трансформер для multi-label (нужен для загрузки препроцессора) --- class MultiLabelBinarizerTransformer: def __init__(self): self.mlb = MultiLabelBinarizer(sparse_output=False) self.fitted = False def _clean_input(self, x): if x is None or (isinstance(x, float) and np.isnan(x)): return [] if isinstance(x, (list, np.ndarray)): cleaned = [str(i) for i in x if i is not None and not (isinstance(i, float) and np.isnan(i))] return cleaned return [str(x)] def fit(self, X, y=None): try: lists = X.iloc[:, 0].apply(self._clean_input) self.mlb.fit(lists) self.fitted = True except Exception as e: print(f"Ошибка в MultiLabelBinarizer: {e}. Пропускаем колонку.") self.fitted = False return self def transform(self, X): if not self.fitted: return np.zeros((len(X), 1)) lists = X.iloc[:, 0].apply(self._clean_input) return self.mlb.transform(lists) def get_feature_names_out(self, input_features=None): return self.mlb.classes_ if self.fitted else ['dummy'] def predict(*args): yield "Идёт рассчёт стоимости..." keys = [ "body_type", "drive_type", "engine_type", "color", "pts", "audiosistema", "diski", "fary", "salon", "upravlenie_klimatom", "steering_wheel", "crashes_count", "owners_count", "production_year", "mileage", "doors_number", "usilitel_rul", "electropodemniki" ]#"equipment", mult_to_none = ["equipment", 'protivoygonnaya_sistema_mult', 'multimedia_navigacia_mult', 'audiosistema_mult', 'pomosh_pri_vozhdenii_mult', 'salon_mult', 'obogrev_mult', 'aktivnaya_bezopasnost_mult', 'upravlenie_klimatom_mult', 'pamyat_nastroek_mult', 'shini_i_diski_mult', 'fary_mult', 'electroprivod_mult', 'podushki_bezopasnosti_mult'] # Создаем словарь: если значение "Не указано", заменяем на None inputs = { key: (val if val != "Не указано" else None) for key, val in zip(keys+mult_to_none, list(args)+[None]*len(mult_to_none)) } new_df = pd.DataFrame([inputs]) # --- Функция расстояния до Москвы --- def distance_to_moscow(lat=58.59, lon=49.66): moscow_coords = (55.7558, 37.6173) if pd.isna(lat) or pd.isna(lon): return np.nan return geodesic((lat, lon), moscow_coords).km # --- Очистка числовых колонок --- def clean_numeric_cols(df, cols_to_clean): for col in cols_to_clean: if col in df.columns: df[col] = df[col].astype(str).replace({'3+': '3', '2+': '2', 'nan': np.nan}) df[col] = pd.to_numeric(df[col], errors='coerce') return df # --- Дополнительный feature engineering --- def advanced_feature_engineering(df): # Преобразуем owners_count в числовой тип if 'owners_count' in df.columns: df['owners_count'] = pd.to_numeric(df['owners_count'], errors='coerce').fillna(0).astype(int) # Преобразуем mileage в числовой тип if 'mileage' in df.columns: df['mileage'] = pd.to_numeric(df['mileage'], errors='coerce').fillna(0) # Возраст авто current_year = 2026 if 'production_year' in df.columns: df['age'] = current_year - df['production_year'] df = df.drop(columns=['production_year']) # Обработка close_date if 'close_date' in df.columns: df['close_date'] = pd.to_datetime(df['close_date'], errors='coerce') df['close_year'] = df['close_date'].dt.year.fillna(current_year) df['close_month'] = df['close_date'].dt.month.fillna(1) df = df.drop(columns=['close_date']) # mileage_per_owner if 'mileage' in df.columns and 'owners_count' in df.columns: df['mileage_per_owner'] = np.where(df['owners_count'] > 0, df['mileage'] / df['owners_count'], 0) return df # --- Загрузка модели и препроцессора --- preprocessor = joblib.load('car_price_preprocessor.pkl') model = joblib.load('stacking_car_price_model.pkl') # --- Применение feature engineering --- new_df = advanced_feature_engineering(new_df) # --- Добавление пространственных признаков --- new_df['dist_to_moscow'] = new_df.apply(lambda row: distance_to_moscow(), axis=1) if 'mileage' in new_df.columns: new_df['mileage_log'] = np.log1p(new_df['mileage']) new_df = new_df.drop(columns=['mileage']) # --- Очистка числовых колонок --- cols_to_clean = ['owners_count', 'crashes_count'] new_df = clean_numeric_cols(new_df, cols_to_clean) # --- Биннинг редких категорий (если применимо) --- single_cat_cols = [col for col in [ 'body_type', 'drive_type', 'engine_type', 'doors_number', 'color', 'pts', 'steering_wheel', 'audiosistema', 'diski', 'electropodemniki', 'fary', 'salon', 'upravlenie_klimatom', 'usilitel_rul', 'owners_count', 'crashes_count' ] if col in new_df.columns] # --- Трансформация данных с помощью препроцессора --- new_data_processed = preprocessor.transform(new_df) # --- Предсказания --- predictions_log = model.predict(new_data_processed) # --- Обратное преобразование в оригинальную шкалу (цены) --- predictions = np.expm1(predictions_log) yield f"{round(float(predictions[0]))} рублей" with gr.Blocks() as demo: gr.Markdown("# 🚗 Калькулятор стоимости авто") with gr.Row(): with gr.Column(): # equipment = gr.Dropdown( # choices=OPTIONS['equipment'], # label="⚙️ Комплектация", # filterable=True, # allow_custom_value=False, # value="Не указано" # ) body_type = gr.Dropdown(choices=OPTIONS['body_type'], label="🚙 Тип кузова") drive_type = gr.Dropdown(choices=OPTIONS['drive_type'], label="⚙️ Привод") engine_type = gr.Dropdown(choices=OPTIONS['engine_type'], label="⛽ Двигатель") production_year = gr.Dropdown(choices=list(range(1960, 2026)), label="⌛ Год производства") mileage = inputs=gr.Slider(minimum=0, maximum=1000000, value=50000, step=1000, label="🛤️ Пробег") with gr.Column(): steering_wheel = gr.Dropdown(choices=OPTIONS['steering_wheel'], label="🛞 Руль", value="Левый") owners_count = gr.Dropdown(choices=OPTIONS['owners_count'], label="👤 Владельцы") crashes_count = gr.Dropdown(choices=OPTIONS['crashes_count'], label="💥 ДТП") color = gr.Dropdown(choices=OPTIONS['color'], label="🎨 Цвет") with gr.Accordion("Дополнительные параметры", open=False): with gr.Row(): with gr.Column(): pts = gr.Dropdown(choices=OPTIONS['pts'], label="📄 ПТС") salon = gr.Dropdown(choices=OPTIONS['salon'], label="💺 Салон") audiosistema = gr.Dropdown(choices=OPTIONS['audiosistema'], label="🎵 Аудио") doors_number = gr.Dropdown(choices=OPTIONS['doors_number'], label="🚪 Число дверей") electropodemniki = gr.Dropdown(choices=OPTIONS['electropodemniki'], label="↕️ Подьёмники") with gr.Column(): fary = gr.Dropdown(choices=OPTIONS['fary'], label="💡 Фары") upravlenie_klimatom = gr.Dropdown(choices=OPTIONS['upravlenie_klimatom'], label="❄️ Климат") diski = gr.Dropdown(choices=OPTIONS['diski'], label="💿 Диски") usilitel_rul = gr.Dropdown(choices=OPTIONS['usilitel_rul'], label="⚙️ Усилитель руля") btn = gr.Button("Рассчитать", variant="primary") output = "" inputs = [ body_type, drive_type, engine_type, color, pts, audiosistema, diski, fary, salon, upravlenie_klimatom, steering_wheel, crashes_count, owners_count, production_year, mileage, doors_number, usilitel_rul, electropodemniki ] # Центрирование с помощью колонок with gr.Row(): with gr.Column(scale=1): pass # Компонент Label для красивого вывода output = gr.Label( label="Прогноз стоимости:", show_label=False, # Скрываем маленькую надпись сверху для чистоты num_top_classes=0 # Нам не нужна классификация, только одно значение ) with gr.Column(scale=1): pass btn.click( fn=lambda: "Идёт рассчёт стоимости...", outputs=output ).then( fn=predict, inputs=inputs, outputs=output, show_progress="full" ) if __name__ == "__main__": demo.launch(css=custom_css, ssr_mode=False, share=True)