القيم المفقودة

المشكلة

تتطلب معظم خوارزميات المكتبة ألا تحتوي البيانات على فراغات.

وهناك أسباب كثيرة تؤدي لفقدان البيانات في كثير من الأحيان، منها:

  • خطأ طبيعي: إغفال الإدخال سهواً أو تعمد المستخدم عدم الإجابة لحماية خصوصيته.
  • عطل تقني: خلل في الحساسات أو انقطاع الاتصال أثناء نقل البيانات.
  • دمج البيانات: فجوات ناتجة عن عمليات الربط (SQL Joins) أو تغير هيكلة الجداول عبر الزمن.
  • الانسحاب: فقدان البيانات بسبب انسحاب المشاركين من الدراسة المرحلية أو حذف الحسابات.
    • مثال: في التجارب الطبية، قد ينسحب المريض بسبب الهجرة، الوفاة، أو ببساطة الملل من المتابعة، أو لأن حالته ساءت أو لأنه عانى من أعراض جانبية حادة.
    • مثال: في الأنظمة التي تعتمد على الاشتراكات، هم العملاء الذين توقفوا عن استخدام الخدمة. تظهر بياناتهم فارغة في الأشهر التي تلت توقفهم.

عينة من البيانات

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

import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer, KNNImputer
# بيانات واقعية: قياسات لمرضى (عمر، ضغط دم انقباضي mmHg، كولسترول mg/dL، مؤشر كتلة الجسم)
np.random.seed(42)
n = 12
age = np.random.randint(35, 70, size=n)
systolic_bp = 100 + 0.5 * age + np.random.randn(n) * 8  # علاقة تقريبية مع العمر
cholesterol = 150 + 0.8 * age + np.random.randn(n) * 15
bmi = 22 + 0.1 * age + np.random.randn(n) * 2

df = pd.DataFrame({
    "عمر": age.astype(float),
    "ضغط_دم": systolic_bp,
    "كولسترول": cholesterol,
    "مؤشر_كتلة_الجسم": bmi,
})

# إدخال قيم مفقودة بشكل واقعي (مثلاً: مريض لم يُسجّل ضغطه، آخر لم يُسجّل الكولسترول)
df.loc[1, "ضغط_دم"] = np.nan
df.loc[3, "كولسترول"] = np.nan
df.loc[5, "عمر"] = np.nan
df.loc[7, "مؤشر_كتلة_الجسم"] = np.nan
df.loc[9, "ضغط_دم"] = np.nan
df.loc[9, "كولسترول"] = np.nan

print("البيانات مع القيم المفقودة (NaN):")
display(df)
البيانات مع القيم المفقودة (NaN):
عمر ضغط_دم كولسترول مؤشر_كتلة_الجسم
0 63.0 132.138654 192.523157 25.544121
1 49.0 NaN 217.891569 26.193767
2 42.0 121.177775 153.199206 25.277069
3 55.0 124.077657 NaN 27.633315
4 53.0 122.245461 204.087890 26.947429
5 NaN 127.560196 179.083534 30.101786
6 45.0 124.276631 202.953423 27.896798
7 45.0 116.356188 191.596784 NaN
8 58.0 130.139717 190.602906 25.985627
9 58.0 NaN NaN 30.177251
10 37.0 127.574714 188.091692 27.271064
11 56.0 127.162036 184.233198 32.912020

الحل السهل: حذف الصفوف التي تحتوي على أي قيمة مفقودة

df_dropped = df.dropna()
print(f"عدد الصفوف الأصلي: {len(df)}")
print(f"بعد حذف الصفوف ذات القيم المفقودة: {len(df_dropped)} صف")
print("→ خسرنا معلومات من صفوف كاملة بسبب قيمة واحدة مفقودة في كل صف.")
display(df_dropped)
عدد الصفوف الأصلي: 12
بعد حذف الصفوف ذات القيم المفقودة: 7 صف
→ خسرنا معلومات من صفوف كاملة بسبب قيمة واحدة مفقودة في كل صف.
عمر ضغط_دم كولسترول مؤشر_كتلة_الجسم
0 63.0 132.138654 192.523157 25.544121
2 42.0 121.177775 153.199206 25.277069
4 53.0 122.245461 204.087890 26.947429
6 45.0 124.276631 202.953423 27.896798
8 58.0 130.139717 190.602906 25.985627
10 37.0 127.574714 188.091692 27.271064
11 56.0 127.162036 184.233198 32.912020

