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

الوحدة 2 — التفكيك: الاتّجاه، والموسميّة، والضجيج

قبل اقتراح أيّ نموذج، نُفكِّك السلسلة لنرى ما فيها. طلب صيدليّاتنا اليوميّ ليس رقمًا فوضويًّا، بل تراكب لثلاث إشارات على الأقلّ: نموّ بطيء، وأنماط أسبوعيّة وسنويّة، وضجيج. التفكيك يُظهرها منفصلةً ويُمهّد لكلّ ما يليها.

النموذجان الجمعيّ والضربيّ

الفرضيّة الأولى في التفكيك اختيار الشكل الذي تُركّب به المكوّنات. النموذج الجمعيّ يفترض تكديسًا بسيطًا:

yt=Tt+St+εty_t = T_t + S_t + \varepsilon_t

حيث TtT_t الاتّجاه، وStS_t الموسميّة، وεt\varepsilon_t البواقي. يناسب هذا النموذج السلاسل التي يبقى فيها حجم التذبذبات ثابتًا رغم نموّ الاتّجاه: قمّة الموسم في الشتاء تعلو بنفس عدد الوحدات فوق المتوسّط سواء كان المتوسّط 100 أو 200 وحدة.

النموذج الضربيّ يفترض بدلًا من ذلك تضخّمًا مع الاتّجاه:

yt=Tt×St×εty_t = T_t \times S_t \times \varepsilon_t

هنا قمّة الموسم لا تعلو بنفس القدر، بل بنفس النسبة: 20% فوق المتوسّط، سواء كان المتوسّط 100 أو 200. هذا نمط شائع جدًّا في المبيعات التجاريّة: كلّما نمت الشبكة، اتّسعت الفروق الموسميّة بالقيمة المطلقة. الاختبار البصريّ سريع: إذا رسمنا السلسلة ورأينا التذبذبات تكبر مع مرور الوقت، فالنموذج ضربيّ. الحيلة العمليّة هي أخذ اللوغاريتم، فتفكيك ضربيّ على السلسلة الأصليّة يصبح جمعيًّا على لوغاريتمها.

STL: تفكيك مرن أصبح المرجع

الأداة الكلاسيكيّة seasonal_decompose من statsmodels تفترض موسميّة ثابتة طوال الفترة. هذا مقبول لسلسلة قصيرة أو مستقرّة، لكنّه غير واقعيّ لأربع سنوات من مبيعات الصيدليّات، حيث تتطوّر أنماط الاستهلاك تدريجيًا. STL (Seasonal and Trend decomposition using Loess) أدخل ميّزتين مهمّتين: يسمح للموسميّة بأن تتغيّر بطيئًا، ويقاوم القيم الشاذّة عبر تنعيم Loess.

import pandas as pd
from statsmodels.tsa.seasonal import STL

df = pd.read_csv("ventes_pharmacies.csv", parse_dates=["date"])
serie = df.set_index("date")["ventes_totales"].asfreq("D")

# الموسميّة الأسبوعية (7 أيّام). robust=True لمقاومة القيم الشاذّة.
res = STL(serie, period=7, robust=True).fit()

figure = res.plot()
figure.set_size_inches(10, 8)

الوسيط period يحدّد طول الموسم بالخطوات: 7 للأسبوعيّة على بيانات يوميّة، و12 للشهريّة على بيانات شهريّة. أمّا robust=True فيُفعّل تكرارًا يُقلّل من أثر الأيّام الشاذّة (يوم إغلاق طارئ، أو خطأ في تسجيل المبيعات)، وهو ما نُنصح به دائمًا على بيانات حقيقيّة.

الموسميّات المتعدّدة، والفخّ الشائع

مبيعاتنا لا تحمل موسميّة واحدة بل اثنتين على الأقلّ: أسبوعيّة (السبت والأحد يختلفان عن أيّام الأسبوع) وسنويّة (ذروة شتائيّة للأمراض التنفّسيّة، وذروة صيفيّة لمنتجات الشمس). الاكتفاء بموسميّة واحدة يُخفي الأخرى في البواقي، فتُخيّل قراءتُها للنموذج ضجيجًا في حين أنّه بنية منتظمة.

