الوحدة 2 — التفكيك: الاتّجاه، والموسميّة، والضجيج
قبل اقتراح أيّ نموذج، نُفكِّك السلسلة لنرى ما فيها. طلب صيدليّاتنا اليوميّ ليس رقمًا فوضويًّا، بل تراكب لثلاث إشارات على الأقلّ: نموّ بطيء، وأنماط أسبوعيّة وسنويّة، وضجيج. التفكيك يُظهرها منفصلةً ويُمهّد لكلّ ما يليها.
النموذجان الجمعيّ والضربيّ
الفرضيّة الأولى في التفكيك اختيار الشكل الذي تُركّب به المكوّنات. النموذج الجمعيّ يفترض تكديسًا بسيطًا:
حيث الاتّجاه، و الموسميّة، و البواقي. يناسب هذا النموذج السلاسل التي يبقى فيها حجم التذبذبات ثابتًا رغم نموّ الاتّجاه: قمّة الموسم في الشتاء تعلو بنفس عدد الوحدات فوق المتوسّط سواء كان المتوسّط 100 أو 200 وحدة.
النموذج الضربيّ يفترض بدلًا من ذلك تضخّمًا مع الاتّجاه:
هنا قمّة الموسم لا تعلو بنفس القدر، بل بنفس النسبة: 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.