Автомобильные объявления как учебный набор данных • 20 октября 2026 года

Посчитать медиану цен в Python можно одной строкой. Гораздо сложнее объяснить, какие объявления попали в эту строку. Если смешать поколения автомобилей, принять пропуск за ноль и дать одному экземпляру несколько голосов через дубли, аккуратно вычисленное число будет описывать плохо подготовленную группу.

Ниже разберём маленький воспроизводимый пример. Он посвящён автомобилям с пробегом, но основная инженерная задача знакома любому разработчику отчётов: сначала определить контракт данных, затем отбор, потом агрегирование и только после этого интерпретацию.

От автомобильного вопроса к правилу отбора

В методике оценки машины по объявлениям «Мотор» описан подбор по марке, модели и близкому году выпуска, а также показ медианы и центральной половины цен. При недостатке предложений диапазон годов расширяется. Описание проверено 20 октября 2026 года.

Это хороший повод сформулировать технический вопрос: как построить собственный прозрачный расчёт на подготовленном наборе строк? Мы не будем воспроизводить внутренний алгоритм сайта, обращаться к его недокументированным API или выдавать придуманные объявления за реальные. Все записи ниже синтетические, а поколение добавлено как отдельный фильтр нашего учебного примера.

Для примера ищем Octavia поколения A5 с целевым годом 2012 и допустимым отклонением два года. Такой контракт не означает, что один диапазон всегда подходит покупателю. В реальном анализе поколение, двигатель, коробка и другие существенные признаки нужно определить по собственной задаче.

Цена и единицы измерения — часть контракта

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

Ноль не означает «неизвестную цену». В этом примере он считается невалидным значением и не участвует в расчёте. То же относится к логическому True: для цены это неподходящий тип. Поэтому проверка использует type(value) is int, а не только isinstance(value, int), которое в Python принимает и булевы значения.

Код получает уже структурированные объекты. Он не является универсальным парсером объявлений: не очищает строки с пробелами, не распознаёт валюты и не подтверждает достоверность характеристик машины. Эти ограничения лучше обозначить заранее, чем спрятать за названием функции clean_data.

Сначала уникальные наблюдения, потом статистика

У каждой записи есть идентификатор id. В нашем контракте одинаковый ID обозначает одно наблюдение в одном срезе. Полностью совпадающий повтор удаляется. Если под одним ID пришли разные данные, функция останавливается с ошибкой, чтобы конфликт не исчез незаметно.

Это правило намеренно простое. В рабочей системе понадобится дата наблюдения, источник и политика обработки изменений. Две цены одного объявления за разные дни могут быть корректной историей, а две конфликтующие строки в одном срезе — поводом проверить загрузку. Без этой разницы безусловное «оставить последнюю запись» часто скрывает важную неопределённость.

Фильтрация учебных автомобильных объявлений: 10 строк превращаются в 5 сопоставимых предложений

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

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

Полный пример на стандартной библиотеке

Используем dataclass для формы записи и модуль statistics для расчётов. Описание функций median() и quantiles() есть в официальной документации Python. Для квартилей явно выбираем method="inclusive"; этот выбор относится к нашему примеру и не описывает внутренние настройки чужого сервиса.

from dataclasses import dataclass from statistics import median, quantiles import json @dataclass(frozen=True) class Listing: id: str model: str generation: str year: int asking_rub: int def summarize(rows, model='Octavia', generation='A5', year=2012): unique = {} invalid = duplicates = 0 for row in rows: if (not row.id or type(row.year) is not int or type(row.asking_rub) is not int or row.asking_rub <= 0): invalid += 1 continue if row.id in unique: if unique[row.id] != row: raise ValueError(f'Conflicting snapshot for {row.id}') duplicates += 1 continue unique[row.id] = row prices = sorted( row.asking_rub for row in unique.values() if row.model.casefold() == model.casefold() and row.generation.casefold() == generation.casefold() and abs(row.year - year) <= 2 ) center_half = None if len(prices) >= 4: q1, _, q3 = quantiles(prices, n=4, method='inclusive') center_half = [q1, q3] return { 'input_rows': len(rows), 'invalid_rows': invalid, 'duplicates': duplicates, 'unique_valid': len(unique), 'selected': len(prices), 'prices_rub': prices, 'median_rub': median(prices) if prices else None, 'center_half_rub': center_half, } # Every record and price below is synthetic. rows = [ Listing('a', 'Octavia', 'A5', 2012, 500_000), Listing('b', 'Octavia', 'A5', 2013, 550_000), Listing('c', 'Octavia', 'A5', 2011, 450_000), Listing('d', 'Octavia', 'A7', 2012, 700_000), Listing('e', 'Octavia', 'A5', 2012, 0), Listing('a', 'Octavia', 'A5', 2012, 500_000), Listing('f', 'Octavia', 'A5', 2014, 600_000), Listing('g', 'Octavia', 'A5', 2010, 400_000), Listing('h', 'Octavia', 'A5', 2018, 800_000), Listing('i', 'Octavia', 'A5', 2012, True), ] if __name__ == '__main__': print(json.dumps(summarize(rows), ensure_ascii=False, indent=2)) 

