Новая Школа - онлайн-школа подготовки к ЕГЭ
Задача Задача №3654513
А
Аноним
14 сентября 2026 14:59 · публичный вопрос
А
Аноним

Представьте, что вы выставляете свой автомобиль на продажу на Авито. Вы загружаете фотографии, но не знаете, какую цену поставить, чтобы машина быстро нашла покупателя и при этом…

Запрос пользователя

Представьте, что вы выставляете свой автомобиль на продажу на Авито. Вы загружаете фотографии, но не знаете, какую цену поставить, чтобы машина быстро нашла покупателя и при этом не продешевить. В этой задаче вам предстоит помочь в решении реальной индустриальной проблемы - автоматического прайсинга автомобилей. Такой сервис помогает продавцам корректно оценивать стоимость машины, ускоряя продажу и делая рынок б/у авто более прозрачным. Перед вами фотографии автомобилей с объявлений Авито за 2024 год - максимум 4 изображений на каждый объект, в основном внешний вид. Тренировочная выборка: 70 000 объектов и 273 873 фотографии. Тест: 25 000 автомобилей и 98 075 фотографий. Задание: Постройте модель, которая для каждого автомобиля из тестовой выборки предскажет его стоимость. Данные: train_dataset.parquet - обучающая выборка, целевая переменная: price_TARGET. test_dataset.parquet - тестовая выборка. sample_submission.csv - шаблон отправки (ID,target). по этой ссылке доступны файлы train_images.zip и test_images.zip - это фотографии автомобилей, например, 100000_0.jpg, где 100000 - это ID объявления. Разделение выполнено по времени: train - это объявления с января по ноябрь 2024 года, а test - объявления за декабрь 2024 года. Помимо фотографий автомобилей вам доступны и некоторые атрибуты объектов: body_type - тип кузова drive_type - тип привода engine_type - тип двигателя doors_number - количество дверей color - цвет pts - вид ПТС steering_wheel - руль crashes_count - число ДТП owners_count - число владельцев mileage - пробег latitude, longitude - координаты локации продажи

Ответ нейросети

Краткий ответ. Это задача регрессии: для каждого автомобиля нужно предсказать price_TARGET. Надёжное решение:

  1. обучить CatBoostRegressor на табличных признаках;
  2. предсказывать не саму цену, а log1p(price_TARGET);
  3. использовать временную валидацию: ранние месяцы — обучение, последние — проверка;
  4. из фотографий получить эмбеддинги предобученной CNN и усреднить их по автомобилю;
  5. объединить табличные признаки и признаки фотографий, затем сформировать submission.csv.

Точную модель нельзя окончательно выбрать без указанной метрики соревнования и самих файлов, но ниже приведён рабочий план.

1. Почему нужна временная валидация

Тест содержит объявления за декабрь 2024 года, а обучение — за январь–ноябрь 2024 года. Поэтому случайное разделение может дать слишком оптимистичную оценку.

Правильнее сделать, например:

  • обучение: январь–октябрь;
  • валидация: ноябрь.

Если в таблице есть дата объявления, используйте её. Если даты нет, можно взять последние 20% строк, но только если строки действительно упорядочены по времени.

Фото нужно разделять на уровне автомобиля, а не отдельно: у одного объекта может быть до 4 изображений.

2. Табличный baseline

CatBoost хорошо подходит для этой задачи, потому что умеет работать с категориальными признаками:

  • body_type;
  • drive_type;
  • engine_type;
  • color;
  • pts;
  • steering_wheel.

Числовые признаки:

  • mileage;
  • owners_count;
  • crashes_count;
  • doors_number;
  • latitude;
  • longitude.

Идентификатор объявления не следует подавать в модель: он нужен только для соединения с фотографиями.

import numpy as np
import pandas as pd

from catboost import CatBoostRegressor

train = pd.read_parquet("train_dataset.parquet")
test = pd.read_parquet("test_dataset.parquet")
sample = pd.read_csv("sample_submission.csv")

