الوحدة 5 — الدمج الصحيح زمنيًّا
هذه الوحدة الأهمّ في الدورة. سلامة النقطة الزمنيّة هي ما يُميّز مخزن متغيّرات فعليًّا من مجرّد قاعدة بيانات مع طبقة تسمية. تشرح الوحدة من أين يأتي تسرّب المستقبل، كيف يُلغى، وما الفرق الرقميّ الذي يُحدثه على مقاييس النموذج.
المسألة
نبني مجموعة تدريب لتسجيل الاحتيال. لدينا جدول أحداث (المعاملات الخام) وجدول متغيّرات (عدّادات محسوبة على النوافذ):
events:
event_id | customer_id | event_time | is_fraud
T1 | C1 | 2026-06-01 09:00:00 | 0
T2 | C1 | 2026-06-01 15:30:00 | 1
T3 | C1 | 2026-06-02 08:00:00 | 0
features (customer_counters):
customer_id | as_of_time | count_1h | count_24h
C1 | 2026-06-01 08:00:00 | 0 | 2
C1 | 2026-06-01 14:00:00 | 1 | 5
C1 | 2026-06-01 20:00:00 | 3 | 8
C1 | 2026-06-02 08:00:00 | 0 | 3
المطلوب: لكلّ حدث في events، أضف قيمة العدّادات كما كانت في زمن الحدث تمامًا.
الدمج الساذج، والفخّ
الغريزة الأولى:
merged = events.merge(features, on="customer_id")
يُنتج هذا 12 صفًّا (3 × 4)، منها صفوف تربط الحدث T1 (في 09:00) بمتغيّرات بتاريخ 20:00 من نفس اليوم، أي بعد الحدث بأحد عشر ساعة. النموذج يرى قيمة عدّاد جُمِعَت بعد أن اتّخذ الاحتيال مكانه. يتعلّم أنّ «العدّاد المرتفع لاحقًا يدلّ على احتيال» — دائرة تامّة تعطي AUC ممتازًا في التدريب وتنهار في الإنتاج لأنّ خدمة الإنتاج لا تستطيع الاطّلاع على المستقبل.
هذا هو تسرّب المستقبل (data leakage) وهو أشيع سبب لفارق أداء صادم بين المصادقة والإنتاج.
الحلّ الأوّل الذي يخطر في البال هو تصفية «فقط الصفوف التي طابع متغيّراتها قبل الحدث»:
merged = merged[merged["as_of_time"] <= merged["event_time"]]
يُنتج هذا 6 صفوف لكلٍّ منها عدّة قيم. الحدث T2 (في 15:30) يجد ثلاث صفوف متغيّرات صالحة: 08:00 و14:00 من نفس اليوم، والحدث T3 يجد ثلاث. علينا اختيار الأحدث بين هذه الصالحة. هذا بالضبط ما يُسمّى as-of join أو دمج النقطة الزمنيّة.