Здесь правило len(prices) >= 4 — выбранное ограничение нашего отчёта, а не утверждение о минимально допустимом количестве значений для любого вызова quantiles(). Даже пять наблюдений не дают оснований уверенно описывать весь рынок. Это всего лишь небольшой набор с прозрачной процедурой расчёта.

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

Какой результат должен получиться

После отбора цены равны 400 000, 450 000, 500 000, 550 000 и 600 000 рублей. Медиана — 500 000 рублей. При выбранном включающем методе границы центральной части равны 450 000 и 550 000 рублей.

Показатель учебного запуска Значение
Входных строк 10
Невалидных цен 2
Точных повторов 1
Уникальных валидных записей 7
После фильтра поколения и года 5
Медиана 500 000 ₽
Центральный диапазон 450 000–550 000 ₽

Центральный диапазон учебных цен: 450–550 тысяч рублей при медиане 500 тысяч

Центральная часть учебного ряда. Диаграмма показывает итог алгоритма на вымышленных значениях.

Все числа в таблице можно воспроизвести приведённым кодом. Они не являются оценкой конкретной Octavia и не отражают фактические сделки в ДНР или ЛНР. Это особенно важно, если диаграмма позже попадёт отдельно от текста в презентацию или отчёт: пояснение об учебных данных стоит сохранять рядом с рисунком.

Какие ошибки такой расчёт помогает заметить

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

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

Поэтому полезно различать корректность вычисления и качество исходного описания. Функция может правильно посчитать медиану выбранных чисел, даже если набор плохо представляет транспортную задачу покупателя. Код делает правила воспроизводимыми; он не гарантирует достоверность каждого объявления.

Почему центральный диапазон не является ценовой гарантией

Нижний и верхний квартили помогают описать разброс центральной части наблюдений. Графическое представление этой идеи приведено в справочнике NIST о box plot. Рядом с серединой становится видна ширина группы.

Из этого диапазона не следует вероятность продажи конкретной машины по определённой цене. В наборе находятся запрашиваемые суммы, а не подтверждённые результаты переговоров. Состояние отдельного автомобиля, обслуживание и неизвестные обстоятельства требуют следующего этапа проверки.

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

Как развить пример для настоящего отчёта

Следующий шаг — добавить дату среза, сведения об источнике и устойчивые правила нормализации версии. Затем полезно отдельно хранить исходную запись и результат проверки, чтобы исключение строки можно было объяснить. Когда появляются несколько дат, текущие предложения и историю изменения одной карточки лучше не смешивать в одном недатированном наборе.

Для неоднозначных совпадений понадобится отдельная процедура, а не только сравнение ID. При этом нельзя безосновательно объявлять любые похожие автомобили дублями. Подтверждение идентичности и степень уверенности должны соответствовать тем сведениям, которые действительно доступны.

Вывод отчёта стоит формулировать в границах данных: «в выбранной группе объявлений на такую-то дату получена такая-то медиана». Формулировка «эта машина точно стоит столько» добавляет обещание, которого в вычислении нет.

Главная инженерная работа здесь происходит до median(). Контракт цены, обработка повторов, сопоставимость версий и понятное поведение при отсутствии данных делают автомобильную сводку полезнее, чем ещё одно число без объяснения его происхождения.

О материале: код и все записи учебные. Статья подготовлена с использованием ИИ в рамках продвижения указанного автомобильного ресурса; пример выполнен и проверен отдельно. Наблюдаемые цены предложения не равны ценам завершённых сделок.