
Введение
Каждый специалист по данным не раз просиживал часы за отладкой модели, чтобы в итоге выяснить: проблема не в алгоритме, а в пустом значении в строке номер 47 832. Соревнования на Kaggle навевают мысль, будто данные всегда приходят в идеальном виде — чистые CSV-файлы с точными метками и без дисбаланса классов. На практике всё обстоит иначе.
В этой статье на примере реального проекта по данным разберём четыре полезных приёма, чтобы научиться справляться с беспорядочными наборами из жизни.
Проект NoBroker: тест на хаос реальных данных
NoBroker — индийская компания в сфере prop-tech, которая объединяет владельцев недвижимости и арендаторов напрямую, без посредников-брокеров.

Этот проект по данным используют на собеседованиях для найма специалистов по data science в NoBroker.
Задача — создать модель, которая предскажет количество взаимодействий с объектом недвижимости за заданный период. Полностью проект здесь не разбираем, но он поможет выявить приёмы работы с беспорядочными реальными данными.
В проекте три набора данных:
property_data_set.csv- Детали объектов: тип, расположение, удобства, площадь, аренда и другие характеристики жилья.
property_photos.tsv- Фотографии объектов.
property_interactions.csv- Метки времени взаимодействий с объектами.
Чистые данные на собеседованиях против production-данных: проверка на деле
Наборы для интервью — отполированные, сбалансированные и предсказуемые. А production-данные? Это сплошной бардак: пропуски, дубликаты, разные форматы и ошибки, которые вылезут в пятницу вечером, сломав пайплайн.
Возьмём набор NoBroker — реальный беспорядок из 28 888 объектов в трёх таблицах. С виду нормально, но при ближайшем рассмотрении: 11 022 пропущенных URL фото (38% данных), испорченные JSON-строки с лишними слешами и прочее.
Вот грань между чистотой и хаосом. Чистые данные учат строить модели, а production-данные — выживать в борьбе.
Разберём четыре приёма для тренировки.

Приём 1: работа с пропусками
Пропуски — не просто раздражение, а момент для решений. Удалить строку? Заполнить средним? Отметить как неизвестно? Всё зависит от причины пропусков и того, сколько данных можно потерять.
В наборе NoBroker три вида пропусков. В столбце photo_urls их 11 022 из 28 888 строк — 38% данных. Вот код проверки.
pics.isna().sum()Результат:

Удалить эти строки — значит потерять ценные записи об объектах. Лучше посчитать отсутствие фото как ноль. Вот решение.
def correction(x): if x is np.nan or x == 'NaN': return 0 # Missing photos = 0 photos else: return len(json.loads(x.replace('\\', '').replace('{title','{"title'))) pics['photo_count'] = pics['photo_urls'].apply(correction)Для числовых столбцов вроде total_floor (23 пропуска) и категориальных вроде building_type (38 пропусков) применили заполнение. Числа — средним, категории — модой.
for col in x_remain_withNull.columns: x_remain[col] = x_remain_withNull[col].fillna(x_remain_withNull[col].mean()) for col in x_cat_withNull.columns: x_cat[col] = x_cat_withNull[col].fillna(x_cat_withNull[col].mode()[0])Первое правило: не удаляй на автомате — думай!
Понять паттерн пропусков. URL фото пропущены не случайно.
Приём 2: поиск выбросов
Выброс не всегда ошибка, но всегда повод насторожиться.
Представьте объект с 21 ванной, возрастом 800 лет или площадью 40 000 кв. футов. Либо мечта, либо ошибка ввода.
В наборе NoBroker полно таких флагов. Диаграммы размаха выявили экстремумы: возраст свыше 100 лет, площадь за 10 000 кв. футов, депозиты больше 3,5 млн. Некоторые — люкс, большинство — опечатки.
df_num.plot(kind='box', subplots=True, figsize=(22,10)) plt.show()Результат:

Решение — удаление по межквартильному размаху (IQR), простая статистика: флаги за пределами 2 IQR.
Сначала функция для удаления.
def remove_outlier(df_in, col_name): q1 = df_in[col_name].quantile(0.25) q3 = df_in[col_name].quantile(0.75) iqr = q3 - q1 fence_low = q1 - 2 * iqr fence_high = q3 + 2 * iqr df_out = df_in.loc[(df_in[col_name] <= fence_high) & (df_in[col_name] >= fence_low)] return df_out # Note: Multiplier changed from 1.5 to 2 to match implementation.Применяем к числовым столбцам.
df = dataset.copy() for col in df_num.columns: if col in ['gym', 'lift', 'swimming_pool', 'request_day_within_3d', 'request_day_within_7d']: continue # Skip binary and target columns df = remove_outlier(df, col) print(f"Before: {dataset.shape[0]} rows") print(f"After: {df.shape[0]} rows") print(f"Removed: {dataset.shape[0] - df.shape[0]} rows ({((dataset.shape[0] - df.shape[0]) / dataset.shape[0] * 100):.1f}% reduction)")Результат:

После чистки набор сжался с 17 386 строк до 15 170 — минус 12,7%, зато модель в порядке. Компромисс окупился.
Для целевых вроде request_day_within_3d вместо удаления — обрезка. Значения свыше 10 ставим в 10, чтобы не искажать предсказания. Вот код с сравнением.
def capping_for_3days(x): num = 10 return num if x > num else x df['request_day_within_3d_capping'] = df['request_day_within_3d'].apply(capping_for_3days) before_count = (df['request_day_within_3d'] > 10).sum() after_count = (df['request_day_within_3d_capping'] > 10).sum() total_rows = len(df) change_count = before_count - after_count percent_change = (change_count / total_rows) * 100 print(f"Before capping (>10): {before_count}") print(f"After capping (>10): {after_count}") print(f"Reduced by: {change_count} ({percent_change:.2f}% of total rows affected)")Результат:

Распределение чище, модель лучше, отладки меньше.
Приём 3: дубликаты и несоответствия
Дубликаты просты — df.drop_duplicates(). Несоответствия сложнее. Например, JSON-строка, изуродованная несколькими системами.
В NoBroker photo_urls должен был хранить JSON-массивы, но там каша: без кавычек, экранированные слеши, лишние символы.
text_before = pics['photo_urls'][0] print('Before Correction: \n\n', text_before)До исправления:

Исправление — цепочка замен строк перед парсингом. Вот код.
text_after = text_before.replace('\\', '').replace('{title', '{"title').replace(']"', ']').replace('],"', ']","') parsed_json = json.loads(text_after)Результат:

JSON стал валидным. Не самый элегантный способ манипуляций со строками, но работает.
Несоответствия повсюду: даты как строки, опечатки в категориях, ID как float. Решение — стандартизация, как с JSON.
Приём 4: проверка типов данных и схемы
Всё начинается при загрузке. Узнавать потом, что даты — строки, а числа — объекты, — потеря времени.
В проекте NoBroker типы проверяли сразу при чтении CSV с параметрами pandas. Вот код.
data = pd.read_csv('property_data_set.csv') print(data['activation_date'].dtype) data = pd.read_csv('property_data_set.csv', parse_dates=['activation_date'], infer_datetime_format=True, dayfirst=True) print(data['activation_date'].dtype)Результат:

То же для взаимодействий.
interaction = pd.read_csv('property_interactions.csv', parse_dates=['request_date'], infer_datetime_format=True, dayfirst=True)Это не просто хорошая практика — необходимость для расчётов вроде разницы дат активации и запроса.
num_req['request_day'] = (num_req['request_date'] - num_req['activation_date']) / np.timedelta64(1, 'D')Проверки схемы спасут от изменений структуры, но данные ещё дрейфуют — распределение меняется со временем. Можно имитировать, варьируя пропорции входов и проверяя, ловит ли модель сдвиги.
Фиксация шагов очистки
Через три месяца забудешь, зачем обрезал request_day_within_3d до 10. Через полгода коллега сломает пайплайн, убрав фильтр выбросов. Через год модель в проде рухнет без объяснений.
Документация обязательна. Это грань между воспроизводимым пайплайном и скриптом на удачу.
В проекте NoBroker все трансформации фиксировали в комментариях кода и разделах ноутбука с оглавлением и объяснениями.
# Assignment # Read and Explore All Datasets # Data Engineering Handling Pics Data Number of Interactions Within 3 Days Number of Interactions Within 7 Days Merge Data # Exploratory Data Analysis and Processing # Feature Engineering Remove Outliers One-Hot Encoding MinMaxScaler Classical Machine Learning Predicting Interactions Within 3 Days Deep Learning # Try to correct the first Json # Try to replace corrupted values then convert to json # Function to correct corrupted json and get count of photosВерсионный контроль тоже ключ. Отслеживай изменения в логике очистки. Сохраняй промежуточные наборы. Веди чейнджлог экспериментов и успехов.
Цель не идеал, а ясность. Не объяснишь решение — не защитишь модель при сбое.
Итоги
Идеальные данные — миф. Лучшие специалисты по данным не бегут от беспорядка, а укрощают его. Они находят пропуски до обучения.
Определяют выбросы до влияния на предсказания. Проверяют схемы перед джойнами таблиц. Фиксируют всё, чтобы следующий не начинал с нуля.
Настоящий эффект рождается не из совершенства, а из умения работать с ошибками и строить рабочее решение.
Так что при виде пропусков, сломанных строк и выбросов в наборе не пугайтесь. Это шанс проявить мастерство на реальных данных.