TARGET = "price_TARGET"

# В sample_submission.csv обычно первый столбец — ID
sample_id_col = sample.columns[0]

id_col = next(
    c for c in train.columns
    if c.lower() == sample_id_col.lower()
)

# Удаляем некорректные цены
train = train[
    train[TARGET].notna() &
    (train[TARGET] > 0)
].reset_index(drop=True)

# Целевая переменная с логарифмированием
y_raw = train[TARGET].astype(float).to_numpy()
y = np.log1p(y_raw)

Добавим простые признаки даты и геолокации:

date_col = next(
    (
        c for c in train.columns
        if any(word in c.lower() for word in ["date", "time", "created"])
    ),
    None
)

def prepare_features(df):
    x = df.drop(
        columns=[TARGET, id_col],
        errors="ignore"
    ).copy()

    # Преобразование даты в числовые признаки
    if date_col is not None and date_col in x.columns:
        date = pd.to_datetime(x[date_col], errors="coerce")

        x[date_col + "_year"] = date.dt.year
        x[date_col + "_month"] = date.dt.month
        x[date_col + "_dayofweek"] = date.dt.dayofweek
        x[date_col + "_day"] = date.dt.day

        x = x.drop(columns=[date_col])

    numeric_columns = [
        "mileage",
        "latitude",
        "longitude",
        "doors_number",
        "owners_count",
        "crashes_count",
    ]

    for col in numeric_columns:
        if col in x.columns:
            x[col] = pd.to_numeric(
                x[col].astype(str).str.replace(",", ".", regex=False),
                errors="coerce"
            )

    # Укрупнённая ячейка расположения
    if {"latitude", "longitude"} <= set(x.columns):
        lat = pd.to_numeric(x["latitude"], errors="coerce")
        lon = pd.to_numeric(x["longitude"], errors="coerce")

        x["geo_cell"] = (
            lat.round(2).astype("string").fillna("NA")
            + "_"
            + lon.round(2).astype("string").fillna("NA")
        )

    return x


X = pd.concat(
    [
        prepare_features(train),
        prepare_features(test)
    ],
    axis=0,
    ignore_index=True
)

# Подготовка категориальных признаков
cat_cols = []

for col in X.columns:
    if (
        pd.api.types.is_object_dtype(X[col])
        or pd.api.types.is_string_dtype(X[col])
        or pd.api.types.is_categorical_dtype(X[col])
        or pd.api.types.is_bool_dtype(X[col])
    ):
        X[col] = X[col].fillna("__MISSING__").astype(str)
        cat_cols.append(col)

X_train_all = X.iloc[:len(train)].copy()
X_test = X.iloc[len(train):].copy()

3. Временное разделение

if date_col is not None:
    dates = pd.to_datetime(
        train[date_col],
        errors="coerce"
    )

    if dates.notna().mean() > 0.8:
        border = dates.quantile(0.8)

        train_idx = np.where(dates < border)[0]
        valid_idx = np.where(dates >= border)[0]
    else:
        border_idx = int(len(train) * 0.8)

        train_idx = np.arange(border_idx)
        valid_idx = np.arange(border_idx, len(train))
else:
    # Запасной вариант, если даты нет
    border_idx = int(len(train) * 0.8)

    train_idx = np.arange(border_idx)
    valid_idx = np.arange(border_idx, len(train))

Обучение модели:

model = CatBoostRegressor(
    loss_function="RMSE",
    eval_metric="RMSE",
    iterations=3000,
    learning_rate=0.05,
    depth=8,
    l2_leaf_reg=5,
    random_seed=42,
    verbose=200,
    allow_writing_files=False
)

model.fit(
    X_train_all.iloc[train_idx],
    y[train_idx],
    cat_features=cat_cols,
    eval_set=(
        X_train_all.iloc[valid_idx],
        y[valid_idx]
    ),
    use_best_model=True,
    early_stopping_rounds=200
)

