انتقل إلى المحتوى الرئيسي

الوحدة 9 — التحقّق المتدرّج والمقاييس الملائمة

بنيت النماذج، ومُلئت الأعمدة الأولى من جدول المقارنة، والسؤال الجوهريّ الآن: كيف نُقارن؟ التحقّق المتقاطع التقليديّ ممنوع (يخلط الزمن)، ونقطة اختبار واحدة على 28 يومًا لا تكفي (الحظّ يلعب). هذه الوحدة تُقدّم المنهجيّة المهنيّة: تحقّق بأصول متدرّجة مع مقاييس ملائمة للسلاسل الزمنية.

لماذا نقطة اختبار واحدة لا تكفي

اختبرنا في الوحدات السابقة على مقطع 28 يومًا واحد (سبتمبر 2027). فمن ذا الذي يضمن أنّ هذا المقطع ليس شهرًا خاصًّا، أو أنّ SARIMA لم يستفد من صدفة موسميّة؟ الجواب الأمين: لا أحد. لهذا نحتاج إلى تكرار الاختبار على أصول متعدّدة.

الفكرة هي التالية. نُحدّد أفقًا H=28H = 28 ونُختار عدّة تواريخ فصل (T1<T2<<TKT_1 < T_2 < \dots < T_K)؛ لكلّ منها نُدرّب على [0,Ti][0, T_i] ونتوقّع على [Ti+1,Ti+H][T_i + 1, T_i + H] ونحسب MAE. ثمّ نُبلّغ عن متوسّط وانحراف MAE عبر الأصول. النموذج الأفضل هو الذي يجمع بين متوسّط منخفض واستقرار (انحراف صغير).

بديل جاهز: TimeSeriesSplit من scikit-learn

import numpy as np
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_absolute_error

serie = df["ventes_totales"].values
n = len(serie)
H = 28

# 5 أصول متتالية، اختبار كلٍّ منها 28 يومًا.
tscv = TimeSeriesSplit(n_splits=5, test_size=H)

resultats = []
for i, (idx_tr, idx_te) in enumerate(tscv.split(serie)):
train = serie[idx_tr]
test = serie[idx_te]

# مثال: تنبّؤ ساذج موسميّ (7 أيّام).
dernier_cycle = train[-7:]
pred = np.tile(dernier_cycle, int(np.ceil(H / 7)))[:H]

mae = mean_absolute_error(test, pred)
resultats.append(mae)
print(f"طيّة {i+1}: MAE = {mae:.1f}")

print(f"\nمتوسّط MAE = {np.mean(resultats):.1f} ± {np.std(resultats):.1f}")

نافذة تدريب متزايدة (expanding) هي الافتراض: تدريب من البداية إلى TiT_i. البديل: نافذة منزلقة (sliding) بعرض ثابت، مفيدة إذا كانت السلسلة تعاني من انحراف بطيء (تغيّر توزيع) ونريد النموذج ألّا يتأثّر ببيانات قديمة جدًّا.

آفاق متعدّدة: لا نُقيم فقط اليوم +28

نموذج قد يكون ممتازًا على اليوم +1 وسيّئًا على اليوم +28، أو العكس. تقييم على أفق واحد يُخفي هذا التباين. الحلّ: نُبلّغ MAE عند آفاق متعدّدة، مثلًا +1، +7، +14، +28.

def mae_par_horizon(pred, vrai, horizons=(1, 7, 14, 28)):
return {h: abs(pred[h-1] - vrai[h-1]) for h in horizons}

# مع تجميع على الطيّات:
tous = []
for idx_tr, idx_te in tscv.split(serie):
train, test = serie[idx_tr], serie[idx_te]
pred = np.tile(train[-7:], int(np.ceil(H / 7)))[:H]
tous.append(mae_par_horizon(pred, test))

# متوسّط عبر الطيّات:
import pandas as pd
tableau = pd.DataFrame(tous).mean()
print(tableau)

عرض النتائج على آفاق متعدّدة يمنح قسم الأعمال معلومة نافعة: «توقّعنا للأسبوع القادم دقّته ±\pm5%، وللأسبوع الرابع ±\pm18%». هذا ما يبني الثقة.

MAE، RMSE، وأخطاء أخرى مطلقة

MAE (Mean Absolute Error) هو متوسّط yty^t|y_t - \hat{y}_t|، بوحدة الهدف نفسها (وحدات مبيعات)، وسهل التفسير للأعمال. RMSE (Root Mean Squared Error) يجذّر متوسّط المربّعات، ويعاقب الأخطاء الكبيرة أكثر من MAE. اختيار بينهما مسألة إطار: MAE إن كانت التكلفة خطّيّة بحجم الخطأ (كلّ وحدة زائدة أو ناقصة تكلّف بنفس القدر)؛ RMSE إن كانت الأخطاء الكبيرة أشدّ ضررًا (نفاد مخزون مكلف بشكل غير خطّي).

على مبيعات صيدليّاتنا، MAE هو المقياس المفضّل لأنّ التكلفة تقريبيًا خطّيّة، وRMSE مفيد للكشف عن أيّام كارثيّة.

MAPE وفخّه على الصفر

MAPE (Mean Absolute Percentage Error) شعبيّ لأنّه يُعطي نسبة قابلة للتفسير عبر مقاييس مختلفة: «خطأنا 8٪». صيغته:

MAPE=100nt=1nyty^tyt\text{MAPE} = \frac{100}{n} \sum_{t=1}^{n} \left| \frac{y_t - \hat{y}_t}{y_t} \right|