طرق الملء

لو تصوَّرنا البيانات في جدوَل، فإن الحل البسيط قد يكون بإزالة تلك الصفوف ذات القيَم المفقودة. لكن ذلك يؤدي لخسارة المعلومات الأخرى الموجودة في نفس الصف. والأفضل أن نقوم باستكمال (Impute) القيم المفقودة، أي: استنتاجها من الجزء المعلوم من البيانات.

واستنتاجُها يكون بأحد طريقين:

  • الأول: من القيَم الأخرى لنفس المتغيِّر (SimpleImputer). كحساب المتوسط أو الوسيط أو الأكثر تكرارًا أو قيمة ثابتة.
  • الثاني: باعتبار المتغيرات الأخرى (مثل: IterativeImputer). ويكون بذلك مسألة تنبؤ: انحدار (Regression) أو تصنيف (Classification) بحسب نوع المتغيِّر ذي القيمة المفقودة.

الأولى: التقدير البسيط (SimpleImputer)

استكمال القيم المفقودة من نفس المتغيّر فقط: المتوسط، الوسيط، الأكثر تكراراً، أو قيمة ثابتة.

X = df.values

mean_imputer = SimpleImputer(strategy="mean")
X_mean = mean_imputer.fit_transform(X)
df_mean = pd.DataFrame(X_mean, columns=df.columns, index=df.index)
print("بعد الاستكمال بالمتوسط (mean):")
display(df_mean.round(2))
بعد الاستكمال بالمتوسط (mean):
عمر ضغط_دم كولسترول مؤشر_كتلة_الجسم
0 63.0 132.14 192.52 25.54
1 49.0 125.27 217.89 26.19
2 42.0 121.18 153.20 25.28
3 55.0 124.08 190.43 27.63
4 53.0 122.25 204.09 26.95
5 51.0 127.56 179.08 30.10
6 45.0 124.28 202.95 27.90
7 45.0 116.36 191.60 27.81
8 58.0 130.14 190.60 25.99
9 58.0 125.27 190.43 30.18
10 37.0 127.57 188.09 27.27
11 56.0 127.16 184.23 32.91
median_imputer = SimpleImputer(strategy="median")
X_median = median_imputer.fit_transform(X)
df_median = pd.DataFrame(X_median, columns=df.columns, index=df.index)
print("بعد الاستكمال بالوسيط (median):")
display(df_median.round(2))
بعد الاستكمال بالوسيط (median):
عمر ضغط_دم كولسترول مؤشر_كتلة_الجسم
0 63.0 132.14 192.52 25.54
1 49.0 125.72 217.89 26.19
2 42.0 121.18 153.20 25.28
3 55.0 124.08 191.10 27.63
4 53.0 122.25 204.09 26.95
5 53.0 127.56 179.08 30.10
6 45.0 124.28 202.95 27.90
7 45.0 116.36 191.60 27.27
8 58.0 130.14 190.60 25.99
9 58.0 125.72 191.10 30.18
10 37.0 127.57 188.09 27.27
11 56.0 127.16 184.23 32.91

الثانية: التقدير بأقرب الجيران (KNNImputer)

يستكمل القيمة المفقودة بناءً على أقرب العينات في المسافة (مع تجاهل القيم المفقودة في حساب المسافة).

knn_imputer = KNNImputer(n_neighbors=3)
X_knn = knn_imputer.fit_transform(X)
df_knn = pd.DataFrame(X_knn, columns=df.columns, index=df.index)
print("بعد الاستكمال بـ KNN (n_neighbors=3):")
display(df_knn.round(2))
بعد الاستكمال بـ KNN (n_neighbors=3):
عمر ضغط_دم كولسترول مؤشر_كتلة_الجسم
0 63.00 132.14 192.52 25.54
1 49.00 123.53 217.89 26.19
2 42.00 121.18 153.20 25.28
3 55.00 124.08 189.13 27.63
4 53.00 122.25 204.09 26.95
5 56.33 127.56 179.08 30.10
6 45.00 124.28 202.95 27.90
7 45.00 116.36 191.60 27.60
8 58.00 130.14 190.60 25.99
9 58.00 126.27 184.64 30.18
10 37.00 127.57 188.09 27.27
11 56.00 127.16 184.23 32.91

ملاحظة: تتطلب هذه العملية تقييس المقادير المتصلة (Numerical Feature Scaling) لتحصل على نتائج دقيقة.