GreatGutsy's picture
Update app.py
fa3d68e verified
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)