الوحدة 7 — متغيّرات التقويم والمتنبّئات الخارجية
النماذج الإحصائيّة (SARIMA وHolt-Winters) وProphet تعمل مباشرة على السلسلة الزمنية. المنهج البديل، الذي يُهيمن اليوم على مسابقات Kaggle الزمنية، هو إعادة صياغة المسألة انحدارًا جدوليًا: نُنشئ صفًا لكلّ زمن، وأعمدة للتأخّرات، والنوافذ، ومتغيّرات التقويم، والأعياد، والحملات الترويجيّة، ثمّ نُطعم كلّ ذلك لـتعزيز تدرّجي (LightGBM أو XGBoost). هذا هو موضوع الوحدة.
من سلسلة إلى جدول: التأخّرات والنوافذ
نموذج انحدار كلاسيكيّ ينتظر مصفوفة من متغيّرات و من هدف. لتحويل سلسلتنا إلى هذا الشكل نُنشئ متغيّرات مؤخّرة: القيم عند خطوات سابقة. متغيّر lag_1 هو مبيعات الأمس، وlag_7 مبيعات الأسبوع الماضي، وlag_28 مبيعات الشهر الماضي.
import pandas as pd
import numpy as np
df = pd.read_csv("ventes_pharmacies.csv", parse_dates=["date"])
df = df.sort_values("date").reset_index(drop=True)
# متغيّرات تأخّر
for k in [1, 7, 14, 28, 365]:
df[f"lag_{k}"] = df["ventes_totales"].shift(k)
# متغيّرات نافذة متحرّكة (بدون النقطة الحاليّة!).
df["moy_7"] = df["ventes_totales"].shift(1).rolling(7).mean()
df["moy_28"] = df["ventes_totales"].shift(1).rolling(28).mean()
df["std_28"] = df["ventes_totales"].shift(1).rolling(28).std()
قاعدتان قاطعتان يُخالفهما نصف المبتدئين. الأولى: shift(1) قبل rolling، لأنّ نافذة تشمل النقطة الحاليّة تُدخِل الهدف في المتغيّرات = تسرّب مستقبل من الوحدة 1 يُنتج MAE قريبًا من الصفر مغريًا في الاختبار. الثانية: التأخّر الأدنى يجب أن يكون على الأقلّ = أفق التنبّؤ. إن كان أفقنا 28 يومًا، فاستعمال lag_1 لتنبّؤ يوم +28 مستحيل في الإنتاج، لأنّ اليوم +27 غير معروف بعد.
متغيّرات التقويم
اليوم من الأسبوع، والشهر، وأسبوع السنة، وربع السنة، وثنائيّات صغيرة (بداية الشهر، نهايته، عطلة نهاية الأسبوع) كلّها إشارات قويّة على المبيعات. نُشفّرها دورانيًا (جيب وجيب تمام) للحفاظ على الطبيعة الدوريّة، بدلًا من ترميز عدديّ ساذج يجعل الأحد بعيدًا عن الاثنين:
df["jour_semaine"] = df["date"].dt.dayofweek # 0..6
df["mois"] = df["date"].dt.month # 1..12
df["semaine_annee"] = df["date"].dt.isocalendar().week.astype(int)
df["fin_de_semaine"] = (df["jour_semaine"] >= 5).astype(int)
# ترميز دوريّ: يحافظ على قرب الأحد من الاثنين.
df["sin_jour"] = np.sin(2 * np.pi * df["jour_semaine"] / 7)
df["cos_jour"] = np.cos(2 * np.pi * df["jour_semaine"] / 7)
df["sin_mois"] = np.sin(2 * np.pi * df["mois"] / 12)
df["cos_mois"] = np.cos(2 * np.pi * df["mois"] / 12)
المتنبّئات الخارجية: الفرق الحاسم بين معروف وغير معروف مستقبلًا
هذه النقطة، إن أخطأنا فيها، أفسدت ا لنموذج بصمت. المتنبّئ الخارجيّ متغيّر ليس من السلسلة نفسها (مثال: حالة الطقس، أو حملة ترويجيّة، أو سعر عبوة). الفرق الحاسم:
- متنبّئ معروف مسبقًا: نستطيع أن نقرأ قيمته المستقبليّة عند التنبّؤ. أمثلة: الأعياد (التقويم مُعطى)، والحملات المُخطَّط لها (مقرَّرة سلفًا)، واليوم من الأسبوع (رياضيّ). هذه المتغيّرات نستعملها كأعمدة عاديّة في الجدول.
- متنبّئ غير معروف مسبقًا: قيمته المستقبليّة مجهولة عند التنبّؤ. مثال: الطقس الفعليّ (نعرف التوقّع، لا الحقيقة)، ومبيعات المنافس (يُبلَّغ بها متأخّرًا)، وسعر الدواء الجديد (يُحدَّد لاحقًا). استعمالها كأعمدة يعطي MAE ممتازًا في التدريب ثمّ ينهار في الإنتاج لأنّ الأعمدة تصير NaN. الحلّ: إمّا لا نستعملها، إمّا نستعمل توقّعاتها بدل قيمها الفعليّة، مع الوعي بأنّنا ندخل تنبّؤًا داخل تنبّؤنا.
# قاعدة صريحة: علم إن كان المتنبّئ معروفًا مسبقًا.
metadata = {
"jour_semaine": "known",
"fin_de_semaine": "known",
"est_ferie": "known",
"promo_prevue": "known",
"meteo_pluie_mm": "unknown", # نستعمل التوقّع فقط
"prix_generique": "unknown", # نتجنّبها في العادة
}
التعزيز التدرّجي: النموذج المهيمن على البيانات الجدوليّة
LightGBM (وXGBoost وCatBoost) هي أدوات التعلّم بالتعزيز التدرّجي على الأشجار. تفوّقها على البيانات الجدوليّة أُثبت مرارًا، وسلاسلنا بعد تحويلها جدوليّة. تنفيذ كامل:
import lightgbm as lgb
from sklearn.metrics import mean_absolute_error
# نُسقط الصفوف بلا تأخّرات (البداية).
data = df.dropna().reset_index(drop=True)
colonnes_X = [c for c in data.columns if c not in ["date", "ventes_totales"]]
X = data[colonnes_X]
y = data["ventes_totales"]
# التقسيم بلا خلط (وحدة 1).
masque_te = data["date"] > "2027-08-31"
X_tr, X_te = X[~masque_te], X[masque_te]
y_tr, y_te = y[~masque_te], y[masque_te]
modele_lgb = lgb.LGBMRegressor(
n_estimators=500,
learning_rate=0.05,
num_leaves=31,
min_child_samples=20,
reg_alpha=0.1,
reg_lambda=0.1,
random_state=42,
)
modele_lgb.fit(X_tr, y_tr, eval_set=[(X_te, y_te)], callbacks=[lgb.early_stopping(30)])
y_pred = modele_lgb.predict(X_te)
mae = mean_absolute_error(y_te, y_pred)
print(f"MAE LightGBM = {mae:.1f}")
على مبيعاتنا يعطي هذا عادةً MAE ≈ 98، أي تحسّن 12٪ على Prophet من الوحدة 6، و32٪ على النموذج المرجعيّ. وأهمّ من الرقم: نستطيع طلب أهمّيّة المتغيّرات (modele_lgb.feature_importances_) لفهم ما يحرّك المبيعات فعلًا، وهذا نفيس أمام الأعمال.
التنبّؤ متعدّد الخطوات: مسار مباشر أو تكراريّ
عند التنبّؤ على أفق 28 يومًا، أمامنا استراتيجيّتان:
- تكراريّ: نتوقّع اليوم +1، ثمّ نستعمله كتأخّر لتوقّع اليوم +2، وهكذا. بسيط لكنّه يُراكم الأخطاء ويُصبح غير مستقرّ بعد أفق طويل.
- مباشر: نُدرِّب 28 نموذجًا، كلّ منها متخصّص في أفق واحد (نموذج للتوقّع +1، آخر للتوقّع +2…). أكلف حسابيًّا لكنّه غالبًا أدقّ في الآفاق البعيدة، ويتجنّب مراكمة الأخطاء.
الاختيار المهنيّ في 2026 يميل إلى المسار المباشر لأنّ حوسبة 28 نموذج LightGBM على بضع مئات من الأسطر أرخص من الوقت الذي يُهدَر في تشخيص انحرافات المسار التكراريّ.
اجعل lag_min = أفق التنبّؤ + 1 على الأقلّ. إن أردنا التنبّؤ لـ28 يومًا، فأدنى تأخّر هو lag_28، لأنّ تأخّرات أصغر (مثل lag_1) تُشير إلى قيم غير معروفة بعد عند التنبّؤ الكامل بالأفق. تخطّي هذه القاعدة يُنتج تسرّبًا خفيًا يظهر في MAE ممتاز على التحقّق ثمّ انهيار في الإنتاج.
قبل إدخال أيّ متنبّئ خارجيّ في الإنتاج، اسأل: «ما القيمة التي سيحصل عليها النموذج لهذا العمود في يوم التنبّؤ الفعليّ؟». إن لم يكن للسؤال إجابة (طقس الغد الحقيقيّ، مبيعات المنافس الفعليّة)، فالعمود ممنوع أو يجب استبداله بتنبّؤ عمود آخر — مع كلّ عيوب التنبّؤ داخل التنبّؤ.
الخلاصة
- تحويل السلسلة إلى جدول عبر تأخّرات، ونوافذ متحرّكة مع
shift(1)قبلrolling، ومتغيّرات تقويم مُشفَّرة دورانيًا. - التمييز الجوهريّ بين معروف مسبقًا (تقويم، أعياد، ح ملات مُخطَّطة) وغير معروف (طقس فعليّ، منافس)؛ الثانية تُوصل إلى انهيار الإنتاج بصمت.
- LightGBM بعد هذه الإعادة الصياغة يهزم عادة SARIMA وProphet على السلاسل ذات المتفاعلات الكثيرة (تقويم، أعياد، حملات).
- التنبّؤ متعدّد الخطوات: تكراريّ بسيط لكن يُراكم الأخطاء؛ مباشر أكلف لكن أدقّ في الآفاق البعيدة، وهو الخيار المهنيّ اليوم.
الوحدة التالية: المقاربات العميقة — LSTM والمحوّلات الزمنيّة — ومتى تفوز حقًّا على النماذج البسيطة.