Към съдържанието
wannacode

Изтичане на данни (data leakage)

Накратко: Изтичането на данни (data leakage) е, когато при обучението моделът вижда информация, която няма да има при истинската прогноза, и затова оценката на теста излиза по-добра, отколкото ще е в живота.

Проблем

99,7% точност

Колега е направил модела от урока за класификацията: кои клиенти няма да поръчат след 1 юли. Само че взима таблицата на клиентите направо от системата, каквато е днес, 1 ноември: брой поръчки, дни от последната поръчка и средна поръчка. И ти показва:

Pythonданни: таблиците на магазина

Клиентите са 1 279. Правилото „всички се връщат“ познава 72,5% от тестовите. Моделът познава 99,7%: от 320 клиенти в теста бърка един. В урока за класификацията със същите три признака моделът беше 72,2%, под правилото. Какво се е променило? Таблицата.

Идеята

Отговорът е в признаците

Виж колко дни от последната поръчка имат върналите се (lost е 0) и загубените (lost е 1):

Pythonданни: таблиците на магазина

Всеки върнал се има от 0 до 122 дни, а всеки загубен от 123 до 363. Едно число ги разделя без грешка. Защо? days_since е сметнато до днес. Който е поръчал след 1 юли, има последна поръчка след 1 юли, значи е на по-малко от 123 дни от 1 ноември. Признакът съдържа етикета. Същото е с orders: брои и поръчките след 1 юли.

Това се казва изтичане на информация (data leakage): в обучението влиза информация, която моделът няма да има, когато прогнозира. На 1 юли не знаеш кой ще поръча през август. Правилото е едно: всеки признак се смята само от това, което е известно в момента на прогнозата.

По-точно, за любопитните

Изтичането се крие в колони, които системата обновява след събитието: „общо поръчки“, „последна покупка“, „статус“, „върнат продукт“. За всяка колона питай кога се записва стойността ѝ. Ако е след деня на прогнозата, колоната не става за признак, колкото и полезна да изглежда. Най-силните признаци в един модел са и първите, които проверяваш.

Идеята

Честното число

Същите признаци два пъти. Веднъж от всички поръчки до днес, веднъж само от поръчките преди 1 юли и до 1 юли:

Pythonданни: таблиците на магазина

С бъдещето в признаците точността е 99,7%. Честно е 72,2%, а базовата линия е 72,5%. С тези три признака моделът по точност не печели нищо, точно като в урока за класификацията. Неприятно, но вярно. По-добре да го знаеш сега, отколкото след като раздадеш купоните.

features(rows, now) е една функция за двете таблици: разликата е само кои поръчки ѝ даваш и до кой ден брои. .loc[honest.index] взима същите 1 279 клиенти в същия ред, затова двете разделяния са еднакви: 959 клиенти за обучение и 320 за тест.

Виж го

Как бъдещето влиза в признака

Същият код бавно. Точките са поръчките на четирима клиенти по месеци, а стената е 1 юли. Етикетът е от точките вдясно от нея. Признакът на колегата брои и тях.

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['value'] = items['quantity'] * items['unit_price']
orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
def features(rows, now):
people = rows.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (now - people['last']).dt.days
return people[['orders', 'days_since', 'basket']]
honest = features(before, cutoff)
leaky = features(orders, pd.Timestamp('2026-11-01')).loc[honest.index]
y = (~honest.index.isin(after['customer_id'])).astype(int)
for name, X in [('с бъдещето', leaky), ('честно', honest)]:
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y)
model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
print(name, round(model.score(X_test, y_test), 3))
print('клиенти:', len(honest), 'обучение:', len(X_train), 'тест:', len(X_test))
print('базова линия:', round(1 - y_test.mean(), 3))
Стъпка по стъпка, с думи
  1. orders са всички поръчки за годината, до 31.10.2026. Точките са поръчките на четирима клиенти по месеци.
  2. cutoff е 1 юли. Вляво от стената е before: това, което знаеш. Вдясно е after: това, което предсказваш.
  3. lost е 1, ако клиентът няма нито една поръчка вдясно. Етикетът е направен от точките след 1 юли.
  4. Таблицата на колегата брои до днес. orders брои и точките вдясно, а days_since се мери от днес: същите точки, от които е етикетът.
  5. Разделяне: 959 клиенти за обучение и 320 за тест, от 1 279.
  6. С бъдещето в признаците точността е 99,7%. Базовата линия „всички се връщат“ е 72,5%.
  7. Честно: признаците спират на 1 юли. Точките вдясно не се броят, а days_since се мери от стената.
  8. Честната точност е 72,2%, под базовата линия. 99,7% беше изтичане, не модел.

Клиентите в сцената са схема: истински са числата в надписите, от броя на клиентите до двете точности.

Пробвай

Още един признак

1. На 1 юли решаваш на кого да пратиш купон. Кой признак е честен за модела?

Колегата иска още един признак: recent, броят поръчки на клиента в последните 30 дни. Сметни го честно, към 1 юли: в month поръчките от 30-те дни преди cutoff, в people['recent'] броя им за всеки клиент (0 за тези без поръчка), а в score точността на модела с четирите признака.

Pythonданни: таблиците на магазина

Идеята

Подготовка преди разделянето

Логистичната регресия работи по-добре, когато признаците са в сходен мащаб: days_since стига до 240, а orders рядко минава 10. StandardScaler изважда от всеки признак средната му и дели на стандартното отклонение. Така всеки признак става около 0, с разсейване 1. Средните скалерът научава с fit, от данните, които му даваш. Колегата му дава всички:

