import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer, KNNImputerالقيم المفقودة
المشكلة
تتطلب معظم خوارزميات المكتبة ألا تحتوي البيانات على فراغات.
وهناك أسباب كثيرة تؤدي لفقدان البيانات في كثير من الأحيان، منها:
- خطأ طبيعي: إغفال الإدخال سهواً أو تعمد المستخدم عدم الإجابة لحماية خصوصيته.
- عطل تقني: خلل في الحساسات أو انقطاع الاتصال أثناء نقل البيانات.
- دمج البيانات: فجوات ناتجة عن عمليات الربط (SQL Joins) أو تغير هيكلة الجداول عبر الزمن.
- الانسحاب: فقدان البيانات بسبب انسحاب المشاركين من الدراسة المرحلية أو حذف الحسابات.
- مثال: في التجارب الطبية، قد ينسحب المريض بسبب الهجرة، الوفاة، أو ببساطة الملل من المتابعة، أو لأن حالته ساءت أو لأنه عانى من أعراض جانبية حادة.
- مثال: في الأنظمة التي تعتمد على الاشتراكات، هم العملاء الذين توقفوا عن استخدام الخدمة. تظهر بياناتهم فارغة في الأشهر التي تلت توقفهم.
عينة من البيانات
نستخدم مجموعة صغيرة من قياسات صحية (عمر، ضغط دم انقباضي، كولسترول، مؤشر كتلة الجسم) مع قيم مفقودة. نرى أولاً كيف تبدو البيانات، ثم ماذا يحدث إذا حذفنا الصفوف التي تحتوي على قيم مفقودة.
# بيانات واقعية: قياسات لمرضى (عمر، ضغط دم انقباضي 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) لتحصل على نتائج دقيقة.
- للمزيد راجع: 7.4. Imputation of missing values.