الحلّ في statsmodels الحديث هو MSTL (Multiple STL) الذي يُفكّك موسميّات متعدّدة تسلسليًّا:

from statsmodels.tsa.seasonal import MSTL

# 7 للأسبوعيّة، 365.25 للسنويّة على تردّد يوميّ.
res = MSTL(serie, periods=(7, 365)).fit()

# res.seasonal يصير DataFrame بعمود لكل موسميّة.
print(res.seasonal.head())

على البيانات الشهريّة يُستعمل periods=(12,)؛ وعلى البيانات ذات التردّد الساعيّ نُضيف 24 (يوميّ) و24*7 (أسبوعيّ). قاعدة عامّة: كلّ موسميّة يجب أن تُخصَّص لها فترة صريحة، ولا ترتجل الأداة الفترات بمفردها.

قراءة البواقي: الفحص الحقيقيّ

المكوّن الثالث، البواقي، هو ما لم يشرحه الاتّجاه ولا الموسميّة. رسمها هو الاختبار الحقيقيّ لجودة التفكيك:

  • إن بدت عشوائيّة تمامًا حول الصفر، فالتفكيك التقط ما هو مُنتظم، وما بقي هو ضجيج حقيقيّ. جيّد.
  • إن رأينا نمطًا واضحًا (قمم متقاربة، اتّجاه صغير، دورات) فالتفكيك لم يشرح كلّ البنية. علينا إمّا زيادة موسميّة، أو تغيير نوع النموذج (جمعيّ ↔ ضربيّ)، أو التحقّق من وجود أحداث خاصّة (أعياد، عروض).
  • إن رأينا قفزة كبيرة في نقطة محدّدة، فالأرجح حدث استثنائيّ (تغيير سياسة، فتح فروع جديدة) يستحقّ متغيّرًا صريحًا في نموذج الوحدة 7.

توظيف التفكيك في التنبّؤ

قد يُغري التفكيك بذاته كنموذج تنبّؤ: نمدّ الاتّجاه خطّيًا، ونُكرّر الموسميّة، ونهمل البواقي. هذا يعمل أحيانًا كخطّ مرجعيّ، لكنّ ثمّة أفضل: نستعمل التفكيك للفهم والتشخيص، ثمّ نمرّر إلى نموذج مناسب يتعامل مع الأمر كوحدة واحدة. Prophet مثلًا (الوحدة 6) يُفكّك ضمنيًا، وSARIMA (الوحدة 5) يتعامل مع الموسميّة عبر ترتيب موسميّ.

اختيار الفترة بلا حدس

إذا شككت في طول الموسميّة (7؟ 30؟ 365؟) فارسم دالّة الارتباط الذاتي (ACF) للسلسلة الأصليّة، وستُظهر قممها الطول الحقيقيّ. سنبني على هذا الأساس في الوحدة التالية. القاعدة: نُفكّك بعد تأكيد الفترات، لا قبله.

الخلاصة

  • الجمعيّ حين يبقى حجم التذبذبات ثابتًا، والضربيّ حين يتضخّم مع الاتّجاه؛ ولوغاريتم السلسلة يُحوّل الثاني إلى الأوّل.
  • STL أفضل من seasonal_decompose الكلاسيكيّ: موسميّة متطوّرة بطيئًا، ومقاومة للقيم الشاذّة عبر robust=True.
  • الموسميّات متعدّدة في الأغلب (أسبوعيّة + سنويّة)؛ نستعمل MSTL ونُخصّص كلّ فترة صراحةً.
  • البواقي هي الفحص: إن بقيت فيها بنية فالتفكيك ناقص؛ إن كانت عشوائيّة فالتفكيك جيّد وننتقل إلى النموذج.

الوحدة التالية: نتناول الاستقرارية — الفرضيّة التي تشترطها ARIMA — ونتعلّم قراءة ACF وPACF لاختيار الرتب.