الوحدة 6 — تنظيف البيانات
أثبتت الوحدة 8 من دورة المقدّمة أنّ تحضير البيانات يستهلك معظم المشروع، وأنّ لا خوارزمية تُصلِح بيانات خاطئة. تحوّل هذه الوحدة ذلك الاستنتاج إلى حركات ملموسة. الخيط الناظم: كلّ قرار تنظيف خيارٌ منهجي يستحقّ سطر تبرير، لا ردّ فعل آلياً.
شخّص أولاً، عالج ثانياً
الخطأ الأشيع هو التنظيف قبل القياس. ثلاثة أوامر ترسم الجرد:
df.isna().mean().sort_values(ascending=False) # نسبة النواقص لكلّ عمود
df.duplicated().sum() # صفوف متطابقة تماماً
df.dtypes # الأنواع الفعلية مقابل المنتظرة
أكملها بـvalue_counts() على كلّ عمود فئوي (أخطاء إملائية، تفاوت حالة الأحرف) وdescribe() على كلّ رقمي (قيم مستحيلة: أعمار سالبة، مبالغ صفرية، تواريخ مستقبلية).
القيم الناقصة: ثلاثة أسئلة قبل أيّ معالجة
لماذا تنقص؟ الجواب يغيّر كلّ شيء. حسّاس معطّل أسبوعاً (غياب عرضي)، وحقل اختياري في الاستمارة (غياب بنيوي)، ودخل لم يصرَّح به تحديداً عند أصحاب الدخول العالية (غياب د الّ). في الحالة الثالثة، الحذف أو التعويض يمحو إشارة حقيقية — وأحياناً يكون عمود income_missing المنطقي أفضل.
كم تنقص؟ عند 2% يصلح كلّ علاج معقول تقريباً. عند 40% يصبح العمود نفسه موضع سؤال. وبينهما تتوقّف الموازنة على أهمّية المتغيّر.
حذف أم تعويض؟
df = df.dropna(subset=["amount"]) # حذف الصفوف — إن كانت نادرة وغير دالّة
df["age"] = df["age"].fillna(df["age"].median()) # تعويض بالوسيط — متين أمام القيم القصوى
df["country"] = df["country"].fillna("غير معروف") # فئة صريحة
df["temperature"] = df["temperature"].interpolate() # سلاسل زمنية
إن كانت البيانات ستدرّب نموذجاً، فإحصاءات التعويض (الوسيط، المتوسّط) تُحسَب على مجموعة التدريب وحدها ثم تُطبَّق على الاختبار. حسابها على كامل البيانات تسرّبُ بيانات — وقد أظهرت دورة المقدّمة (الوحدة 5) كلفته. هذا بالضبط ما تؤتمته خطوط scikit-learn.
التكرارات: التامّة والتقريبية
df = df.drop_duplicates() # صفوف متطابقة تماماً
df = df.drop_duplicates(subset=["client_id", "date"]) # تكرارات "مهنية"
df = df.sort_values("updated").drop_duplicates(subset=["client_id"], keep="last") # الأحدث يبقى
التكرار التامّ سهل. أمّا التكرار المهني — صفّان مختلفان يصفان الواقع نفسه (العميل نفسه بعنوانين مكتوبين بطريقتين) — فيستلزم تعريف مفتاح التفرّد مع أهل المهنة، وقاعدة البقاء (keep). والسؤال المسبق في كلّ الأحوال: هل التكرار خطأ جمع أم واقع (قد يطلب عميل مرّتين في اليوم نفسه بصورة مشروعة)؟
الأنواع: العمود الذي يكذب في طبيعته
عمود منمّط خطأً يفسد كلّ ما بعده في صمت: المبالغ النصّية لا تُجمَع، والتواريخ النصّية لا تُقارَن، وdescribe() يتجاهلها.
df["amount"] = pd.to_numeric(df["amount"], errors="coerce") # غير القابل للتحويل ← NaN
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df["zip_code"] = df["zip_code"].astype("string") # الرمز ليس رقماً
df["segment"] = df["segment"].astype("category") # فئوي: ذاكرة ودلالة
errors="coerce" تحوّل غير القابل للتحويل إلى NaN يُعاد عدّها بعد ذلك: إن صارت 3000 قيمة فارغة فالمشكلة في المنبع (فاصل عشري، «N/A» حرفية، وحدات ملتصقة بالأرقام) وتُصلَح عند المصدر، غالباً منذ read_csv (decimal=","، na_values=["N/A", "-"]).
حالة المعرّفات الخاصة: الرموز البريدية وأرقام السجلّات وأرقام الهواتف سلاسل نصّية. تنميطها أعداداً صحيحة يدمّر الأصفار البادئة — ضرر كلاسيكي لا رجعة فيه بعد إعادة كتابة الملفّ.
تضارب الإدخال: المِلحق .str
df["city"] = df["city"].str.strip().str.title() # الفراغات وحالة الأحرف
df["country"] = df["country"].replace({"KSA": "السعودية", "سعودية": "السعودية"})
منهج العمل: value_counts() قبلُ لرؤية الصيغ، ثم توحيد، ثم value_counts() بعدُ للتحقّق. وللمرجعيات المهمّة (بلدان، مناطق) يتفوّق جدول مطابقة صريح على الإصلاحات المتفرّقة: فهو قابل للمراجعة وإعادة الاستخدام.