تنظيف البيانات الزمنية في Pandas: معالجة القيم المفقودة والفجوات والتكرارات
مقدمة
تُعد البيانات الزمنية من أكثر أنواع البيانات انتشاراً في علم البيانات، إذ تظهر في سجلات المبيعات اليومية، وقراءات الحساسات، وأسعار الأسهم، وحركة المستخدمين، ومؤشرات الأداء التشغيلي. غير أن هذه البيانات نادراً ما تصل في صورة مثالية؛ فقد تحتوي على تواريخ غير موحّدة، وصفوف مكررة، وقيم مفقودة، وفجوات زمنية تجعل التحليل أو التنبؤ غير موثوقين.
لذلك فإن تنظيف البيانات الزمنية في Pandas: معالجة القيم المفقودة والفجوات والتكرارات ليس خطوة تجميلية تسبق التحليل، بل عملية أساسية لحماية صحة الاستنتاجات والنماذج. توفر مكتبة Pandas أدوات مرنة لمعالجة هذه المشكلات، بدءاً من تحويل الأعمدة إلى نوع التاريخ والوقت، ووصولاً إلى الاستيفاء وإعادة الفهرسة واكتشاف السجلات المتكررة. يشرح هذا المقال منهجاً عملياً متكاملاً لتنظيف السلاسل الزمنية مع أمثلة قابلة للتطبيق.
تهيئة البيانات الزمنية وتوحيد بنية التاريخ
قبل معالجة القيم الناقصة أو التكرارات، يجب التأكد من أن عمود الزمن ممثل بالنوع الصحيح. قد تأتي التواريخ من ملفات CSV على هيئة نصوص، وقد تستخدم صيغاً مختلفة مثل 2026-01-05 أو 05/01/2026. التعامل مع هذه القيم كنصوص يؤدي إلى أخطاء في الفرز، والتجميع، وحساب الفواصل الزمنية.
تُستخدم الدالة pd.to_datetime() لتحويل العمود إلى نوع datetime64. ومن الأفضل تمرير errors="coerce" أثناء مرحلة التنظيف؛ إذ تُحوّل القيم غير القابلة للتفسير إلى NaT بدلاً من إيقاف البرنامج بخطأ. تمثل NaT القيمة الزمنية المفقودة، وهي النظير الزمني للقيمة NaN.
import pandas as pd
df = pd.read_csv("sales.csv")
df["date"] = pd.to_datetime(
df["date"],
format="%Y-%m-%d",
errors="coerce"
)
print(df[df["date"].isna()])
إذا كانت البيانات تحتوي على صيغ متعددة، يمكن حذف الوسيط format وترك Pandas يستنتج الصيغة، لكن تحديد الصيغة صراحةً يكون أسرع وأكثر أماناً عندما تكون معروفة. ويجب الانتباه إلى اليوم والشهر في الصيغ المحلية؛ فالتاريخ 03/04/2026 قد يعني الثالث من أبريل أو الرابع من مارس. في هذه الحالة يمكن استخدام dayfirst=True.
بعد التحويل، تُرتّب البيانات زمنياً ويُفضّل جعل الوقت فهرساً للسلسلة، لأن معظم عمليات الفهرسة الزمنية وإعادة أخذ العينات تعتمد عليه:
df = df.dropna(subset=["date"])
df = df.sort_values("date")
df = df.set_index("date")
print(df.index.is_monotonic_increasing)
يمكن كذلك توحيد المنطقة الزمنية عند دمج مصادر مختلفة. فالبيانات القادمة من خادم يعمل بالتوقيت العالمي المنسق قد لا تتوافق مع بيانات متجر يستخدم توقيتاً محلياً. يساعد استخدام utc=True عند التحويل، أو الدالتين tz_localize() وtz_convert()، على منع انزياحات زمنية خفية تؤثر في النتائج.
فحص الجودة واكتشاف القيم المفقودة
القيمة المفقودة ليست دائماً خطأً واحداً له المعنى نفسه. قد تعني أن حساساً توقف عن الإرسال، أو أن المتجر لم يفتح في يوم معين، أو أن عملية الإدخال لم تكتمل، أو أن القيمة غير معروفة فعلاً. لذلك يجب قياس حجم النقص وتحديد مواضعه قبل اختيار طريقة العلاج.
تُظهر isna() مواضع القيم المفقودة، بينما تجمع sum() عددها في كل عمود. ومن المفيد حساب النسبة المئوية أيضاً، لأن فقدان عشر قيم قد يكون ضئيلاً في جدول كبير وخطيراً في جدول صغير.
missing_count = df.isna().sum()
missing_ratio = (df.isna().mean() * 100).round(2)
quality_report = pd.DataFrame({
"عدد القيم المفقودة": missing_count,
"النسبة المئوية": missing_ratio
})
print(quality_report)
ينبغي التفريق بين غياب القيمة داخل صف زمني موجود، وغياب الصف الزمني بالكامل. فمثلاً، إذا وُجد صف ليوم 10 يناير وقيمة المبيعات فيه NaN، فهذه قيمة مفقودة. أما إذا لم يظهر يوم 10 يناير في الفهرس إطلاقاً، فتلك فجوة زمنية، وتعالج بأسلوب مختلف. كما يجب فحص الأعمدة التي لا يمكن أن تقبل قيماً مفقودة منطقياً، مثل معرّف الجهاز أو رمز المنتج.
قبل ملء القيم، من المفيد استكشاف نمطها. فإذا كانت القيم المفقودة تتركز دائماً في عطلة نهاية الأسبوع، فقد يكون السبب عدم وجود نشاط وليس خللاً في الجمع. أما إذا ظهرت ككتل متتالية خلال ساعات التشغيل، فقد تدل على عطل تقني. ويمكن كشف الفترات المتصلة المفقودة بصرياً أو عبر فحص السجلات المجاورة، بدلاً من الاعتماد على العدد الإجمالي فقط.
اختيار استراتيجية مناسبة لمعالجة القيم المفقودة
تقدم Pandas طريقتين أساسيتين: حذف البيانات الناقصة باستخدام dropna()، أو تعويضها باستخدام fillna(). ولا توجد طريقة صحيحة في كل الحالات؛ فالاختيار يتحدد بمعنى المتغير، وحجم النقص، وطبيعة الظاهرة، والغرض من التحليل.
يفيد dropna() عندما تكون الصفوف المفقودة قليلة وغير مؤثرة، أو حين تكون القيمة المفقودة في حقل أساسي لا يمكن استنتاجه بأمان. مثلاً، لا يُستحسن اختراع معرّف عميل مفقود. لكن حذف الصفوف عشوائياً من سلسلة زمنية قد يخلق فجوات أو يغيّر التوزيع، لذلك يجب استخدامه بحذر.
# حذف الصفوف التي لا تحتوي على قيمة للمبيعات
df = df.dropna(subset=["sales"])
أما fillna() فيلائم الحالات التي يكون فيها التعويض ذا معنى عملي. يمكن ملء عدد الطلبات بصفر إذا كان غياب التسجيل يعني عدم وجود طلبات، أو ملء قيمة ثابتة مثل الوسيط في بيانات غير زمنية. غير أن استخدام الصفر دون تفسير صحيح قد يحول عطل جهاز القياس إلى إشارة كاذبة تدل على انعدام النشاط.
# ملء عدد الطلبات المفقود بصفر فقط عندما يكون الصفر ذا معنى
df["orders"] = df["orders"].fillna(0)
# ملء سعر مفقود بوسيط العمود
df["price"] = df["price"].fillna(df["price"].median())
في السلاسل الزمنية، يشيع استخدام الملء الأمامي ffill() لتمرير آخر قيمة معروفة إلى الفترات اللاحقة، مثل حالة سعر رسمي يبقى ثابتاً حتى صدور تحديث جديد. ويصلح الملء الخلفي bfill() في حالات محدودة، كاستكمال قيمة تصنيف تبدأ صلاحيتها من أول سجل لاحق. ويجب تقييد مدى الملء بالوسيط limit كي لا تنتقل قيمة قديمة عبر فجوة طويلة.
df["exchange_rate"] = df["exchange_rate"].ffill(limit=2)
القاعدة العملية هي الاحتفاظ بمعلومة عن القيم التي عُوّضت. يمكن إنشاء عمود منطقي قبل التعديل، مثل sales_was_missing، حتى يستطيع فريق التحليل أو التدقيق التمييز لاحقاً بين القياسات الأصلية والقيم المعالجة.
اكتشاف الفجوات الزمنية وإعادة بناء التردد المتوقع
قد تكون البيانات خالية من قيم NaN تماماً، ومع ذلك تحتوي على مشكلة كبيرة: أيام أو ساعات أو دقائق مفقودة بالكامل. لا تستطيع isna() كشف هذه الحالة لأن الصف غير موجود أصلاً. لذلك يجب تحديد التردد المتوقع للسلسلة، مثل يومي D أو ساعي h أو شهري MS.
يمكن استخدام date_range() لبناء فهرس زمني كامل، ثم مقارنة الفهرس الموجود بالفهرس المتوقع. يوضح المثال التالي التواريخ الغائبة من سلسلة يومية:
expected_index = pd.date_range(
start=df.index.min(),
end=df.index.max(),
freq="D"
)
missing_dates = expected_index.difference(df.index)
print("التواريخ المفقودة:")
print(missing_dates)
ولإدخال الفجوات ضمن الجدول نفسه، تستخدم الدالة reindex(). ستُضاف صفوف جديدة للتواريخ المفقودة وتحتوي الأعمدة المقابلة على NaN. عندئذ تصبح الفجوات ظاهرة وقابلة للمعالجة بمنهج موحد.
df_daily = df.reindex(expected_index)
df_daily.index.name = "date"
print(df_daily.loc["2026-01-01":"2026-01-10"])
بديل عملي هو asfreq("D") عندما يكون الفهرس مضبوطاً بالفعل وتريد فرض تردد محدد. أما resample() فيستخدم عند تحويل الدقة الزمنية، كتحويل معاملات على مستوى الساعة إلى إجماليات يومية. يجب عدم الخلط بينهما: إعادة الفهرسة تضيف نقاطاً متوقعة، بينما إعادة أخذ العينات تجمع أو تحول البيانات إلى فترات جديدة.
من المهم أيضاً التمييز بين الفجوات الشرعية وغير الشرعية. فغياب بيانات نهاية الأسبوع قد يكون طبيعياً في أسواق مالية معينة، وفرض فهرس يومي عليها قد يخلق قيماً مفقودة مصطنعة. في هذه الحالة قد يكون التردد الصحيح هو أيام العمل B، أو قد يلزم استخدام تقويم أعمال خاص بالمؤسسة.
الاستيفاء الزمني دون تشويه الإشارة
الاستيفاء هو تقدير قيم داخل فجوة بالاعتماد على القيم المحيطة بها. وهو مناسب غالباً لقياسات مستمرة نسبياً، مثل درجة الحرارة أو مستوى استهلاك الطاقة أو قراءة حساس ضغط. أما في بيانات المبيعات والمعاملات والأحداث المنفصلة، فقد يكون الاستيفاء غير واقعي؛ إذ إن القيمة بين يومين ليست بالضرورة خطاً مستقيماً بينهما.
عند امتلاك فهرس من نوع تاريخ ووقت، تسمح interpolate(method="time") بالاستيفاء وفق المسافات الزمنية الفعلية، لا وفق ترتيب الصفوف فقط. وهذا مهم خصوصاً إذا لم تكن الفواصل متساوية تماماً.
df_daily["temperature"] = df_daily["temperature"].interpolate(
method="time",
limit=3,
limit_area="inside"
)
في هذا المثال، يمنع limit=3 ملء أكثر من ثلاث فترات متتابعة، ويضمن limit_area="inside" ملء الفجوات المحصورة بين قياسات حقيقية فقط، دون مدّ أول قيمة أو آخر قيمة إلى ما قبل السلسلة أو ما بعدها. وهذان القيدان مهمان لتقليل الافتراضات غير المبررة.
تدعم Pandas أساليب أخرى مثل الاستيفاء الخطي linear، والقيمي polynomial، والسبلاين، لكن زيادة تعقيد النموذج لا تعني دائماً زيادة الموثوقية. قد تنتج الطرق متعددة الحدود تذبذبات غير منطقية، خاصة قرب أطراف الفجوات. لذا يُفضّل البدء بالاستيفاء الزمني البسيط ومقارنة النتيجة مع المعرفة بالمجال.
بعد الاستيفاء، يُستحسن وضع علامة على الصفوف التي كانت فجوات أصلاً. بهذه الطريقة لا تختلط القيم المقدّرة بالقراءات الحقيقية عند تدريب نموذج أو إعداد تقرير تشغيلي.
إزالة التكرارات ومعالجة تضارب السجلات
تظهر التكرارات الزمنية بأكثر من صورة. قد يتكرر الصف كله حرفياً بسبب استيراد الملف مرتين، أو قد يتكرر الطابع الزمني مع اختلاف قيمة القياس نتيجة وصول تحديث لاحق، أو قد يكون التكرار مشروعاً لأن عدة منتجات أو أجهزة سجّلت حدثاً في اللحظة نفسها. لذلك لا يجوز حذف التكرارات قبل تحديد مفتاح السجل الصحيح.
لكشف الصفوف المكررة تماماً، تستخدم duplicated()، ثم يمكن حذفها بـ drop_duplicates(). ولضمان أن التمييز يتعلق بوقت وجهاز محددين، نستخدم وسيط subset.
# إظهار السجلات المتكررة بحسب الوقت ومعرّف الجهاز
duplicates = df.reset_index().loc[
lambda x: x.duplicated(subset=["date", "device_id"], keep=False)
]
print(duplicates)
# الاحتفاظ بآخر سجل عند وجود تكرار
df = (
df.reset_index()
.sort_values("date")
.drop_duplicates(subset=["date", "device_id"], keep="last")
.set_index("date")
)
الاحتفاظ بآخر سجل ليس قاعدة عامة؛ فقد يكون المطلوب الاحتفاظ بالأول، أو اختيار السجل الذي يحمل أعلى جودة، أو تجميع القيم. فعند وجود عدة معاملات صحيحة في الساعة نفسها، قد يكون الإجراء المناسب هو جمعها بدلاً من حذفها. ويمكن تنفيذ ذلك بالتجميع حسب الوقت والمعرّف واختيار دالة مناسبة لكل عمود.
df = (
df.reset_index()
.groupby(["date", "device_id"], as_index=False)
.agg({
"sales": "sum",
"temperature": "mean"
})
.set_index("date")
)
توضح هذه الخطوة أهمية فهم دلالة البيانات: تكرار قراءة الحرارة قد يمثل إعادة إرسال لنفس الرسالة، بينما تكرار المبيعات قد يمثل عمليتين مستقلتين. لا تعالج التكرارات كمسألة برمجية بحتة، بل كقرار متعلق بقواعد العمل ومصدر البيانات.
التحقق النهائي وبناء خط أنابيب قابل للتكرار
لا تكتمل عملية التنظيف بمجرد تنفيذ الدوال؛ بل يجب التحقق من أن النتيجة تفي بشروط الجودة المطلوبة. تشمل الفحوصات الأساسية: ترتيب الفهرس، وعدم وجود تكرارات في المفتاح الزمني المحدد، وعدد القيم المفقودة المتبقية، وانتظام التردد المتوقع، وعدم ظهور قيم مستحيلة بعد الملء أو الاستيفاء.
assert df.index.is_monotonic_increasing
assert not df.reset_index().duplicated(
subset=["date", "device_id"]
).any()
print("القيم المفقودة المتبقية:")
print(df.isna().sum())
من الأفضل تغليف خطوات التنظيف في دالة أو خط أنابيب واضح، بدلاً من تنفيذها يدوياً في خلايا متفرقة. يضمن ذلك قابلية إعادة الإنتاج عند وصول ملفات جديدة، ويجعل مراجعة القرارات أسهل. كما يُستحسن حفظ نسخة خام من البيانات وعدم الكتابة فوقها، ثم تصدير النسخة المنظفة مع تقرير يلخص عدد الصفوف المحذوفة، والقيم المعوضة، والفجوات المكتشفة، والسجلات المكررة.
وأخيراً، ينبغي إجراء فحص بصري للسلسلة قبل التنظيف وبعده. فالرسوم الزمنية قد تكشف هبوطاً أو قفزات غير منطقية لم تظهر في اختبارات الجداول. الجودة الحقيقية لا تعني فقط غياب NaN، بل تعني أن السلسلة الناتجة تحافظ على المعنى التشغيلي والواقعي للظاهرة التي تقيسها.
خاتمة
يتطلب تنظيف البيانات الزمنية في Pandas منهجاً يتجاوز ملء الخانات الفارغة. تبدأ العملية بتحويل التاريخ إلى نوع صحيح وتوحيد المنطقة الزمنية، ثم قياس القيم المفقودة وفهم سببها، واكتشاف الفجوات التي لا تظهر كصفوف، واختيار التعويض أو الاستيفاء وفق طبيعة المتغير. كذلك يجب التعامل مع التكرارات بناءً على مفتاح أعمال واضح، لا بحذف آلي قد يفقد معلومات صحيحة.
تمنح أدوات مثل to_datetime() وfillna() وreindex() وinterpolate() وdrop_duplicates() أساساً قوياً لهذه المهمة. لكن قيمتها الحقيقية تظهر عندما تُستخدم ضمن قواعد موثقة واختبارات جودة قابلة للتكرار. عندها تصبح السلسلة الزمنية أكثر جاهزية للتحليل الإحصائي، والتصور البياني، وبناء نماذج التنبؤ بثقة أكبر.
تعليقات
إرسال تعليق