Класификация
Накратко: Класификацията предсказва клас, например „ще купи пак“ или не; логистичната регресия дава вероятност, а прагът разменя precision срещу recall според това кои грешки са по-скъпи.
Проблем
„Ще купи ли клиентът пак?“
Маркетингът иска да прати купон за отстъпка на клиентите, които иначе ще спрат да купуват. Купон за всички струва много, а повечето клиенти и без това се връщат. Въпросът е: на кого?
Нека се върнем във времето. Застани на 1 юли 2026: това е границата. За всеки клиент знаеш поръчките му до тази дата. Колко са, преди колко дни е последната, колко е средната кошница. Дали ще поръча пак, ще разбереш от юли до края на октомври:
Преди 1 юли са купували 1 279 клиенти. След това 72,6% от тях са поръчали пак. Колоната lost е 1 за останалите: те не са поръчали нищо за 4 месеца. Това са хората, за които е купонът.
По-точно, за любопитните
Тук броим всяка поръчка, и отказаните: въпросът е дали клиентът идва, а не колко оборот носи. basket е средната стойност на кошницата му. ~people.index.isin(...) е маската „клиентът го няма след границата“: ~ обръща True и False.
Пробвай
Колко е даром?
Запиши в lost_share каква част от клиентите са загубени, а в baseline_acc колко често познава правилото „всички се връщат“, без никакъв модел.
Решение
import pandas as pd
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]
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
lost_share = people['lost'].mean()
baseline_acc = (people['lost'] == 0).mean()
print(round(lost_share, 3), round(baseline_acc, 3))people['lost'].mean() дава 0,274: 27,4% са загубени. Правилото „всички се връщат“ греши точно при тях и познава останалите: 1 − 0,274 = 0,726. Това е базовата линия от урока за работния процес. Всеки модел за този въпрос трябва да е над 72,6%, иначе не ти трябва.
Идеята
Класификация: отговорът е клас
В регресията прогнозата е число: оборот за ден. Тук отговорът е „да“ или „не“: загубен ли е клиентът. Такава задача е класификация, а възможните отговори са класове. Пътят е същият: признаци X, етикет y, разделяне, fit, predict.
Първият модел е логистична регресия (LogisticRegression). Името е подвеждащо: тя класифицира. Като линейната регресия събира признаците с тегла, но после свива сумата в шанс между 0 и 1. Срещу нея слагаме DummyClassifier, който винаги казва най-честия клас:
Правилото „всички се връщат“ познава 72,5% от тестовите клиенти. Моделът познава 72,2%: по-малко от правилото. Точността (accuracy, делът на верните отговори) тук лъже. Когато загубените са около всеки четвърти, „всички остават“ е почти винаги вярно.
По-точно, за любопитните
stratify=y разделя така, че делът на загубените е еднакъв в обучението и в теста: 27,5%. Без него при неравни класове тестът може случайно да получи повече или по-малко загубени. Тук е честно да разделиш по клиенти на случаен принцип: признаците на всеки клиент са от преди границата, а етикетът е от след нея. max_iter=1000 дава на модела повече стъпки да намери теглата: признаците са в различен мащаб (поръчки до 77, дни до 240).
Идеята
Матрица на объркването
Точността е едно число за всички грешки. А грешките не са еднакви. Пропуснат загубен клиент е изгубен приход. Купон за клиент, който щеше да се върне, е подарена отстъпка. Таблицата, която ги брои поотделно, се казва матрица на объркването (confusion matrix):
Редовете са истината, колоните са какво казва моделът. От 88 загубени клиенти моделът посочва 1. Останалите 87 ги изпуска. Затова е точен колкото правилото: и той почти винаги казва „остава“.
Идеята
Precision и recall: кой получава купон
Две числа описват купоните по-добре от точността:
- precision: от клиентите с купон колко наистина щяха да си тръгнат. Ниско значи много подарени отстъпки;
- recall: от всички, които щяха да си тръгнат, колко получават купон. Ниско значи много изпуснати клиенти.
Моделът дава 3 купона. Един отива при загубен клиент: precision е 0,333. От 88-те загубени хваща 1: recall е 0,011. Такъв модел не помага на маркетинга, каквато и да е точността му.
Идеята
Шансът и прагът
predict не е всичко, което моделът знае. predict_proba дава шанс за всеки клас. Колоните са в реда на model.classes_: първата е за 0 (остава), втората за 1 (загубен):
Най-високият шанс за загуба в теста е 0,56. predict дава купон само ако шансът е над 0,5, а такива клиенти са 3. Но 0,5 не е закон. Числото, над което казваш „да“, се казва праг (threshold), и ти го избираш. При праг 0,3 купоните са 160:
От 160 купона 63 отиват при загубени клиенти: precision е 39,4%. От 88-те загубени хващаш 63: recall е 71,6%. Точността пада, но моделът вече работи. За сравнение: 160 купона на случайни клиенти биха имали precision колкото дела на загубените, 27,5%. Моделът подрежда клиентите по-добре от случая, макар и далеч от идеално.
По-точно, за любопитните
Друг модел дава подобна картина. Дърво на решенията с дълбочина 3 също е точно колкото правилото, а при праг 0,3 има precision 38,8% и recall 64,8%:
С тези три признака повече не се изкарва. Ако искаш по-добър модел, търси признаци, които казват повече за клиента: какви продукти купува, колко редовно поръчва.
Пробвай
Сметни ги сам
При праг 0,4 преброй fp (купон за клиент, който остава) и fn (загубен клиент без купон), после сметни precision и recall от tp, fp и fn, без sklearn.
Решение
import pandas as pd
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]
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
X = people[['orders', 'days_since', 'basket']]
y = people['lost']
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)
proba = model.predict_proba(X_test)[:, 1]
pred = (proba >= 0.4).astype(int)
truth = y_test.to_numpy()
tp = ((pred == 1) & (truth == 1)).sum()
fp = ((pred == 1) & (truth == 0)).sum()
fn = ((pred == 0) & (truth == 1)).sum()
precision = tp / (tp + fp)
recall = tp / (tp + fn)
print(tp, fp, fn, round(precision, 3), round(recall, 3))При праг 0,4 tp = 37, fp = 44 и fn = 51. Precision = 37 / (37 + 44) = 0,457: делиш на всички с купон. Recall = 37 / (37 + 51) = 0,42: делиш на всички загубени. Ако ги размениш, отговаряш на другия въпрос.
Идеята
Прагът мени едното за другото
Всеки праг дава различни купони. Ето всички прагове от 0,1 до 0,5:
При праг 0,1 купоните са 262 и хващаш 96,6% от загубените, но precision е 32,4%: малко над 27,5%-те на случайните купони. При 0,4 купоните са 81 и precision е 45,7%, но изпускаш повече от половината загубени. При 0,5 купоните са само 3. Няма праг, който е най-добър сам по себе си. Изборът зависи от цената: колко струва купонът и колко струва изгубеният клиент.
Упражнение
Купоните на маркетинга
Маркетингът има 80 купона за клиентите в теста. Дай ги на 80-те с най-висок шанс за загуба: proba.argsort() дава позициите от най-малкия към най-големия шанс. Запиши в hits колко от тях са наистина загубени, в precision дела им сред 80-те, а в recall каква част от всички загубени в теста получават купон.
Решение
import pandas as pd
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]
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
X = people[['orders', 'days_since', 'basket']]
y = people['lost']
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)
proba = model.predict_proba(X_test)[:, 1]
top = proba.argsort()[::-1][:80]
hits = y_test.to_numpy()[top].sum()
precision = hits / 80
recall = hits / y_test.sum()
print(hits, round(precision, 3), round(recall, 3))proba.argsort()[::-1][:80] дава позициите на 80-те с най-висок шанс. От тях 36 са загубени: precision 0,45, recall 36 / 88 = 0,409. 80 случайни купона щяха да намерят около 22 загубени. С argsort() без обръщане даваш купоните на най-сигурните клиенти, а с колоната [:, 0] подреждаш по шанса клиентът да остане.
Маркетингът казва: „Искаме да стигнем поне 80% от загубените.“ Намери в best най-високия праг от 0,05 до 0,5, при който recall е поне 0,8, а в best_precision precision при него.
Решение
import pandas as pd
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]
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
X = people[['orders', 'days_since', 'basket']]
y = people['lost']
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)
proba = model.predict_proba(X_test)[:, 1]
import numpy as np
from sklearn.metrics import precision_score, recall_score
best = None
for t in np.arange(0.05, 0.51, 0.05).round(2):
pred = (proba >= t).astype(int)
if recall_score(y_test, pred) >= 0.8:
best = t
pred = (proba >= best).astype(int)
best_precision = precision_score(y_test, pred)
print(best, round(best_precision, 3))Recall е поне 0,8 до праг 0,25. При него купоните са 193, recall е 86,4%, а precision 39,4%. По-нисък праг също стига 80%, но с повече излишни купони. Затова търсиш най-високия праг, който още изпълнява целта.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Дати и времеви редове · Първата графика · Модел: учене от примери · Линейна регресия
- Продължава в: Изтичане на данни (data leakage) · Кръстосана проверка (cross-validation)
- Задачи по темата: Модели със scikit-learn