IlmHamroh
Data Science va sun'iy intellekt/Feature engineering5/10-dars21 daqiqa
Mundarija (22)

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

python
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.minute
text
QAYSI 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

python
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()
text
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

python
# 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.days
text
JUDA 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

text
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

python
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
    ...
text
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

text
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

python
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 qilma

Vaqt 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'RI

4. Batafsil misollar

Misollar real numpy/pandas/sklearn bilan (Python 3.14).

Misol 1 — Sana komponentlari

python
"""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:

text
=== 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 yetarli

Nima ko'rsatdi: 2.1-bo'lim.

Misol 2 — Lag, oyna va shift(1)

python
"""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:

text
=== 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 qoidasi

Nima ko'rsatdi: 2.2, 2.4-bo'limlar.

Misol 3 — O'tgan vaqt va validatsiya

python
"""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:

text
=== 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 ishlating

Nima ko'rsatdi: 2.3, 2.5-bo'limlar.

Misol 4 — Bayram va gap

python
"""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:

text
=== 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 beradi

Nima 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

python
df["oyna"] = g.rolling(7).mean()                                  # ⚠️
df["oyna"] = g.shift(1).rolling(7).mean()                         # ✅

2. Vaqt ma'lumotida tasodifiy CV

python
cross_val_score(m, X, y, cv=KFold(5, shuffle=True))               # ⚠️
cross_val_score(m, X, y, cv=TimeSeriesSplit(5))                   # ✅

3. Butun davrdan agregatsiya

python
df["mijoz_ortacha"] = df.groupby("mijoz")["summa"].transform("mean") # ⚠️
df["mijoz_ortacha"] = g.shift(1).expanding().mean()                 # ✅

4. Saralamasdan rolling

python
df.groupby("mijoz")["summa"].shift(1)    # sana tartibi noma'lum  # ⚠️
df = df.sort_values(["mijoz", "sana"]); ...                       # ✅

5. Kelajak belgisi

python
df["keyingi"] = g.shift(-1)                                       # ⚠️
# bu maqsad bo'lishi mumkin, belgi emas                           # ✅

6. gap ni unutish

python
TimeSeriesSplit(5)               # 7 kun oldin bashorat qilinadi  # ⚠️
TimeSeriesSplit(5, gap=7)                                         # ✅

7. Yil belgisidan trend kutish

python
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

  1. Sana bo'yicha saralang.

  2. shift(1) ni har doim qo'ying.

  3. TimeSeriesSplit ishlating.

  4. gap ni gorizontga moslang.

  5. Davriy belgilarni kodlang.

  6. Bayramlarni qo'shing.

  7. Recency belgilarini yarating.

  8. Har belgi uchun "qachon ma'lum?" savolini bering.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
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
  1. Hafta kuni, oy, soat, chorak va h.k.
  2. sin/cos
  3. shift(1) va saralash
  4. Joriy qiymat o'z bashoratiga kirmasligi uchun
  5. Oxirgi hodisadan o'tgan vaqt
  6. shift yo'q, tasodifiy CV, butun davrdan agregatsiya
  7. TimeSeriesSplit
  8. Bashorat gorizonti uchun
  9. Siljuvchi oyna
  10. Daraxt ekstrapolyatsiya qilmaydi
  11. Kelajak — leakage
  12. Tashqi bilim

Vazifa 2: Xatolarni tuzating

python
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 bashorat
Javoblar
python
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:

  1. Ma'lumot
  2. Komponentlar
  3. Guruhlar
  4. Davriy kodlash

Vazifa 4: Lag va oyna

Modellang:

  1. Ma'lumot
  2. shift tuzog'i
  3. To'plamlar
  4. Leakage ta'siri

Vazifa 5: Recency

Modellang:

  1. Ma'lumot
  2. O'tgan vaqt
  3. To'plamlar
  4. CV taqqoslash

Vazifa 6: Kalendar

Modellang:

  1. Ma'lumot
  2. Bayram belgilari
  3. gap
  4. 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

  1. Trendni ayirish (detrending):

    • Chiziqli model bilan trendni moslang
    • Qoldiqni daraxt bilan modellang (15.1 dagi kabi)
    • Bashoratda ikkalasini qo'shing
  2. Farqlar (differencing):

    • y_t - y_{t-1} ni bashorat qiling
    • Trend avtomatik yo'qoladi
  3. Nisbatlar:

    • y_t / oyna_o'rtachasi — trendga chidamli
  4. 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'ring

5. Xulosa

  1. Chiziqli modelga foydali, daraxtga xavfli
  2. Daraxt kelajakda qotib qoladi
  3. Trendni ayirish yoki farqlarni bashorat qilish
  4. Har doim kelajak davrida tekshiring

Nimani mustahkamlaydi: 2.1, 2.4-bo'limlar.


Xulosa

Bu darsda vaqt belgilarini o'rgandik.

Eng muhim uch fikr:

  1. 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.

  2. Vaqt ma'lumotida tasodifiy CV noto'g'ri. KFold(shuffle=True) modelga kelajakni ko'rsatadi va natijani optimistik qiladi. TimeSeriesSplit ishlating — kerak bo'lsa gap bilan (bashorat gorizonti bor bo'lsa) va max_train_size bilan (siljuvchi oyna).

  3. 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.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!