Mundarija (22)
- 1. Kirish va motivatsiya
- 2. Nazariya — chuqur tushuntirish
- 2.1. Sana komponentlari
- 2.2. Lag va harakatlanuvchi oyna
- 2.3. O'tgan vaqt belgilari
- 2.4. Vaqt leakage
- 2.5. TimeSeriesSplit
- 2.6. Bayram va tashqi kalendar
- 2.7. Tuzoqlar
- 2.8. Vaqt — boy, lekin xavfli
- 3. Tez ma'lumotnoma
- 4. Batafsil misollar
- Misol 1 — Sana komponentlari
- Misol 2 — Lag, oyna va shift(1)
- Misol 3 — O'tgan vaqt va validatsiya
- Misol 4 — Bayram va gap
- 5. To'g'ri va noto'g'ri tushunishlar
- 6. Keng tarqalgan xatolar va yechimlari
- 7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 8. Eng yaxshi amaliyotlar
- 9. Amaliy topshiriq
- Xulosa
17.5-dars: Sana va vaqt belgilari
17-QISM — FEATURE ENGINEERING · 5-dars
1. Kirish va motivatsiya
Sana ustuni — modelga to'g'ridan-to'g'ri berib bo'lmaydigan, lekin eng boy ma'lumot manbalaridan biri. Bitta 2026-03-14 09:15 qiymatidan o'nlab belgi chiqarish mumkin: hafta kuni, oyning kuni, soat, ish vaqtimi, bayram oldidami, oxirgi hodisadan qancha vaqt o'tgan.
Vaqt belgilari ikki toifaga bo'linadi: komponentlar (sanadan to'g'ridan-to'g'ri chiqariladi) va tarixga asoslangan (lag, harakatlanuvchi oyna, oxirgi hodisadan o'tgan vaqt). Ikkinchisi ancha kuchliroq va ancha xavfliroq.
Xavf — vaqt leakage: "so'nggi 30 kundagi o'rtacha" belgisini hisoblashda kelajakdagi kunlarni qo'shib yuborish juda oson, natijada model CV da ajoyib, ishlab chiqarishda esa yaroqsiz bo'ladi.
Bu darsda: sana komponentlari, davriy kodlash, lag va harakatlanuvchi oyna belgilari, o'tgan vaqt belgilari, vaqt leakage va to'g'ri validatsiya (TimeSeriesSplit).
Real vaziyat. Yetkazib berish kechikishini bashorat qilishda model 0.91 AUC berdi. Ishlab chiqarishda 0.62. Sabab: "shu kundagi o'rtacha kechikish" belgisi butun kun ma'lumotidan hisoblangan edi — ertalabki buyurtma uchun kechqurungi ma'lumot ishlatilgan. To'g'ri (kengayuvchi) oyna bilan CV 0.68 chiqdi va ishlab chiqarishda 0.67 bo'ldi.
Bu darsda vaqt belgilarini o'rganamiz.
Bu darsda:
- Sana komponentlari
- Lag va harakatlanuvchi oyna
- O'tgan vaqt belgilari
- Vaqt leakage
- TimeSeriesSplit
- Bayram va tashqi kalendar
- Tuzoqlar
- Amaliy: savdo bashorati
ℹ Misollar real numpy/pandas/sklearn bilan (Python 3.14).
2. Nazariya — chuqur tushuntirish
2.1. Sana komponentlari
d = df["sana"].dt
df["yil"], df["oy"], df["kun"] = d.year, d.month, d.day
df["hafta_kuni"] = d.dayofweek # 0 = dushanba
df["hafta"] = d.isocalendar().week
df["chorak"] = d.quarter
df["yil_kuni"] = d.dayofyear
df["oy_boshi"] = d.is_month_start.astype(int)
df["oy_oxiri"] = d.is_month_end.astype(int)
df["hafta_oxiri"] = (d.dayofweek >= 5).astype(int)
df["soat"], df["daqiqa"] = d.hour, d.minuteQAYSI KOMPONENT KERAK - vazifaga bog'liq:
savdo -> hafta kuni, oy, bayram
veb-trafik -> soat, hafta kuni
energiya -> soat, mavsum, harorat
moliya -> ish kuni, chorak oxiri
EHTIYOT: "yil" belgisi EKSTRAPOLYATSIYA muammosini beradi 15.1-bob
daraxt 2027 yilni ko'rmagan -> 2026 dagi qiymatni qaytaradi"Yil" belgisidan ehtiyot bo'ling: daraxtlar ekstrapolyatsiya qilmaydi, shuning uchun kelajak yil uchun model oxirgi ko'rgan yil qiymatini qaytaradi. Trend kerak bo'lsa, uni alohida modellang 15.1-bob.
2.2. Lag va harakatlanuvchi oyna
df = df.sort_values(["mijoz", "sana"])
g = df.groupby("mijoz")["summa"]
df["lag_1"] = g.shift(1) # oldingi qiymat
df["lag_7"] = g.shift(7)
df["oyna_7"] = g.shift(1).rolling(7).mean() # SHIFT(1) MAJBURIY
df["oyna_30_std"] = g.shift(1).rolling(30).std()
df["kengayuvchi"] = g.shift(1).expanding().mean()SHIFT(1) NEGA MAJBURIY:
rolling(7).mean() JORIY qiymatni ham o'z ichiga oladi
-> maqsadni bashorat qilishda o'z javobini ko'radi -> LEAKAGE
TO'G'RI TARTIB: shift(1) -> rolling(n) -> agregat
Foydali agregatlar: mean, std, min, max, median, sum, count
Nisbatlar: joriy / oyna_o'rtachasi -> odatdan chetlanish shift(1) unutilishi — vaqt leakage ning eng keng tarqalgan ko'rinishi. rolling(7).mean() joriy qatorni ham hisobga oladi; to'g'risi — avval shift(1), keyin rolling.
2.3. O'tgan vaqt belgilari
# oxirgi hodisadan o'tgan vaqt
df["oxirgi_xariddan"] = (df["sana"] - g["sana"].shift(1)).dt.days
# ro'yxatdan o'tgandan beri
df["mijoz_yoshi"] = (df["sana"] - df["royxat_sanasi"]).dt.days
# keyingi hodisagacha (LEAKAGE - faqat tahlil uchun)
df["keyingi_xaridgacha"] = (g["sana"].shift(-1) - df["sana"]).dt.daysJUDA KUCHLI BELGILAR:
recency (oxirgi hodisadan o'tgan kun) - RFM ning R komponenti
frekventlik (hodisalar orasidagi o'rtacha interval)
tezlashuv (oxirgi interval / o'rtacha interval)
EHTIYOT: shift(-1) - bu KELAJAK, leakage"Oxirgi hodisadan o'tgan vaqt" — churn va tavsiya modellarida eng kuchli belgilardan biri: u mijozning faolligini bitta songa siqadi.
2.4. Vaqt leakage
UCH KO'RINISH:
1. ROLLING da shift yo'q
rolling(7).mean() joriy qiymatni o'z ichiga oladi
2. TASODIFIY CV
train_test_split(shuffle=True) -> kelajak o'quvda, o'tmish testda
-> model "kelajakni ko'rgan"
3. BUTUN MA'LUMOTDAN agregatsiya
df.groupby("mijoz")["summa"].transform("mean") - butun davr bo'yicha
-> kelajakdagi xaridlar ham hisobga olingan
TEKSHIRUV:
- har belgi uchun "bu sanada ma'lum bo'lganmi?" savoli
- vaqt bo'yicha validatsiya (TimeSeriesSplit)
- CV va ishlab chiqarish natijasini solishtirish Tasodifiy CV vaqt ma'lumotida noto'g'ri: u modelga kelajakni ko'rsatadi va natijani optimistik qiladi. TimeSeriesSplit yoki qo'lda sana bo'yicha bo'lish kerak.
2.5. TimeSeriesSplit
from sklearn.model_selection import TimeSeriesSplit
cv = TimeSeriesSplit(n_splits=5) # kengayuvchi oyna
cv = TimeSeriesSplit(n_splits=5, max_train_size=10000) # siljuvchi oyna
cv = TimeSeriesSplit(n_splits=5, gap=100) # oraliq (leakage ga qarshi)
for tr, te in cv.split(X):
# tr har doim te DAN OLDIN keladi
...gap NEGA KERAK:
bashorat gorizonti bor bo'lsa (masalan 7 kun oldin bashorat)
-> o'quv va test orasida 7 kunlik BO'SHLIQ qoldiring
-> aks holda model "ertangi" ma'lumotni ko'radi
MUHIM: ma'lumot SANA bo'yicha saralangan bo'lishi kerak gap parametri bashorat gorizonti bor vazifalarda majburiy: agar siz 7 kun oldin bashorat qilsangiz, o'quv va test orasida ham 7 kunlik bo'shliq bo'lishi kerak.
2.6. Bayram va tashqi kalendar
KALENDAR BELGILARI:
bayram kunimi (davlat bayramlari ro'yxati)
bayram oldi / keyingi kun
maktab ta'tili
ish kunlari soni (oy ichida)
maosh kuni (oyning 5 va 20-kuni kabi)
TASHQI MANBALAR:
ob-havo (harorat, yog'ingarchilik)
valyuta kursi
aksiya/chegirma kalendari
QO'SHIMCHA: pandas.tseries.holiday yoki qo'lda ro'yxat
workalendar, holidays kutubxonalari (mamlakat bo'yicha)Bayram belgilari savdo modellarida ko'pincha eng katta foyda beradi: ular modelga hech qanday algoritm topa olmaydigan tashqi bilim beradi.
2.7. Tuzoqlar
Asosiy tuzoqlar: rolling da shift(1) ni unutish; vaqt ma'lumotida tasodifiy CV; butun davr bo'yicha agregatsiya; shift(-1) (kelajak) ishlatish; "yil" belgisidan ekstrapolyatsiya kutish; davriy belgilarni xom qoldirish; gap ni hisobga olmaslik; ma'lumotni saralamasdan rolling qo'llash.
2.8. Vaqt — boy, lekin xavfli
Sanadan komponentlar (hafta kuni, soat, oy) va davriy kodlash (sin/cos) olinadi. Lag va harakatlanuvchi oyna ancha kuchliroq, lekin shift(1) majburiy — aks holda joriy qiymat o'z bashoratiga kiradi. "Oxirgi hodisadan o'tgan vaqt" eng kuchli belgilardan biri. Validatsiya TimeSeriesSplit bilan (kerak bo'lsa gap bilan), tasodifiy CV noto'g'ri. Bayram va tashqi kalendar ko'pincha katta foyda beradi. Keyingi dars — matn belgilari.
3. Tez ma'lumotnoma
import numpy as np
import pandas as pd
from sklearn.model_selection import TimeSeriesSplit
d = df["sana"].dt
df["hafta_kuni"], df["soat"], df["oy"] = d.dayofweek, d.hour, d.month
df["hafta_oxiri"] = (d.dayofweek >= 5).astype(int)
df["soat_sin"] = np.sin(2 * np.pi * d.hour / 24)
df["soat_cos"] = np.cos(2 * np.pi * d.hour / 24)
df = df.sort_values(["mijoz", "sana"])
g = df.groupby("mijoz")["summa"]
df["lag_1"] = g.shift(1)
df["oyna_7"] = g.shift(1).rolling(7).mean() # shift MAJBURIY
df["oxirgidan"] = (df["sana"]
- df.groupby("mijoz")["sana"].shift(1)).dt.days
cv = TimeSeriesSplit(n_splits=5, gap=100)
QOIDA: shift(1) qo'y · sana bo'yicha sarala · TimeSeriesSplit ishlat ·
butun davrdan agregatsiya qilmaVaqt belgilari xulosasi
Komponentlar: hafta kuni, soat, oy, bayram; davriylarni sin/cos bilan
Lag va oyna: g.shift(1).rolling(n).mean() - shift MAJBURIY
O'tgan vaqt: oxirgi hodisadan o'tgan kun - eng kuchli belgilardan
Validatsiya: TimeSeriesSplit (gap bilan); tasodifiy CV NOTO'G'RI4. Batafsil misollar
Misollar real numpy/pandas/sklearn bilan (Python 3.14).
Misol 1 — Sana komponentlari
"""Bitta ustundan o'nlab belgi (real pandas/sklearn)."""
import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
def yarat(seed: int = 5, kunlar: int = 900) -> pd.DataFrame:
"""Kunlik savdo: hafta kuni, oy va bayram naqshlari."""
rng = np.random.default_rng(seed)
sanalar = pd.date_range("2024-01-01", periods=kunlar, freq="D")
hafta_kuni = sanalar.dayofweek.to_numpy()
oy = sanalar.month.to_numpy()
bayramlar = {(1, 1), (3, 8), (3, 21), (9, 1), (10, 1), (12, 8)}
bayram = np.array([(m, d) in bayramlar
for m, d in zip(oy, sanalar.day.to_numpy())])
savdo = (1000
+ 260 * (hafta_kuni >= 5) # hafta oxiri
+ 150 * np.sin(2 * np.pi * oy / 12) # mavsumiylik
+ 700 * bayram # bayram
+ 180 * np.isin(sanalar.day.to_numpy(), [5, 20]) # maosh kuni
+ rng.normal(0, 90, kunlar))
return pd.DataFrame({"sana": sanalar, "savdo": savdo,
"bayram": bayram.astype(int)})
def main() -> None:
df = yarat()
y = df["savdo"].to_numpy()
cv = TimeSeriesSplit(n_splits=5)
print("=== 1. Ma'lumot ===")
print(f" {len(df)} kun: {df['sana'].min().date()} .. "
f"{df['sana'].max().date()}")
print(f" savdo: o'rtacha {y.mean():.0f}, std {y.std():.0f}")
print(f" bayram kunlari: {int(df['bayram'].sum())}")
print("\n=== 2. Komponentlarni chiqarish ===")
d = df["sana"].dt
df["hafta_kuni"] = d.dayofweek
df["oy"] = d.month
df["kun"] = d.day
df["yil_kuni"] = d.dayofyear
df["hafta_oxiri"] = (d.dayofweek >= 5).astype(int)
df["oy_boshi"] = d.is_month_start.astype(int)
df["oy_oxiri"] = d.is_month_end.astype(int)
df["chorak"] = d.quarter
komponentlar = ["hafta_kuni", "oy", "kun", "yil_kuni", "hafta_oxiri",
"oy_boshi", "oy_oxiri", "chorak"]
print(f" {len(komponentlar)} ta belgi: {komponentlar}")
print(f" {'belgi':<14} {'savdo bilan korrelyatsiya':>26}")
for nom in komponentlar:
print(f" {nom:<14} {df[nom].corr(df['savdo']):>+26.4f}")
print("\n=== 3. Belgilar guruhlarining hissasi ===")
def model():
return HistGradientBoostingRegressor(learning_rate=0.1, max_iter=200,
random_state=0)
guruhlar = {
"faqat yil_kuni": ["yil_kuni"],
"hafta": ["hafta_kuni", "hafta_oxiri"],
"hafta + oy": ["hafta_kuni", "hafta_oxiri", "oy"],
"barcha komponentlar": komponentlar,
"komponentlar + bayram": komponentlar + ["bayram"],
}
print(f" {'guruh':<24} {'belgilar':>9} {'CV R^2':>9}")
for nom, ustunlar in guruhlar.items():
b = cross_val_score(model(), df[ustunlar], y, cv=cv,
scoring="r2").mean()
print(f" {nom:<24} {len(ustunlar):>9} {b:>9.4f}")
print("\n=== 4. Davriy kodlash ===")
df["oy_sin"] = np.sin(2 * np.pi * df["oy"] / 12)
df["oy_cos"] = np.cos(2 * np.pi * df["oy"] / 12)
df["hafta_sin"] = np.sin(2 * np.pi * df["hafta_kuni"] / 7)
df["hafta_cos"] = np.cos(2 * np.pi * df["hafta_kuni"] / 7)
from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
variantlar = {
"xom (oy, hafta_kuni)": ["oy", "hafta_kuni", "bayram"],
"sin/cos": ["oy_sin", "oy_cos", "hafta_sin", "hafta_cos", "bayram"],
"ikkalasi": ["oy", "hafta_kuni", "oy_sin", "oy_cos", "hafta_sin",
"hafta_cos", "bayram"],
}
print(f" {'variant':<24} {'Ridge R^2':>11} {'HistGB R^2':>12}")
for nom, ustunlar in variantlar.items():
r = cross_val_score(Pipeline([("sc", StandardScaler()),
("m", Ridge(alpha=1.0))]),
df[ustunlar], y, cv=cv, scoring="r2").mean()
g = cross_val_score(model(), df[ustunlar], y, cv=cv,
scoring="r2").mean()
print(f" {nom:<24} {r:>11.4f} {g:>12.4f}")
print(" ⭐ Chiziqli modelga sin/cos, daraxtga xom ham yetarli")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Ma'lumot ===
900 kun: 2024-01-01 .. 2026-06-18
savdo: o'rtacha 1115, std 205
bayram kunlari: 15
=== 2. Komponentlarni chiqarish ===
8 ta belgi: ['hafta_kuni', 'oy', 'kun', 'yil_kuni', 'hafta_oxiri', 'oy_boshi', 'oy_oxiri', 'chorak']
belgi savdo bilan korrelyatsiya
hafta_kuni +0.4124
oy -0.4117
kun -0.0833
yil_kuni -0.4176
hafta_oxiri +0.5374
oy_boshi +0.1393
oy_oxiri -0.0256
chorak -0.4349
=== 3. Belgilar guruhlarining hissasi ===
guruh belgilar CV R^2
faqat yil_kuni 1 -0.1267
hafta 2 -0.0267
hafta + oy 3 0.2602
barcha komponentlar 8 0.2929
komponentlar + bayram 9 0.2929
=== 4. Davriy kodlash ===
variant Ridge R^2 HistGB R^2
xom (oy, hafta_kuni) 0.0356 0.2602
sin/cos 0.6100 0.2743
ikkalasi 0.6239 0.2578
⭐ Chiziqli modelga sin/cos, daraxtga xom ham yetarliNima ko'rsatdi: 2.1-bo'lim.
Misol 2 — Lag, oyna va shift(1)
"""Eng kuchli belgilar va eng keng tarqalgan xato (real pandas/sklearn)."""
import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.metrics import r2_score
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
def yarat(seed: int = 11, mijozlar: int = 200, kunlar: int = 180) -> pd.DataFrame:
"""Har mijozning kunlik xaridi: inersiya bilan."""
rng = np.random.default_rng(seed)
sanalar = pd.date_range("2025-01-01", periods=kunlar, freq="D")
qatorlar = []
for m in range(mijozlar):
daraja = rng.lognormal(10.5, 0.5)
holat = daraja
for s in sanalar:
holat = 0.75 * holat + 0.25 * daraja * rng.lognormal(0, 0.35)
qatorlar.append((m, s, holat * (1.25 if s.dayofweek >= 5 else 1.0)))
return pd.DataFrame(qatorlar, columns=["mijoz", "sana", "summa"])
def main() -> None:
df = yarat().sort_values(["mijoz", "sana"]).reset_index(drop=True)
print("=== 1. Ma'lumot ===")
print(f" {len(df)} qator, {df['mijoz'].nunique()} mijoz, "
f"{df['sana'].nunique()} kun")
print(f" maqsad: keyingi kungi summa")
# maqsad: keyingi kungi summa
df["maqsad"] = df.groupby("mijoz")["summa"].shift(-1)
df = df.dropna(subset=["maqsad"]).reset_index(drop=True)
g = df.groupby("mijoz")["summa"]
print("\n=== 2. NOTO'G'RI: shift(1) siz rolling ===")
df["oyna7_notogri"] = g.transform(lambda s: s.rolling(7, min_periods=1).mean())
df["oyna7_togri"] = g.transform(
lambda s: s.shift(1).rolling(7, min_periods=1).mean())
print(f" joriy summa bilan korrelyatsiya:")
print(f" shift siz: {df['oyna7_notogri'].corr(df['summa']):.4f}")
print(f" shift bilan: {df['oyna7_togri'].corr(df['summa']):.4f}")
print(" (shift siz oyna joriy qiymatni o'z ichiga oladi)")
print("\n=== 3. Belgilar to'plamlari ===")
df["hafta_kuni"] = df["sana"].dt.dayofweek
df["lag_1"] = g.shift(1)
df["lag_7"] = g.shift(7)
df["oyna7_std"] = g.transform(
lambda s: s.shift(1).rolling(7, min_periods=2).std())
df["nisbat"] = df["summa"] / df["oyna7_togri"]
toza = df.dropna(subset=["lag_7", "oyna7_std"]).reset_index(drop=True)
y = toza["maqsad"].to_numpy()
cv = TimeSeriesSplit(n_splits=4)
toza = toza.sort_values("sana").reset_index(drop=True)
y = toza["maqsad"].to_numpy()
def model():
return HistGradientBoostingRegressor(learning_rate=0.1, max_iter=200,
random_state=0)
to_plamlar = {
"faqat joriy summa": ["summa"],
"+ hafta kuni": ["summa", "hafta_kuni"],
"+ lag": ["summa", "hafta_kuni", "lag_1", "lag_7"],
"+ oyna (to'g'ri)": ["summa", "hafta_kuni", "lag_1", "lag_7",
"oyna7_togri", "oyna7_std"],
"+ nisbat": ["summa", "hafta_kuni", "lag_1", "lag_7", "oyna7_togri",
"oyna7_std", "nisbat"],
}
print(f" {'to_plam':<22} {'belgilar':>9} {'CV R^2':>9}")
for nom, ustunlar in to_plamlar.items():
b = cross_val_score(model(), toza[ustunlar], y, cv=cv,
scoring="r2").mean()
print(f" {nom:<22} {len(ustunlar):>9} {b:>9.4f}")
print("\n=== 4. Leakage ning ta'siri ===")
oxirgi = toza["sana"].max() - pd.Timedelta(days=20)
tr = toza["sana"] <= oxirgi
te = ~tr
togri_ust = ["summa", "hafta_kuni", "lag_1", "oyna7_togri"]
notogri_ust = ["summa", "hafta_kuni", "lag_1", "oyna7_notogri"]
print(f" {'variant':<22} {'CV R^2':>9} {'kelajak testda R^2':>21}")
for nom, ustunlar in [("to'g'ri (shift bilan)", togri_ust),
("noto'g'ri (shift siz)", notogri_ust)]:
cvb = cross_val_score(model(), toza[ustunlar], y, cv=cv,
scoring="r2").mean()
m = model().fit(toza.loc[tr, ustunlar], y[tr.to_numpy()])
test = r2_score(y[te.to_numpy()],
m.predict(toza.loc[te, ustunlar]))
print(f" {nom:<22} {cvb:>9.4f} {test:>21.4f}")
print(" ⭐ shift(1) - vaqt belgilarining birinchi qoidasi")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Ma'lumot ===
36000 qator, 200 mijoz, 180 kun
maqsad: keyingi kungi summa
=== 2. NOTO'G'RI: shift(1) siz rolling ===
joriy summa bilan korrelyatsiya:
shift siz: 0.9503
shift bilan: 0.9390
(shift siz oyna joriy qiymatni o'z ichiga oladi)
=== 3. Belgilar to'plamlari ===
to_plam belgilar CV R^2
faqat joriy summa 1 0.8885
+ hafta kuni 2 0.9561
+ lag 4 0.9575
+ oyna (to'g'ri) 6 0.9570
+ nisbat 7 0.9573
=== 4. Leakage ning ta'siri ===
variant CV R^2 kelajak testda R^2
to'g'ri (shift bilan) 0.9576 0.9582
noto'g'ri (shift siz) 0.9575 0.9580
⭐ shift(1) - vaqt belgilarining birinchi qoidasiNima ko'rsatdi: 2.2, 2.4-bo'limlar.
Misol 3 — O'tgan vaqt va validatsiya
"""Recency belgilari va TimeSeriesSplit (real pandas/sklearn)."""
import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import (StratifiedKFold, TimeSeriesSplit,
cross_val_score)
def yarat(seed: int = 7, mijozlar: int = 3000) -> pd.DataFrame:
"""Churn: oxirgi xariddan o'tgan vaqt hal qiluvchi."""
rng = np.random.default_rng(seed)
boshlanish = pd.Timestamp("2025-01-01")
qatorlar = []
for m in range(mijozlar):
faollik = rng.lognormal(2.2, 0.7) # o'rtacha interval (kun)
kuzatuv = boshlanish + pd.Timedelta(days=int(rng.integers(200, 420)))
oxirgi = kuzatuv - pd.Timedelta(days=int(rng.exponential(faollik * 1.4)))
xaridlar = int(rng.poisson(max((oxirgi - boshlanish).days / faollik, 1)))
otgan = (kuzatuv - oxirgi).days
kuch = -1.0 + 0.055 * otgan - 0.04 * xaridlar
churn = int(rng.random() < 1 / (1 + np.exp(-kuch)))
qatorlar.append((m, kuzatuv, oxirgi, xaridlar, faollik, churn))
df = pd.DataFrame(qatorlar, columns=["mijoz", "kuzatuv_sanasi",
"oxirgi_xarid", "xaridlar",
"faollik", "churn"])
return df.sort_values("kuzatuv_sanasi").reset_index(drop=True)
def main() -> None:
df = yarat()
y = df["churn"].to_numpy()
print("=== 1. Ma'lumot ===")
print(f" {len(df)} mijoz, churn ulushi {y.mean():.2%}")
print(f" kuzatuv sanalari: {df['kuzatuv_sanasi'].min().date()} .. "
f"{df['kuzatuv_sanasi'].max().date()}")
print("\n=== 2. O'tgan vaqt belgilari ===")
df["otgan_kun"] = (df["kuzatuv_sanasi"] - df["oxirgi_xarid"]).dt.days
df["ortacha_interval"] = np.where(
df["xaridlar"] > 0,
(df["oxirgi_xarid"] - pd.Timestamp("2025-01-01")).dt.days
/ df["xaridlar"].clip(lower=1), np.nan)
df["tezlashuv"] = df["otgan_kun"] / df["ortacha_interval"].clip(lower=1)
print(f" {'belgi':<20} {'churn bilan korrelyatsiya':>26}")
for nom in ["xaridlar", "otgan_kun", "ortacha_interval", "tezlashuv"]:
print(f" {nom:<20} {df[nom].corr(df['churn']):>+26.4f}")
print("\n=== 3. Belgilar to'plamlari ===")
def model():
return HistGradientBoostingClassifier(learning_rate=0.1, max_iter=200,
random_state=0)
cv = TimeSeriesSplit(n_splits=5)
to_plamlar = {
"faqat xaridlar": ["xaridlar"],
"+ o'tgan kun": ["xaridlar", "otgan_kun"],
"+ interval": ["xaridlar", "otgan_kun", "ortacha_interval"],
"+ tezlashuv": ["xaridlar", "otgan_kun", "ortacha_interval",
"tezlashuv"],
}
print(f" {'to_plam':<20} {'CV ROC AUC':>12}")
for nom, ustunlar in to_plamlar.items():
b = cross_val_score(model(), df[ustunlar], y, cv=cv,
scoring="roc_auc").mean()
print(f" {nom:<20} {b:>12.4f}")
print("\n=== 4. Tasodifiy CV va TimeSeriesSplit ===")
ustunlar = ["xaridlar", "otgan_kun", "ortacha_interval", "tezlashuv"]
tasodifiy = cross_val_score(model(), df[ustunlar], y,
cv=StratifiedKFold(5, shuffle=True,
random_state=0),
scoring="roc_auc")
vaqt = cross_val_score(model(), df[ustunlar], y,
cv=TimeSeriesSplit(n_splits=5), scoring="roc_auc")
print(f" tasodifiy CV: {tasodifiy.mean():.4f} "
f"(+-{tasodifiy.std():.4f})")
print(f" TimeSeriesSplit: {vaqt.mean():.4f} (+-{vaqt.std():.4f})")
# haqiqiy kelajak
chegara = df["kuzatuv_sanasi"].quantile(0.8)
tr = df["kuzatuv_sanasi"] <= chegara
m = model().fit(df.loc[tr, ustunlar], y[tr.to_numpy()])
kelajak = roc_auc_score(y[(~tr).to_numpy()],
m.predict_proba(df.loc[~tr, ustunlar])[:, 1])
print(f" haqiqiy kelajak: {kelajak:.4f}")
print(f" {'usul':<20} {'kelajakdan farq':>18}")
print(f" {'tasodifiy CV':<20} {tasodifiy.mean() - kelajak:>+18.4f}")
print(f" {'TimeSeriesSplit':<20} {vaqt.mean() - kelajak:>+18.4f}")
print(" ⭐ Vaqt ma'lumotida TimeSeriesSplit ishlating")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Ma'lumot ===
3000 mijoz, churn ulushi 21.97%
kuzatuv sanalari: 2025-07-20 .. 2026-02-24
=== 2. O'tgan vaqt belgilari ===
belgi churn bilan korrelyatsiya
xaridlar -0.3147
otgan_kun +0.5143
ortacha_interval +0.3615
tezlashuv +0.2644
=== 3. Belgilar to'plamlari ===
to_plam CV ROC AUC
faqat xaridlar 0.7902
+ o'tgan kun 0.8177
+ interval 0.8153
+ tezlashuv 0.8119
=== 4. Tasodifiy CV va TimeSeriesSplit ===
tasodifiy CV: 0.8077 (+-0.0177)
TimeSeriesSplit: 0.8119 (+-0.0288)
haqiqiy kelajak: 0.8212
usul kelajakdan farq
tasodifiy CV -0.0135
TimeSeriesSplit -0.0092
⭐ Vaqt ma'lumotida TimeSeriesSplit ishlatingNima ko'rsatdi: 2.3, 2.5-bo'limlar.
Misol 4 — Bayram va gap
"""Tashqi kalendar va bashorat gorizonti (real pandas/sklearn)."""
import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
BAYRAMLAR = [(1, 1), (3, 8), (3, 21), (5, 9), (9, 1), (10, 1), (12, 8)]
def yarat(seed: int = 3, kunlar: int = 1000) -> pd.DataFrame:
rng = np.random.default_rng(seed)
sanalar = pd.date_range("2023-06-01", periods=kunlar, freq="D")
oy = sanalar.month.to_numpy()
kun = sanalar.day.to_numpy()
hafta = sanalar.dayofweek.to_numpy()
bayram = np.array([(m, d) in BAYRAMLAR for m, d in zip(oy, kun)])
# bayram oldidan savdo oshadi, bayram kuni tushadi
oldi = np.roll(bayram, -1) | np.roll(bayram, -2)
savdo = (1200 + 300 * (hafta >= 5) + 900 * oldi - 500 * bayram
+ 180 * np.sin(2 * np.pi * oy / 12)
+ np.linspace(0, 200, kunlar) # trend
+ rng.normal(0, 80, kunlar))
return pd.DataFrame({"sana": sanalar, "savdo": savdo})
def kalendar_belgilari(df: pd.DataFrame) -> pd.DataFrame:
d = df["sana"].dt
df = df.copy()
oy, kun = d.month.to_numpy(), d.day.to_numpy()
bayram = np.array([(m, k) in BAYRAMLAR for m, k in zip(oy, kun)])
df["hafta_kuni"] = d.dayofweek
df["oy"] = oy
df["hafta_oxiri"] = (d.dayofweek >= 5).astype(int)
df["bayram"] = bayram.astype(int)
df["bayram_oldi"] = (np.roll(bayram, -1) | np.roll(bayram, -2)).astype(int)
df["bayram_keyin"] = (np.roll(bayram, 1) | np.roll(bayram, 2)).astype(int)
# keyingi bayramgacha kunlar
bayram_idx = np.where(bayram)[0]
keyingi = np.full(len(df), 365)
for i in range(len(df)):
qolgan = bayram_idx[bayram_idx >= i]
if len(qolgan):
keyingi[i] = qolgan[0] - i
df["bayramgacha"] = np.minimum(keyingi, 30)
return df
def main() -> None:
df = kalendar_belgilari(yarat())
y = df["savdo"].to_numpy()
print("=== 1. Ma'lumot ===")
print(f" {len(df)} kun, bayramlar {int(df['bayram'].sum())}")
print(f" {'holat':<16} {'o_rtacha savdo':>16} {'n':>6}")
for nom, m in [("oddiy kun", (df["bayram"] == 0)
& (df["bayram_oldi"] == 0) & (df["hafta_oxiri"] == 0)),
("hafta oxiri", df["hafta_oxiri"] == 1),
("bayram oldi", df["bayram_oldi"] == 1),
("bayram kuni", df["bayram"] == 1)]:
print(f" {nom:<16} {df.loc[m, 'savdo'].mean():>16.0f} "
f"{int(m.sum()):>6}")
print("\n=== 2. Kalendar belgilarining hissasi ===")
def model():
return HistGradientBoostingRegressor(learning_rate=0.1, max_iter=200,
random_state=0)
cv = TimeSeriesSplit(n_splits=5)
guruhlar = {
"asosiy": ["hafta_kuni", "oy"],
"+ hafta oxiri": ["hafta_kuni", "oy", "hafta_oxiri"],
"+ bayram": ["hafta_kuni", "oy", "hafta_oxiri", "bayram"],
"+ bayram oldi/keyin": ["hafta_kuni", "oy", "hafta_oxiri", "bayram",
"bayram_oldi", "bayram_keyin"],
"+ bayramgacha": ["hafta_kuni", "oy", "hafta_oxiri", "bayram",
"bayram_oldi", "bayram_keyin", "bayramgacha"],
}
print(f" {'guruh':<22} {'CV R^2':>9}")
for nom, ustunlar in guruhlar.items():
b = cross_val_score(model(), df[ustunlar], y, cv=cv,
scoring="r2").mean()
print(f" {nom:<22} {b:>9.4f}")
print("\n=== 3. gap parametri ===")
ustunlar = list(guruhlar["+ bayramgacha"])
print(f" {'gap (kun)':>10} {'CV R^2':>9} {'std':>8}")
for gap in [0, 7, 30, 90]:
b = cross_val_score(model(), df[ustunlar], y,
cv=TimeSeriesSplit(n_splits=5, gap=gap),
scoring="r2")
print(f" {gap:>10} {b.mean():>9.4f} {b.std():>8.4f}")
print(" (gap oshgani sari baho konservativroq)")
print("\n=== 4. max_train_size: kengayuvchi va siljuvchi oyna ===")
print(f" {'max_train_size':>15} {'CV R^2':>9} {'o_quv hajmi':>13}")
for mts in [None, 200, 400, 700]:
cvx = TimeSeriesSplit(n_splits=5, max_train_size=mts)
b = cross_val_score(model(), df[ustunlar], y, cv=cvx,
scoring="r2").mean()
hajmlar = [len(tr) for tr, _ in cvx.split(df)]
nom = "None (kengayuvchi)" if mts is None else str(mts)
print(f" {nom:>15} {b:>9.4f} {str(hajmlar[:3]):>13}")
print(" ⭐ Bayram belgilari tashqi bilim beradi")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Ma'lumot ===
1000 kun, bayramlar 18
holat o_rtacha savdo n
oddiy kun 1284 674
hafta oxiri 1617 286
bayram oldi 2294 36
bayram kuni 893 18
=== 2. Kalendar belgilarining hissasi ===
guruh CV R^2
asosiy 0.1894
+ hafta oxiri 0.1894
+ bayram 0.1894
+ bayram oldi/keyin 0.3174
+ bayramgacha 0.4093
=== 3. gap parametri ===
gap (kun) CV R^2 std
0 0.4093 0.3889
7 0.4143 0.3861
30 0.4295 0.3592
90 0.3844 0.3955
(gap oshgani sari baho konservativroq)
=== 4. max_train_size: kengayuvchi va siljuvchi oyna ===
max_train_size CV R^2 o_quv hajmi
None (kengayuvchi) 0.4093 [170, 336, 502]
200 0.1444 [170, 200, 200]
400 0.3232 [170, 336, 400]
700 0.4104 [170, 336, 502]
⭐ Bayram belgilari tashqi bilim beradiNima ko'rsatdi: 2.5, 2.6-bo'limlar.
5. To'g'ri va noto'g'ri tushunishlar
| Noto'g'ri fikr | To'g'risi |
|---|---|
| "rolling(7).mean() xavfsiz" | shift(1) kerak |
| "Vaqt ma'lumotida oddiy CV" | TimeSeriesSplit |
| "groupby transform xavfsiz" | Butun davrni ko'radi |
| "Yil belgisi trendni beradi" | Daraxt ekstrapolyatsiya qilmaydi |
| "Soat oddiy son" | Davriy — sin/cos |
| "gap keraksiz" | Bashorat gorizonti bo'lsa majburiy |
| "Bayramlar kichik detal" | Ko'pincha eng katta foyda |
| "shift(-1) foydali belgi" | Bu kelajak — leakage |
6. Keng tarqalgan xatolar va yechimlari
1. shift(1) siz rolling
df["oyna"] = g.rolling(7).mean() # ⚠️
df["oyna"] = g.shift(1).rolling(7).mean() # ✅2. Vaqt ma'lumotida tasodifiy CV
cross_val_score(m, X, y, cv=KFold(5, shuffle=True)) # ⚠️
cross_val_score(m, X, y, cv=TimeSeriesSplit(5)) # ✅3. Butun davrdan agregatsiya
df["mijoz_ortacha"] = df.groupby("mijoz")["summa"].transform("mean") # ⚠️
df["mijoz_ortacha"] = g.shift(1).expanding().mean() # ✅4. Saralamasdan rolling
df.groupby("mijoz")["summa"].shift(1) # sana tartibi noma'lum # ⚠️
df = df.sort_values(["mijoz", "sana"]); ... # ✅5. Kelajak belgisi
df["keyingi"] = g.shift(-1) # ⚠️
# bu maqsad bo'lishi mumkin, belgi emas # ✅6. gap ni unutish
TimeSeriesSplit(5) # 7 kun oldin bashorat qilinadi # ⚠️
TimeSeriesSplit(5, gap=7) # ✅7. Yil belgisidan trend kutish
df["yil"] = df["sana"].dt.year # daraxt uchun # ⚠️
# trendni alohida modellang yoki vaqt indeksini chiziqli bering # ✅7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 17.2-dars (o'tilgan): Agregatsiya va davriy kodlash
- 12.3-dars (o'tilgan): Validatsiya strategiyasi
- 12.9-dars (o'tilgan): Leakage
- 15.1-dars (o'tilgan): Ekstrapolyatsiya
- 30-qism: Vaqt qatorlari bashorati
8. Eng yaxshi amaliyotlar
Sana bo'yicha saralang.
shift(1) ni har doim qo'ying.
TimeSeriesSplit ishlating.
gap ni gorizontga moslang.
Davriy belgilarni kodlang.
Bayramlarni qo'shing.
Recency belgilarini yarating.
Har belgi uchun "qachon ma'lum?" savolini bering.
9. Amaliy topshiriq
Vazifa 1: Bashorat qiling
1. # sanadan qanday komponentlar olinadi?
2. # davriy belgilar qanday kodlanadi?
3. # rolling dan oldin nima kerak?
4. # nega shift(1)?
5. # eng kuchli vaqt belgilaridan biri?
6. # vaqt leakage ning uch ko'rinishi?
7. # qaysi CV ishlatiladi?
8. # gap nima uchun?
9. # max_train_size nima qiladi?
10. # yil belgisining muammosi?
11. # shift(-1) nima?
12. # bayram belgilari nima beradi?Javoblar
- Hafta kuni, oy, soat, chorak va h.k.
- sin/cos
- shift(1) va saralash
- Joriy qiymat o'z bashoratiga kirmasligi uchun
- Oxirgi hodisadan o'tgan vaqt
- shift yo'q, tasodifiy CV, butun davrdan agregatsiya
- TimeSeriesSplit
- Bashorat gorizonti uchun
- Siljuvchi oyna
- Daraxt ekstrapolyatsiya qilmaydi
- Kelajak — leakage
- Tashqi bilim
Vazifa 2: Xatolarni tuzating
1. df["oyna"] = g.rolling(7).mean()
2. cross_val_score(m, X, y, cv=KFold(5, shuffle=True))
3. df["ortacha"] = df.groupby("mijoz")["summa"].transform("mean")
4. df["keyingi"] = g.shift(-1) # belgi sifatida
5. TimeSeriesSplit(5) # 7 kun oldin bashoratJavoblar
1. df["oyna"] = g.shift(1).rolling(7).mean()
2. cross_val_score(m, X, y, cv=TimeSeriesSplit(5))
3. df["ortacha"] = g.shift(1).expanding().mean()
4. # bu maqsad bo'lishi mumkin, belgi emas
5. TimeSeriesSplit(5, gap=7)Vazifa 3: Komponentlar
Modellang:
- Ma'lumot
- Komponentlar
- Guruhlar
- Davriy kodlash
Vazifa 4: Lag va oyna
Modellang:
- Ma'lumot
- shift tuzog'i
- To'plamlar
- Leakage ta'siri
Vazifa 5: Recency
Modellang:
- Ma'lumot
- O'tgan vaqt
- To'plamlar
- CV taqqoslash
Vazifa 6: Kalendar
Modellang:
- Ma'lumot
- Bayram belgilari
- gap
- max_train_size
Vazifa 7: O'ylash
Vaqt qatorlari uchun modelga "vaqt indeksi" (0, 1, 2, ...) belgisini berish mumkinmi?
Javob
Qisqa javob: chiziqli modelga — ha, u trendni ifodalaydi. Daraxtlarga — yo'q, chunki ular ekstrapolyatsiya qilmaydi 15.1-bob va kelajakdagi indeks uchun oxirgi ko'rgan qiymatni qaytaradi.
1. Nima bo'ladi
| Model | Vaqt indeksi bilan |
|---|---|
| Chiziqli regressiya | Trendni to'g'ri ifodalaydi va davom ettiradi |
| Ridge/Lasso | Xuddi shunday |
| Qaror daraxti | O'quv diapazonida ishlaydi, tashqarida qotib qoladi |
| Gradient boosting | Xuddi shunday |
| Neyron tarmoq | Qisman ekstrapolyatsiya qiladi (aktivatsiyaga bog'liq) |
2. Amaliy yechimlar
Trendni ayirish (detrending):
- Chiziqli model bilan trendni moslang
- Qoldiqni daraxt bilan modellang (15.1 dagi kabi)
- Bashoratda ikkalasini qo'shing
Farqlar (differencing):
y_t - y_{t-1}ni bashorat qiling- Trend avtomatik yo'qoladi
Nisbatlar:
y_t / oyna_o'rtachasi— trendga chidamli
Gibrid: chiziqli trend + mavsumiy daraxt
3. Qachon vaqt indeksi foydali
- O'quv va bashorat bir xil diapazonda bo'lsa (ichki interpolyatsiya)
- Trend yo'q yoki juda zaif bo'lsa
- Chiziqli model ishlatilsa
- Vaqt indeksi boshqa belgilar bilan o'zaro ta'sirda ishlatilsa
4. Amaliy tekshiruv
1. Vaqt indeksi bilan va bilansiz CV qiling
2. Oxirgi davrda (kelajak) alohida tekshiring
3. Agar CV yaxshi, kelajak yomon bo'lsa - ekstrapolyatsiya muammosi
4. Unda trendni ayirib ko'ring5. Xulosa
- Chiziqli modelga foydali, daraxtga xavfli
- Daraxt kelajakda qotib qoladi
- Trendni ayirish yoki farqlarni bashorat qilish
- Har doim kelajak davrida tekshiring
Nimani mustahkamlaydi: 2.1, 2.4-bo'limlar.
Xulosa
Bu darsda vaqt belgilarini o'rgandik.
Eng muhim uch fikr:
shift(1)— birinchi qoida.g.rolling(7).mean()joriy qiymatni ham o'z ichiga oladi, ya'ni model o'z javobini ko'radi. To'g'ri tartib: saralash →shift(1)→rolling(n)→ agregat. Bu — vaqt leakage ning eng keng tarqalgan ko'rinishi va u CV da ajoyib, ishlab chiqarishda esa halokatli natija beradi.Vaqt ma'lumotida tasodifiy CV noto'g'ri.
KFold(shuffle=True)modelga kelajakni ko'rsatadi va natijani optimistik qiladi.TimeSeriesSplitishlating — kerak bo'lsagapbilan (bashorat gorizonti bor bo'lsa) vamax_train_sizebilan (siljuvchi oyna).Eng kuchli belgilar — recency va lag. "Oxirgi hodisadan o'tgan kun", "o'rtacha interval" va "tezlashuv" mijoz faolligini bitta songa siqadi va churn modellarida ko'pincha hal qiluvchi bo'ladi. Bayram va tashqi kalendar esa modelga hech qanday algoritm topa olmaydigan tashqi bilim beradi. "Yil" belgisidan esa ehtiyot bo'ling: daraxtlar ekstrapolyatsiya qilmaydi.
Keyingi darsda matn belgilarini o'rganamiz: bag-of-words, TF-IDF va sodda matn statistikasi.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!