Spaces:
Sleeping
Sleeping
| 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) |