الفخّ الكارثيّ: القسمة على yty_t. إن كانت السلسلة تحتوي أيّامًا بلا مبيعات (إغلاق، عيد)، yt=0y_t = 0 ينفجر MAPE إلى ++\infty أو يعطي NaN. حتى القيم الصغيرة جدًّا تُعطي نسبًا هائلة تُشوّه المتوسّط.

الحلول الشائعة كلّها ناقصة:

  • إسقاط الأصفار: يغيّر التقييم ويُخفي الأداء على أهمّ الحالات.
  • إضافة ϵ\epsilon للمقام: يعتمد الأداء على قيمة ϵ\epsilon اعتباطًا.
  • sMAPE (symmetric MAPE): yy^/((y+y^)/2)|y - \hat{y}| / ((|y| + |\hat{y}|)/2). أقلّ سوءًا لكن يُعطي تفسيرًا غير طبيعيّ.

التوصية المهنيّة: إن احتوت السلسلة أصفارًا أو قيمًا صغيرة، لا تستعمل MAPE. استعمل MASE بدلًا منه.

MASE: المقياس الذي يحلّ المشكلة

MASE (Mean Absolute Scaled Error) يقسم MAE للنموذج على MAE للنموذج الساذج الموسميّ على التدريب:

MASE=MAEنموذجMAEساذج موسميّ على التدريب\text{MASE} = \frac{\text{MAE}_{\text{نموذج}}}{\text{MAE}_{\text{ساذج موسميّ على التدريب}}}

مزايا. آمن من الأصفار: المقام محسوب من الفروق (ytyts|y_t - y_{t-s}|)، لا من القسمة على القيم. قابل للتفسير: MASE = 0.7 يعني «أخطأ بنسبة 70٪ ممّا يخطئ به الساذج الموسميّ»، فتحسّن 30٪. قابل للمقارنة عبر سلاسل: نفس المقياس يعمل على سلاسل بنطاقات مختلفة.

def mase(y_vrai, y_pred, y_train, s=7):
mae_pred = np.mean(np.abs(y_vrai - y_pred))
mae_naif = np.mean(np.abs(y_train[s:] - y_train[:-s]))
return mae_pred / mae_naif

score = mase(test, pred, train, s=7)
print(f"MASE = {score:.3f}")

قاعدة قراءة: MASE < 1 = أفضل من الساذج الموسميّ؛ MASE > 1 = أسوأ. القيمة الجيّدة في الإنتاج بين 0.6 و0.8؛ أدنى من 0.5 نادر ومريب (تحقّق من التسرّب).

تقييم الفواصل: تغطية، وعرض

النقطة (mean forecast) ليست كلّ شيء. الفواصل التي أنتجتها SARIMA وProphet والوحدة 10 تحتاج تقييمًا خاصًّا:

  • التغطية: إن أعلنّا فاصل 95٪، هل يحتوي القيمة الحقيقيّة فعلًا في 95٪ من الحالات على التاريخ الفعليّ؟ نحسب النسبة على مجموعة اختبار.
  • العرض المتوسّط: فاصل 95٪ بتغطية 95٪ ممتاز، لكن إن كان عرضه ضِعف MAE، فهو غير مفيد للأعمال. نُبلّغ عرضًا متوسّطًا.
  • Pinball loss (خطأ الكمّيّات): يُقيم كمّيّة معيّنة qq بمكافأة عدم التماثل بين الأخطاء العلويّة والسفليّة؛ صيغته المعياريّة لأداء نموذج كمّيّ.

فاصل بتغطية 70٪ حين أعلن 95٪ يعني نموذجًا ثقة زائدة؛ مشكلة كبرى في الأعمال لأنّه يبرّر مخزونًا أدنى من اللازم. فاصل بعرض ضخم يعطي تغطية جيّدة لكن بلا قيمة إخبار.

MAPE على مبيعات ذات أصفار

شاهدنا فرقًا للتنبّؤ: MAPE = 12٪ على النموذج A، MAPE = 340٪ على النموذج B. القرار البديهيّ: A أفضل. المشكلة: B فقط له خطأ يومًا واحدًا بمبيعات 3 وحدات (خطأ 10 وحدات) = خطأ 330٪ يُهيمن على المتوسّط بأكمله. النموذجان متساويان تقريبًا في MAE. لا تُقارِن نماذج بـMAPE على سلاسل غير مستقرّة القيم. استعمل MASE.

قاعدة الطيّات الخمس

لا تُبلّغ MAE من طيّة واحدة أبدًا. الحدّ الأدنى المهنيّ: 5 طيّات، مع تبليغ متوسّط ± انحراف. الطيّات الأقلّ تُعطي إحصاءات ذات ثقة ضعيفة، والطيّات الأكثر (10+) لا تُضيف كثيرًا مقابل تكلفة الحوسبة إلّا في سلاسل قصيرة جدًّا.

الخلاصة

  • تحقّق بأصول متدرّجة (TimeSeriesSplit) هو المعيار؛ نافذة متزايدة افتراضيًا، منزلقة إن كان الانحراف قويًّا.
  • تقييم على آفاق متعدّدة (+1، +7، +14، +28) يكشف تباينًا تُخفيه نقطة واحدة.
  • MAE أسهل تفسيرًا للأعمال؛ RMSE يعاقب الأخطاء الكبيرة؛ MAPE ممنوع عند وجود أصفار أو قيم صغيرة.
  • MASE يحلّ المشكلة: قابل للتفسير، آمن من الأصفار، وقابل للمقارنة عبر السلاسل؛ اعتمده افتراضيًا.
  • تقييم الفواصل بالتغطية والعرض: فاصل ضيّق ثقة زائدة، فاصل ضخم بلا قيمة إخبار.

الوحدة التالية: نُغلق الحلقة بمشروع نهائيّ بفواصل حقيقيّة وخطّة إعادة تدريب.