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

Задание: Автоматический точный расчёт стоимости доставки для продавцов и покупателей Авито подразумевает, что необходимо заранее предсказывать индивидуальные весогабаритные…

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

Задание: Автоматический точный расчёт стоимости доставки для продавцов и покупателей Авито подразумевает, что необходимо заранее предсказывать индивидуальные весогабаритные характеристики (ВГХ) товара по его описанию и фото. Для этого постройте модель, которая для каждого объекта из тестовой выборки предсказывает четыре непрерывные величины: target_weight - вес (кг), target_height - высота (см), target_width - ширина (см), target_length - длина (см). Данные: train.parquet - обучающая выборка, test.parquet - тестовая выборка, sample_submission.csv - шаблон отправки, train.zip и test.zip - изображения товаров (ровно 1 фото на товар). Все данные можно скачать из Облака Тренировочный набор train.parquet с колонками: item_id - уникальный ID товара, order_date - дата оформления заказа, item_condition - состояние товара, item_price - цена, ₽, category_name, subcategory_name, microcat_name - иерархия категорий (ур. 1/2/3), seller_id, buyer_id - ID продавца/покупателя, title, description - заголовок и описание объявления, image_name - имя изображения, соответствует объекту в train.zip. Таргеты: real_weight (кг), real_height (см), real_width (см), real_length (см). Тестовая выборка: test.parquet - те же признаки, без таргетов. Метрика В качестве метрики качества используется Macro Log-MAE. Считаем MAE в лог-шкале по каждому таргету и усредняем: LogMAE t = 1 N ∑ N i = 1 ∣ ∣ log ( 1 + y i , t ) − log ( 1 + ^ y i , t ) ∣ ∣ , Score = 1 4 ∑ t ∈ { weight,height,length,width } LogMAE t , где y i , t - истинное значение, ^ y i , t - предсказание. Система оценки Промежуточный балл, отображаемый в таблице результатов до момента завершения раунда, рассчитывается по формуле: Score = 1 / (1 + LogMAE), где LogMAE - значение метрики, рассчитанное по указанной выше формуле Окончательное количество баллов за задание определяется по формуле: Points = 200 ⋅ percentileofscore(Scores, Score), где Points – количество баллов. percentileofscore – функция, вычисляющая percentile rank, Scores – промежуточные баллы, подсчитанные по метрике LogMAE для всех участников, Score – промежуточный балл, подсчитанный по метрике LogMAE данного участника. При расчете баллов используются значения метрики участников, чьи решения показали результат лучше, чем приложенный пример ответа (sample_submission.csv) на задачу. Решения с метрикой ниже или равной значению, полученному с помощью примера, оцениваются в 0 баллов. Файлы sample_submission.csv 1.84 Мб test.parquet 19.61 Мб train.parquet 70.18 Мб

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

Краткий ответ: обучайте четыре регрессора на преобразованных целях
\[ z=\log(1+y), \] поскольку метрика считается как MAE именно в лог-шкале. После предсказания выполните обратное преобразование: \[ \hat y=\exp(\hat z)-1. \] Ниже — рабочий baseline на CatBoost для текста, категорий, цены и даты. Точный LogMAE и Points без файлов и результатов других участников вычислить нельзя.

1. Основной принцип

Для каждого товара предсказываются:

  • real_weight — вес в кг;
  • real_height — высота в см;
  • real_width — ширина в см;
  • real_length — длина в см.

Обучать модель на исходных значениях невыгодно: большие размеры будут сильнее влиять на ошибку. Поэтому:

y_log = np.log1p(y)

Модель обучается с loss_function="MAE" на y_log. Это непосредственно соответствует Log-MAE.

После предсказания:

prediction = np.expm1(prediction_log)
prediction = np.maximum(prediction, 0)

В файл отправляются именно значения в кг и см, а не логарифмы.

2. Базовое решение

import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split
from catboost import CatBoostRegressor


# ----------------------------
# Загрузка данных
# ----------------------------
train = pd.read_parquet("train.parquet")
test = pd.read_parquet("test.parquet")
sample = pd.read_csv("sample_submission.csv")

REAL_TARGETS = [
    "real_weight",
    "real_height",
    "real_width",
    "real_length",
]

SUBMISSION_TARGETS = [
    "target_weight",
    "target_height",
    "target_width",
    "target_length",
]