Проверим результат:

valid_pred_log = model.predict(
    X_train_all.iloc[valid_idx]
)

valid_pred = np.maximum(
    np.expm1(valid_pred_log),
    0
)

rmse_log = np.sqrt(
    np.mean(
        (valid_pred_log - y[valid_idx]) ** 2
    )
)

rmse_price = np.sqrt(
    np.mean(
        (valid_pred - y_raw[valid_idx]) ** 2
    )
)

mape = np.mean(
    np.abs(
        (valid_pred - y_raw[valid_idx])
        / np.maximum(y_raw[valid_idx], 1)
    )
) * 100

print("RMSE по log-цене:", rmse_log)
print("RMSE по цене:", rmse_price)
print("MAPE:", mape)

Логарифмирование помогает, потому что цена автомобиля имеет длинный хвост: несколько дорогих автомобилей не должны полностью определять обучение. После предсказания выполняется обратное преобразование:

\[ \widehat{price} = \exp(\widehat{\log(1+price)}) - 1. \]

4. Как использовать фотографии

Каждая фотография сначала преобразуется предобученной нейронной сетью, например ResNet-18 или ResNet-50, в вектор признаков.

Для файла:

100000_0.jpg

ID автомобиля равен:

100000

У одного автомобиля может быть несколько векторов. Их удобно объединить:

\[ z_{\text{car}} = [ \operatorname{mean}(z_1,\ldots,z_k), \operatorname{std}(z_1,\ldots,z_k), k ]. \]

То есть для каждого автомобиля используются:

  • средний визуальный вектор;
  • стандартное отклонение по фотографиям;
  • количество фотографий.

Среднее и стандартное отклонение лучше простого соединения векторов, потому что порядок фотографий не важен.

Практический процесс:

# Для каждой фотографии:
photo_id = filename.stem.rsplit("_", 1)[0]

# Получаем embedding:
embedding = image_model(image)

# Группируем:
embeddings_by_car[photo_id].append(embedding)

Затем:

car_mean = np.mean(car_embeddings, axis=0)
car_std = np.std(car_embeddings, axis=0)
car_features = np.concatenate([
    car_mean,
    car_std,
    [len(car_embeddings)]
])

Если вектор ResNet имеет размер 512, можно дополнительно применить PCA, например до 64 или 128 признаков. PCA нужно обучать только на фотографиях обучающей выборки, а к тесту применять уже готовое преобразование.

После этого признаки фотографий добавляются к X_train_all и X_test, и обучается второй CatBoostRegressor.

Полезно сравнить:

  1. модель только на таблицах;
  2. модель на таблицах и фотографиях.

Если фотографии не улучшают результат на временной валидации, их лучше не использовать: визуальные признаки могут содержать шум.

5. Формирование отправки

test_pred_log = model.predict(X_test)

test_pred = np.maximum(
    np.expm1(test_pred_log),
    0
)

submission = sample.copy()
submission[submission.columns[1]] = test_pred

assert len(submission) == 25_000
assert submission.iloc[:, 0].tolist() == sample.iloc[:, 0].tolist()
assert submission.iloc[:, 1].notna().all()
assert (submission.iloc[:, 1] >= 0).all()

submission.to_csv(
    "submission.csv",
    index=False
)

Что считать хорошей проверкой

Нужно проверить, что:

  • валидация имитирует декабрь 2024 года;
  • ID не используется как числовой признак;
  • цена после обратного преобразования не содержит NaN;
  • в отправке ровно 25 000 строк;
  • порядок и значения ID совпадают с sample_submission.csv;
  • модель с фотографиями действительно лучше табличного baseline;
  • выбранная оценка соответствует метрике соревнования.

Если метрика считается на исходной цене, сравнивайте предсказания после expm1. Если используется RMSLE или ошибка по логарифму, сравнивайте значения в логарифмическом масштабе.