Pythonданни: таблиците на магазина

Скалерът е учен от всичките 1 279 клиенти: средните са 5,88 поръчки, 67,42 дни и 10,69 €. Само обучението има 5,96, 67,84 и 10,68. Разликата идва от 320-те тестови клиенти: те са повлияли на подготовката, преди моделът да ги види. Тестът вече не е съвсем непознат.

Тук точността не се мени: пак е 72,2%, защото средните са близки. Не винаги е така. Изтичането расте при малко данни и при подготовка, която гледа етикета, например когато избираш признаците по връзката им с lost във всички редове. Попълването на липсващи стойности със средната (fillna) е същият случай: средната се смята само от обучението.

Идеята

Pipeline: подготовката учи само от обучението

Pipeline свързва подготовката и модела в един обект. make_pipeline(StandardScaler(), LogisticRegression()) прави верига от две стъпки:

  • pipe.fit(X_train, y_train) учи скалера и после модела, само от обучението;
  • pipe.score(X_test, y_test) и pipe.predict мащабират теста с научените средни и питат модела.
Pythonданни: таблиците на магазина

Средните на скалера в pipe са 5,96, 67,84 и 10,68: точно тези на обучението. Точността е 72,2%. Сега редът не може да се сбърка: подготовката е част от модела. Pipeline помага и в следващия урок: при кръстосаната проверка всяко разделяне учи скалера наново.

1. Какво прави pipe.score(X_test, y_test), когато pipe = make_pipeline(StandardScaler(), LogisticRegression()) е учен с обучението?
По-точно, за любопитните

pipe[0] е първата стъпка, а pipe[-1] моделът. В една верига може да има повече стъпки: SimpleImputer попълва липсващите, OneHotEncoder прави колони 0/1 от категории, а ColumnTransformer прилага различна подготовка на различни колони. Всички се учат с един fit, само от обучението.

Идеята

Един клиент в обучението и в теста

Таблицата е слепена от два експорта и всеки клиент е в нея два пъти. В кода това прави pd.concat: слага двете таблици една под друга. Изглежда безобидно: същите данни, само повече. Пробваш дърво на решенията (DecisionTreeClassifier), което расте без ограничение на дълбочината:

Pythonданни: таблиците на магазина

Редовете са 2 558, а 1 279 от тях са повторения. С дубликатите дървото познава 89,4% от теста, над базовата линия. Без тях познава 58,4%. При случайно разделяне повечето тестови редове имат близнак в обучението, а дървото без ограничение запомня всеки ред. То не прогнозира, а си спомня. Нов клиент няма близнак: 58,4% е истинското число, под базовата линия, защото дървото е преобучено. За това е следващият урок.

Правилото: махаш дубликатите (drop_duplicates) преди разделянето. Тук reset_index() прави customer_id колона, за да се махат повторенията на един и същ клиент, а не двама различни клиенти с еднакви числа.

По-точно, за любопитните

Понякога един клиент има няколко истински реда, например по един за всяка поръчка. Дубликати няма, но ако редовете му са и в обучението, и в теста, моделът пак учи клиента, а не правилото. Тогава разделяш по клиенти: GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=42).split(X, y, groups=rows['customer_id']) слага всички редове на един клиент от едната страна.

Идеята

Случайни дни от времеви ред

В урока за регресията разделяше по време. Ето защо случайното разделяне лъже. Прогнозата е за поръчките на ден, по номера на деня и деня от седмицата. Моделът е случайна гора (RandomForestRegressor): много дървета, всяко учено на малко различни редове, а прогнозата е средната им.

Pythonданни: таблиците на магазина

При случайни дни гората греши средно с 4,88 поръчки на ден, а средната (DummyRegressor) с 5,76: гората изглежда по-добра. Учена до 31 юли и пробвана на август–октомври, гората греши със 7,96, а средната с 5,44. Гората е по-лоша от средната.

При случайно разделяне, за да познае 15 септември, гората е видяла 14 и 16 септември. В живота на 31 юли нямаш нито един ден от септември. Освен това дърветата не продължават тенденция: ден след последния познат е за тях като последните познати дни. А през август–октомври има и изненада: септември е по-слаб. Защо, ще разберем в проекта. Модел, учен до юли, не може да го знае, и ти на 31 юли също. Затова 7,96 е честното число.

Упражнение

Поправи модела на колегата

Поправи кода на колегата. Признаците са само от поръчките преди cutoff, а days_since се брои до cutoff. Скалерът и логистичната регресия са в pipe, учен само с обучението. Запиши в score точността на pipe върху теста.

Pythonданни: таблиците на магазина

Колегата има и модел за оборота на ден, без отказаните поръчки. Пробвай го честно. Учи случайната гора (n_estimators=50, random_state=42) и DummyRegressor с дните до 31 юли (late е маската на дните от 1 август). После в mae запиши грешката (MAE) на гората на август–октомври, а в base_mae грешката на средната.

Pythonданни: таблиците на магазина

Тест

Провери се

1. Модел за „ще се върне ли клиентът“ дава 99% точност, а базовата линия е 72,5%. Какво правиш първо?
2. Попълваш липсващите стойности в колона със средната ѝ. От кои редове смяташ средната?
3. След drop_duplicates точността на дървото пада от 89,4% на 58,4%. Кое число е вярното?
4. Предсказваш поръчките на ден за следващите три месеца. Как разделяш данните?
5. Защо слагаш скалера и модела в един Pipeline, вместо да мащабираш таблицата веднъж в началото?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·