# В разных версиях шаблона имена целевых столбцов
# могут быть real_* или target_*.
if all(col in sample.columns for col in SUBMISSION_TARGETS):
    output_cols = SUBMISSION_TARGETS
    train_target_cols = [
        col.replace("target_", "real_")
        for col in output_cols
    ]
elif all(col in sample.columns for col in REAL_TARGETS):
    output_cols = REAL_TARGETS
    train_target_cols = REAL_TARGETS
else:
    raise ValueError(
        "Не удалось определить названия целевых столбцов "
        "в sample_submission.csv"
    )


# ----------------------------
# Построение признаков
# ----------------------------
CAT_COLS = [
    "item_condition",
    "category_name",
    "subcategory_name",
    "microcat_name",
    "seller_id",
    "buyer_id",
]

TEXT_COLS = [
    "title",
    "description",
]


date_origin = pd.to_datetime(
    train["order_date"],
    errors="coerce"
).min()

if pd.isna(date_origin):
    date_origin = pd.Timestamp("2000-01-01")


def make_features(df, origin):
    x = df.copy()

    # Цели не должны попасть в признаки
    x = x.drop(
        columns=[col for col in REAL_TARGETS if col in x.columns],
        errors="ignore"
    )

    # Уникальные идентификаторы и имя изображения
    # напрямую обычно не дают полезного обобщения
    x = x.drop(
        columns=["item_id", "image_name"],
        errors="ignore"
    )

    # Разбор даты
    if "order_date" in x.columns:
        date = pd.to_datetime(
            x.pop("order_date"),
            errors="coerce"
        )

        x["order_year"] = (
            date.dt.year.fillna(-1).astype("int16")
        )
        x["order_month"] = (
            date.dt.month.fillna(-1).astype("int8")
        )
        x["order_day"] = (
            date.dt.day.fillna(-1).astype("int8")
        )
        x["order_weekday"] = (
            date.dt.dayofweek.fillna(-1).astype("int8")
        )

        x["days_from_origin"] = (
            (date - origin)
            .dt.total_seconds()
            .div(86400)
            .fillna(-1)
            .astype("float32")
        )

    # Цена в исходном масштабе и в логарифмическом
    if "item_price" in x.columns:
        price = pd.to_numeric(
            x["item_price"],
            errors="coerce"
        )

        x["item_price"] = price
        x["item_price_log1p"] = np.log1p(
            price.clip(lower=0)
        )

    # Категориальные признаки
    for col in CAT_COLS:
        if col in x.columns:
            x[col] = (
                x[col]
                .astype("string")
                .fillna("__MISSING__")
                .astype(str)
            )

    # Текстовые признаки
    for col in TEXT_COLS:
        if col in x.columns:
            x[col] = (
                x[col]
                .astype("string")
                .fillna("")
                .astype(str)
            )

    # Все оставшиеся строковые признаки считаем категориальными
    for col in x.columns:
        is_string = (
            pd.api.types.is_object_dtype(x[col])
            or pd.api.types.is_string_dtype(x[col])
        )

        if col not in TEXT_COLS and is_string:
            x[col] = (
                x[col]
                .astype("string")
                .fillna("__MISSING__")
                .astype(str)
            )

    return x


X = make_features(train, date_origin)
X_test = make_features(test, date_origin)

# Одинаковый порядок столбцов
X_test = X_test.reindex(columns=X.columns)

cat_cols = [
    col for col in CAT_COLS
    if col in X.columns
]

text_cols = [
    col for col in TEXT_COLS
    if col in X.columns
]

# Добавим прочие строковые столбцы в категориальные
for col in X.columns:
    is_string = (
        pd.api.types.is_object_dtype(X[col])
        or pd.api.types.is_string_dtype(X[col])
    )

    if col not in text_cols and is_string:
        if col not in cat_cols:
            cat_cols.append(col)


# ----------------------------
# Разбиение для проверки
# ----------------------------
indices = np.arange(len(train))

train_idx, valid_idx = train_test_split(
    indices,
    test_size=0.2,
    random_state=42
)


# ----------------------------
# Обучение четырех моделей
# ----------------------------
base_params = {
    "iterations": 2000,
    "depth": 8,
    "learning_rate": 0.04,
    "loss_function": "MAE",
    "eval_metric": "MAE",
    "l2_leaf_reg": 8,
    "random_seed": 42,
    "allow_writing_files": False,
}

