دليل عملي لتحليل البيانات الزمنية: اكتشاف الموسمية والاتجاهات باستخدام Pandas
مقدمة
تُعد البيانات الزمنية من أكثر أنواع البيانات انتشاراً في علم البيانات؛ فهي تظهر في سجلات المبيعات اليومية، وقياسات أجهزة الاستشعار، وأسعار الأسهم، وزيارات المواقع، واستهلاك الطاقة، ومعدلات الطلب على الخدمات. ويكمن اختلافها الأساسي عن البيانات الجدولية التقليدية في أن ترتيب الملاحظات عبر الزمن يحمل معنىً تحليلياً مهماً، فلا يجوز التعامل مع كل صف باعتباره مستقلاً تماماً عن الصفوف السابقة واللاحقة.
في دليل عملي لتحليل البيانات الزمنية: اكتشاف الموسمية والاتجاهات باستخدام Pandas سنتناول منهجية عملية تبدأ من تجهيز الطوابع الزمنية وتنظيفها، ثم استكشاف السلسلة بصرياً، وإعادة أخذ العينات، وحساب المتوسطات المتحركة، وصولاً إلى تمييز الاتجاه العام والموسمية والتحقق من صحة الاستنتاجات. وتوفر مكتبة Pandas أدوات فعالة لهذه المهام، خصوصاً عند استخدام الفهرس الزمني ودوال مثل resample وrolling وshift.
الهدف ليس بناء نموذج تنبؤي متقدم فحسب، بل فهم السلوك الكامن في البيانات أولاً. فالنمو المستمر في المبيعات قد يخفي انخفاضاً موسمياً في شهر معين، والقفزة المفاجئة قد تكون نتيجة خطأ في القياس لا تغيراً حقيقياً في سلوك العملاء. لذلك يمثل التحليل الاستكشافي المنهجي أساساً ضرورياً قبل أي قرار تجاري أو نموذج تعلم آلي.
فهم مكوّنات السلسلة الزمنية وبنية البيانات
السلسلة الزمنية هي مجموعة من القياسات المرتبطة بطوابع زمنية متتابعة. وقد تكون هذه الطوابع كل ثانية أو دقيقة أو ساعة أو يوم أو شهر، وقد تكون الفواصل بينها منتظمة أو غير منتظمة. عند تحليلها، من المفيد التفكير في السلسلة بوصفها مزيجاً من أربعة مكونات رئيسية: المستوى العام، والاتجاه، والموسمية، والضوضاء.
المستوى العام هو القيمة المعتادة التي تتحرك حولها البيانات. أما الاتجاه فهو حركة طويلة الأجل صعوداً أو هبوطاً، مثل ازدياد الإيرادات تدريجياً خلال عدة سنوات. والموسمية هي نمط يتكرر ضمن دورة زمنية ثابتة، مثل ارتفاع مبيعات المتاجر في عطلة نهاية الأسبوع أو زيادة الطلب في فصل الصيف. أما الضوضاء فتمثل التغيرات العشوائية أو الأحداث غير المعتادة التي لا تتبع نمطاً متكرراً.
قبل البدء، يجب تحديد سؤال تحليلي واضح. مثلاً: هل المبيعات تنمو فعلاً أم أن الزيادة ناتجة عن موسم سنوي؟ هل تتكرر الذروة كل أسبوع؟ هل تغير أداء المنتج بعد حملة تسويقية؟ يحدد هذا السؤال التردد الزمني المناسب، وطريقة التجميع، وطول النافذة المتحركة، وطريقة تفسير النتائج.
لنفترض وجود ملف باسم sales.csv يحتوي على الأعمدة التالية: date للتاريخ وsales لقيمة المبيعات. يمكن تحميله مبدئياً كما يلي:
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv("sales.csv")
print(df.head())
print(df.dtypes)
في هذه المرحلة لا ينبغي افتراض أن عمود التاريخ صالح للتحليل. فقد يحتوي على نصوص غير قابلة للتحويل، أو صيغ تاريخ متعددة، أو قيم مكررة، أو مناطق زمنية مختلفة. كذلك، يجب التحقق مما إذا كانت قيمة المبيعات تمثل إجمالي اليوم، أم عدد الفواتير، أم متوسط قيمة الطلب؛ لأن معنى القياس يحدد عملية التجميع اللاحقة.
تهيئة الطوابع الزمنية وتنظيف السلسلة
الخطوة الأهم في Pandas هي تحويل عمود التاريخ إلى نوع datetime ثم جعله فهرساً للبيانات. يتيح الفهرس الزمني اختيار فترات محددة بسهولة، وإعادة أخذ العينات، وإجراء الحسابات المتحركة بطريقة طبيعية. ويُفضّل تمرير errors="coerce" عند التحويل حتى تتحول القيم غير الصحيحة إلى قيم مفقودة بدلاً من إيقاف البرنامج.
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df = df.dropna(subset=["date", "sales"])
df = df.set_index("date").sort_index()
print(df.index.min(), df.index.max())
print(df.index.is_monotonic_increasing)
يضمن sort_index() ترتيب السجلات زمنياً، وهو أمر ضروري لأن معظم العمليات الزمنية تفترض الترتيب التصاعدي. بعد ذلك ينبغي فحص التكرارات. قد تكون التواريخ المتكررة مشروعة، مثل وجود عدة معاملات بيع في اليوم نفسه، وقد تكون أخطاء إدخال. إذا كانت كل ملاحظة تمثل معاملة مستقلة، يمكن جمعها على مستوى اليوم؛ أما إذا كانت تمثل قياساً واحداً يفترض عدم تكراره، فيجب التحقيق في سبب التكرار قبل حذفه.
daily_sales = df.groupby(level=0)["sales"].sum().to_frame()
print(daily_sales.index.duplicated().sum())
من المهم أيضاً كشف الفجوات الزمنية. إذا كان النشاط يجب أن يُسجل يومياً لكن بعض الأيام غائبة، فإن المتوسطات والرسوم قد تعطي انطباعاً مضللاً. يمكن فرض تردد يومي باستخدام asfreq. لكن لا ينبغي ملء القيم المفقودة بالصفر تلقائياً؛ إذ قد يعني الغياب انقطاعاً في جمع البيانات، لا انعدام المبيعات.
daily_sales = daily_sales.asfreq("D")
print(daily_sales["sales"].isna().sum())
daily_sales["sales"] = daily_sales["sales"].interpolate(method="time")
الاستيفاء الزمني مناسب عندما تكون القيم المفقودة قليلة وتمثل فجوات قياس قصيرة، لكنه غير مناسب دائماً. ففي بيانات المبيعات، قد يكون الصفر قيمة ذات معنى إذا كان المتجر مغلقاً أو لم تحدث أي عملية بيع. لذلك يجب أن تنطلق سياسة معالجة القيم الناقصة من معرفة المجال التشغيلي، لا من قاعدة برمجية عامة.
الاستكشاف البصري وتحديد الإشارات الأولية
الرسم البياني هو أسرع وسيلة لفهم السلسلة الزمنية. فهو يكشف الاتجاهات الكبيرة، والقفزات، والانخفاضات الشاذة، والفجوات، والتغير في مستوى التذبذب. قد يبدو الجدول الرقمي سليماً، بينما يوضح الرسم وجود قيمة متطرفة ناتجة عن خطأ في وحدة القياس أو تكرار غير مقصود.
plt.figure(figsize=(14, 5))
plt.plot(daily_sales.index, daily_sales["sales"], color="steelblue")
plt.title("المبيعات اليومية")
plt.xlabel("التاريخ")
plt.ylabel("قيمة المبيعات")
plt.grid(alpha=0.3)
plt.show()
عند وجود عدة سنوات من البيانات، تصبح التقلبات اليومية كثيفة وقد تخفي البنية العامة. هنا يمكن رسم بيانات أسبوعية أو شهرية إلى جانب الرسم اليومي. كما يفيد اختيار نطاق زمني محدود، مثل ثلاثة أشهر، في فحص سلوك قصير الأجل لا يظهر في المخطط الكامل.
daily_sales.loc["2025-01":"2025-03"].plot(
figsize=(14, 5),
title="المبيعات خلال الربع الأول"
)
plt.show()
لا يقتصر الاستكشاف على الخط الزمني. فمن المفيد حساب الإحصاءات الوصفية، مثل المتوسط والوسيط والانحراف المعياري وأدنى وأعلى قيمة. الفارق الكبير بين المتوسط والوسيط قد يشير إلى وجود قيم مرتفعة استثنائياً، بينما ازدياد الانحراف المعياري بمرور الزمن قد يعني أن تذبذب المبيعات نفسه يتغير، وهي ملاحظة مهمة عند تصميم نماذج التنبؤ.
print(daily_sales["sales"].describe())
ينبغي كذلك توثيق الأحداث الخارجية المعروفة: إطلاق منتج جديد، انقطاع خدمة الدفع، عطلة رسمية، خصم كبير، أو تغيير في طريقة تسجيل البيانات. هذه الأحداث تفسر كثيراً من القيم غير المعتادة، وتمنع المحلل من تصنيفها خطأً على أنها موسمية أو جزء طبيعي من الاتجاه.
إعادة أخذ العينات لاكتشاف الاتجاهات طويلة الأجل
إعادة أخذ العينات، أو resampling، تعني تحويل البيانات من تردد زمني إلى آخر. فالسجلات اليومية يمكن تجميعها أسبوعياً أو شهرياً أو ربع سنوياً. وتساعد هذه العملية في تقليل الضوضاء اليومية وإبراز الاتجاه العام. في بيانات المبيعات، يُستخدم المجموع عند التجميع لأننا نريد إجمالي المبيعات خلال الفترة، بينما قد يكون المتوسط أكثر ملاءمة لبيانات الحرارة أو زمن الاستجابة.
weekly_sales = daily_sales["sales"].resample("W").sum()
monthly_sales = daily_sales["sales"].resample("MS").sum()
print(monthly_sales.head())
الرمز W يشير إلى تجميع أسبوعي، بينما يشير MS إلى بداية كل شهر. ويجب الانتباه إلى أن اختيار التردد ليس قراراً شكلياً: فإذا كان النشاط يتأثر بعطلة نهاية الأسبوع، فقد يطمس التجميع الشهري هذا الأثر؛ وإذا كان الهدف هو قراءة نمو سنوي، فقد تكون البيانات اليومية شديدة التشوش.
fig, ax = plt.subplots(figsize=(14, 5))
monthly_sales.plot(ax=ax, marker="o", color="darkgreen")
ax.set_title("إجمالي المبيعات الشهري")
ax.set_xlabel("الشهر")
ax.set_ylabel("إجمالي المبيعات")
ax.grid(alpha=0.3)
plt.show()
يمكن استخراج الاتجاه بصورة أولية عبر مقارنة متوسطات الفترات أو التغير النسبي بينها. على سبيل المثال، تحسب pct_change() نسبة التغير من شهر إلى الشهر السابق. إلا أن نسبة التغير وحدها لا تثبت وجود اتجاه ثابت؛ فقد يرتفع شهر وينخفض آخر بسبب الموسم. لذلك ينبغي ربطها دائماً بالرسم والمتوسطات المتحركة والمقارنة السنوية.
monthly_growth = monthly_sales.pct_change() * 100
print(monthly_growth.tail())
عند وجود موسمية سنوية، قد تكون مقارنة شهر معين بالشهر السابق مضللة. مقارنة يناير هذا العام بيناير العام السابق أكثر عدلاً في كثير من المجالات. يمكن إجراء ذلك باستخدام إزاحة مقدارها 12 شهراً بعد تحويل السلسلة إلى تردد شهري منتظم.
yearly_comparison = monthly_sales.pct_change(periods=12) * 100
print(yearly_comparison.tail())
المتوسطات المتحركة وعزل الاتجاه عن الضوضاء
المتوسط المتحرك هو متوسط القيم ضمن نافذة زمنية تنزلق تدريجياً على السلسلة. فإذا استخدمنا نافذة مدتها سبعة أيام، فإن كل نقطة جديدة تمثل متوسط الأيام السبعة الأخيرة. يؤدي ذلك إلى تنعيم التذبذبات القصيرة ويجعل الاتجاه أكثر وضوحاً. وتوفر Pandas هذه الإمكانية عبر الدالة rolling.
daily_sales["ma_7"] = daily_sales["sales"].rolling(
window=7,
min_periods=1
).mean()
daily_sales["ma_30"] = daily_sales["sales"].rolling(
window=30,
min_periods=1
).mean()
اختيار حجم النافذة يجب أن يعكس طبيعة العمل. نافذة سبعة أيام مفيدة غالباً عندما توجد دورة أسبوعية، ونافذة ثلاثين يوماً مناسبة لرؤية اتجاه شهري تقريبي. أما النافذة الطويلة جداً فقد تخفي تغيراً حقيقياً ومهماً، بينما النافذة القصيرة جداً قد لا تقلل الضوضاء بما يكفي.
plt.figure(figsize=(14, 5))
plt.plot(daily_sales.index, daily_sales["sales"],
alpha=0.35, label="المبيعات اليومية")
plt.plot(daily_sales.index, daily_sales["ma_7"],
label="متوسط متحرك 7 أيام")
plt.plot(daily_sales.index, daily_sales["ma_30"],
linewidth=2, label="متوسط متحرك 30 يوماً")
plt.title("مقارنة المبيعات بالمتوسطات المتحركة")
plt.legend()
plt.grid(alpha=0.3)
plt.show()
إذا كان المتوسط المتحرك لثلاثين يوماً يرتفع باستمرار، فهذا دليل بصري قوي على اتجاه صاعد، حتى لو كانت القيم اليومية متقلبة. أما إذا بقي المتوسط حول مستوى ثابت مع قمم متكررة، فربما تكون الموسمية هي المؤثر الأهم. ويمكن أيضاً استخدام المتوسط المتحرك المركزي عبر center=True لتحسين العرض البصري، لكنه يستفيد من بيانات مستقبلية داخل النافذة، لذلك لا يصلح مباشرةً عند بناء نظام تنبؤ لحظي.
اكتشاف الموسمية باستخدام التجميع الدوري والإزاحة الزمنية
الموسمية ليست مجرد وجود قمم وقيعان؛ بل هي تكرار منتظم لنمط معين في دورات زمنية معروفة. قد تكون أسبوعية، مثل ارتفاع الطلب يومي الخميس والجمعة، أو شهرية، أو سنوية، أو حتى يومية في بيانات الساعات. لاكتشافها، يمكن تجميع القيم حسب خصائص التاريخ مثل رقم الشهر أو اسم يوم الأسبوع.
daily_sales["day_name"] = daily_sales.index.day_name()
weekday_pattern = daily_sales.groupby("day_name")["sales"].mean()
ordered_days = [
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"
]
weekday_pattern = weekday_pattern.reindex(ordered_days)
print(weekday_pattern)
إذا ظهر متوسط أعلى باستمرار في أيام محددة، فهذا مؤشر على موسمية أسبوعية. لكن يجب الحذر: المتوسط قد يتأثر بفترة ترويج قصيرة أو بعدد غير متساوٍ من الملاحظات. لذلك من الأفضل فحص عدد السجلات لكل مجموعة، ومقارنة النمط عبر عدة أشهر أو سنوات.
daily_sales["month"] = daily_sales.index.month
monthly_pattern = daily_sales.groupby("month")["sales"].mean()
monthly_pattern.plot(kind="bar", figsize=(12, 4),
title="متوسط المبيعات حسب الشهر")
plt.xlabel("رقم الشهر")
plt.ylabel("متوسط المبيعات")
plt.show()
ومن الأدوات المفيدة أيضاً الإزاحة الزمنية عبر shift. إذا كانت البيانات يومية ويُشتبه بوجود دورة أسبوعية، يمكن مقارنة قيمة اليوم بقيمة اليوم الذي يسبقه بسبعة أيام. ويُحسب الارتباط بين السلسلة الأصلية والسلسلة المزاحة؛ فالارتباط المرتفع عند إزاحة سبعة أيام يدعم فرضية الموسمية الأسبوعية.
weekly_lag_corr = daily_sales["sales"].corr(
daily_sales["sales"].shift(7)
)
yearly_lag_corr = daily_sales["sales"].corr(
daily_sales["sales"].shift(365)
)
print("ارتباط الإزاحة الأسبوعية:", weekly_lag_corr)
print("ارتباط الإزاحة السنوية:", yearly_lag_corr)
لا ينبغي التعامل مع الارتباط المرتفع بوصفه برهاناً مطلقاً؛ إذ يمكن للاتجاه العام القوي أن يرفع الارتباط بين القيم المتباعدة زمنياً. لذلك تكون النتيجة أكثر موثوقية عند دمجها مع الرسم، والتجميع حسب اليوم أو الشهر، وتحليل سلسلة بعد تنعيمها أو بعد إزالة الاتجاه بصورة مناسبة.
التحقق من النتائج وتجنب الأخطاء الشائعة
بعد اكتشاف اتجاه أو موسمية، تأتي مرحلة التحقق. أول خطأ شائع هو الخلط بين الارتباط والسببية. ارتفاع المبيعات كل جمعة لا يعني بالضرورة أن يوم الجمعة هو السبب؛ فقد يرتبط الأمر بموعد دفع الرواتب أو بعروض دورية أو بسلوك شحن خاص بالشركة. لذا ينبغي ربط التحليل بالمعرفة التجارية والبيانات الخارجية عند الإمكان.
الخطأ الثاني هو تجاهل التردد الزمني. إذا جمعت بيانات ساعات إلى مستوى شهري، فلن تستطيع اكتشاف نمط الذروة المسائية. والخطأ الثالث هو ملء الفجوات أو معالجة القيم المتطرفة بلا توثيق. يجب الاحتفاظ بنسخة من البيانات الأصلية، وتسجيل كل قرار تنظيف، واختبار أثره في النتيجة.
كما ينبغي الفصل بين البيانات المستخدمة للفهم والبيانات المستخدمة لتقييم التنبؤ. في السلاسل الزمنية لا يصح خلط الصفوف عشوائياً كما يحدث في كثير من مسائل التعلم الآلي، لأن ذلك يسمح بتسرب معلومات من المستقبل إلى الماضي. يجب أن يكون التقسيم زمنياً: فترة تدريب قديمة، ثم فترة اختبار أحدث.
train = daily_sales.loc[:"2025-09-30"]
test = daily_sales.loc["2025-10-01":]
print(train.shape, test.shape)
وأخيراً، من المفيد إنشاء لوحة متابعة بسيطة تتضمن القيمة اليومية، والمتوسط المتحرك، والمقارنة بالشهر أو السنة السابقة، والتنبيهات للقيم المتطرفة. بهذه الطريقة يتحول التحليل من تقرير لمرة واحدة إلى عملية مراقبة مستمرة تدعم اتخاذ القرار.
خاتمة
يوفر Pandas بيئة عملية ومرنة لتحليل البيانات الزمنية دون الحاجة إلى البدء بأدوات معقدة. تبدأ العملية بتحويل التاريخ إلى فهرس زمني موثوق وتنظيف الفجوات والتكرارات، ثم استخدام الرسوم وإعادة أخذ العينات لفهم السلوك العام، والمتوسطات المتحركة لعزل الاتجاه، والتجميع الدوري والإزاحات الزمنية لاكتشاف الموسمية.
النتائج الجيدة لا تأتي من دالة واحدة، بل من الجمع بين الفهم الإحصائي والسياق العملي والتحقق البصري. وعندما يصبح الاتجاه والموسمية واضحين، يمكن الانتقال بثقة إلى مراحل أكثر تقدماً، مثل التفكيك الموسمي، ونماذج التنبؤ، وقياس أثر الحملات والأحداث على السلسلة الزمنية.
تعليقات
إرسال تعليق