fit_params = {
    "cat_features": cat_cols,
}

if text_cols:
    fit_params["text_features"] = text_cols


valid_pred_log = np.zeros(
    (len(valid_idx), len(train_target_cols)),
    dtype=np.float32
)

test_pred_log = np.zeros(
    (len(test), len(train_target_cols)),
    dtype=np.float32
)


for target_number, target_col in enumerate(train_target_cols):
    print(f"\nОбучение для {target_col}")

    y_raw = pd.to_numeric(
        train[target_col],
        errors="coerce"
    ).to_numpy(dtype=np.float32)

    # Физические величины не должны быть отрицательными
    y_log = np.log1p(
        np.maximum(y_raw, 0)
    )

    model = CatBoostRegressor(
        **base_params,
        verbose=250
    )

    model.fit(
        X.iloc[train_idx],
        y_log[train_idx],
        eval_set=(
            X.iloc[valid_idx],
            y_log[valid_idx]
        ),
        use_best_model=True,
        **fit_params
    )

    # Предсказание на валидации в логарифмической шкале
    valid_log = model.predict(
        X.iloc[valid_idx]
    )

    valid_pred_log[:, target_number] = np.maximum(
        valid_log,
        0
    )

    # Число итераций выбираем по валидации
    best_iteration = model.get_best_iteration()

    if best_iteration is None or best_iteration < 0:
        best_iteration = base_params["iterations"] - 1

    # Переобучение на всех доступных данных
    final_params = dict(
        base_params,
        iterations=int(best_iteration) + 1,
        verbose=False
    )

    final_model = CatBoostRegressor(**final_params)

    final_model.fit(
        X,
        y_log,
        **fit_params
    )

    test_log = final_model.predict(X_test)

    test_pred_log[:, target_number] = np.maximum(
        test_log,
        0
    )


# ----------------------------
# Проверка Log-MAE
# ----------------------------
valid_true = train.iloc[
    valid_idx
][train_target_cols].to_numpy(dtype=float)

valid_pred = np.expm1(valid_pred_log)

valid_true_log = np.log1p(
    np.maximum(valid_true, 0)
)

valid_pred_log_checked = np.log1p(
    np.maximum(valid_pred, 0)
)

per_target_log_mae = np.mean(
    np.abs(valid_true_log - valid_pred_log_checked),
    axis=0
)

log_mae = per_target_log_mae.mean()
score = 1 / (1 + log_mae)

print("\nLog-MAE по целям:")
for col, value in zip(train_target_cols, per_target_log_mae):
    print(f"{col}: {value:.6f}")

print(f"\nСредний Log-MAE: {log_mae:.6f}")
print(f"Промежуточный Score: {score:.6f}")


# ----------------------------
# Формирование отправки
# ----------------------------
test_predictions = np.expm1(test_pred_log)
test_predictions = np.maximum(test_predictions, 0)

submission = sample.copy()

for number, col in enumerate(output_cols):
    submission[col] = test_predictions[:, number]

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

print("\nФайл submission.csv сохранён.")

3. Что делает модель

  • seller_id и buyer_id передаются как категориальные признаки;
  • category_name, subcategory_name, microcat_name также обрабатываются как категории;
  • title и description используются CatBoost как текст;
  • дата превращается в год, месяц, день недели и число дней от начала;
  • цена используется в исходном и логарифмическом виде;
  • для каждой из четырёх целей обучается отдельная модель.

item_id и image_name не подаются напрямую: это почти уникальные идентификаторы, которые могут привести к переобучению.

4. Как использовать изображения

В baseline выше изображения пока не используются. Для улучшения можно:

  1. открыть изображение по image_name из соответствующего ZIP-архива;
  2. пропустить его через предобученную ResNet, EfficientNet или CLIP;
  3. получить вектор признаков, например из 512 или 2048 чисел;
  4. добавить эти числа в X и X_test как обычные числовые признаки.

Важно сопоставлять изображения именно по image_name, а не по порядку файлов в ZIP.

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

\[ \hat z = \alpha \hat z_{\text{табличная}} + (1-\alpha)\hat z_{\text{изображение}}. \]

Значение \(\alpha\) выбирают по валидационному Log-MAE. Усреднять лучше логарифмические предсказания, потому что именно в этой шкале считается метрика.

Points нельзя точно получить только по своей валидации: они зависят от промежуточных результатов других участников и от качества sample_submission.csv.