تنظيف البيانات (Data Cleaning): اكتشاف ومعالجة القيم المفقودة (Missing Values)


تنظيف البيانات (Data Cleaning): اكتشاف ومعالجة القيم المفقودة (Missing Values)

في هذا الدرس، سنتعلم كيفية اكتشاف القيم المفقودة في مجموعات البيانات وكيفية معالجتها بفعالية باستخدام مكتبات Python القوية مثل Pandas وScikit-learn.

سنستعرض أيضاً لمحة سريعة عن كيفية التعامل مع هذه المشكلة في بيئات البيانات الضخمة باستخدام PySpark.

الخطوة 1: استيراد البيانات واكتشاف القيم المفقودة

نبدا بتحميل مجموعة بيانات (سنفترض وجود ملف CSV) ثم نستخدم دوال Pandas لاكتشاف مكان وعدد القيم المفقودة.

ملاحظة تقنية: القيم المفقودة يمكن أن تظهر بأشكال مختلفة مثل NaN (ليس رقم)، None، أو حتى سلاسل نصية مثل '?' أو 'N/A'. Pandas يتعامل مع NaN و None تلقائياً، ولكن قد تحتاج لتحويل القيم الأخرى يدوياً.

import pandas as pd
import numpy as np

# إنشاء بيانات عينة لغرض الشرح، يمكنك استبدالها بملف CSV حقيقي
data = {
    'الاسم': ['أحمد', 'ليلى', 'سارة', 'خالد', 'فاطمة', 'عمر', 'زينب'],
    'العمر': [25, 30, np.nan, 35, 28, np.nan, 22],
    'المدينة': ['الرياض', 'جدة', 'الدمام', np.nan, 'مكة', 'الرياض', 'جدة'],
    'الراتب': [5000, 7000, 6000, np.nan, 8000, 5500, 6500]
}
df = pd.DataFrame(data)

print("البيانات الأصلية:")
print(df)
print("\n")

# اكتشاف القيم المفقودة في كل عمود
missing_values = df.isnull().sum()
print("عدد القيم المفقودة في كل عمود:")
print(missing_values)
print("\n")

# عرض نسبة القيم المفقودة
missing_percentage = (df.isnull().sum() / len(df)) * 100
print("نسبة القيم المفقودة في كل عمود:")
print(missing_percentage)

الخطوة 2: تحليل وتصوير القيم المفقودة

تساعدنا الرؤية البصرية على فهم توزيع القيم المفقودة والعلاقة بينها. سنستخدم مكتبة Seaborn لإنشاء خريطة حرارية (heatmap) تصور الأماكن التي توجد بها القيم المفقودة.

ملاحظة تقنية: تصوير القيم المفقودة يمكن أن يكشف عن أنماط معينة؛ على سبيل المثال، قد تكون القيم مفقودة بشكل عشوائي (MCAR)، أو مرتبطة ببيانات أخرى (MAR)، أو مرتبطة بالقيمة المفقودة نفسها (MNAR).

import matplotlib.pyplot as plt
import seaborn as sns

# إعداد حجم الرسم البياني
plt.figure(figsize=(10, 6))

# إنشاء خريطة حرارية للقيم المفقودة
# اللون الفاتح يمثل قيمة موجودة، واللون الداكن (أو المحدد) يمثل قيمة مفقودة
sns.heatmap(df.isnull(), cbar=False, cmap='viridis')
plt.title('خريطة حرارية للقيم المفقودة')
plt.show()

# عرض الصفوف التي تحتوي على قيم مفقودة فقط
print("الصفوف التي تحتوي على قيم مفقودة:")
print(df[df.isnull().any(axis=1)])

الخطوة 3: استراتيجيات معالجة القيم المفقودة (الإزالة والإدخال)

هناك طريقتان رئيسيتان للتعامل مع القيم المفقودة: إما إزالتها (dropping) أو إدخال قيم بديلة (imputation). سنستعرض كلتا الطريقتين.

أ. إزالة الصفوف/الأعمدة ذات القيم المفقودة

يمكننا إزالة الصفوف التي تحتوي على أي قيمة مفقودة، أو الصفوف التي تحتوي على جميع القيم المفقودة، أو حتى الأعمدة.

# إنشاء نسخة من DataFrame للعمل عليها لتجنب تعديل الأصل
df_cleaned_drop = df.copy()

# إزالة الصفوف التي تحتوي على أي قيمة مفقودة
# 'any' يعني إزالة إذا كان هناك قيمة مفقودة واحدة على الأقل
# 'all' يعني إزالة فقط إذا كانت جميع القيم في الصف مفقودة
df_cleaned_drop_rows = df_cleaned_drop.dropna(how='any')
print("البيانات بعد إزالة الصفوف ذات القيم المفقودة:")
print(df_cleaned_drop_rows)
print("\n")

# إزالة الأعمدة التي تحتوي على أي قيمة مفقودة (نادراً ما تستخدم إلا إذا كانت نسبة القيم المفقودة عالية جداً)
# df_cleaned_drop_cols = df_cleaned_drop.dropna(axis=1, how='any')
# print("البيانات بعد إزالة الأعمدة ذات القيم المفقودة:")
# print(df_cleaned_drop_cols)
# print("\n")

ب. إدخال القيم المفقودة (Imputation)

تتضمن هذه الطريقة استبدال القيم المفقودة بقيم محسوبة مثل المتوسط (mean)، الوسيط (median)، الوضع (mode)، أو باستخدام نماذج أكثر تعقيداً. سنستخدم SimpleImputer من Scikit-learn و fillna من Pandas.

from sklearn.impute import SimpleImputer

# إنشاء نسخة من DataFrame للعمل عليها
df_cleaned_impute = df.copy()

# 1. الإدخال بالمتوسط (للقيم العددية فقط)
# نحدد الأعمدة العددية
numeric_cols = df_cleaned_impute.select_dtypes(include=np.number).columns
imputer_mean = SimpleImputer(strategy='mean')
df_cleaned_impute[numeric_cols] = imputer_mean.fit_transform(df_cleaned_impute[numeric_cols])
print("البيانات بعد إدخال القيم المفقودة بالمتوسط:")
print(df_cleaned_impute)
print("\n")

# إعادة إنشاء df_cleaned_impute للخطوة التالية (الوسيط)
df_cleaned_impute = df.copy()

# 2. الإدخال بالوسيط (للقيم العددية فقط)
imputer_median = SimpleImputer(strategy='median')
df_cleaned_impute[numeric_cols] = imputer_median.fit_transform(df_cleaned_impute[numeric_cols])
print("البيانات بعد إدخال القيم المفقودة بالوسيط:")
print(df_cleaned_impute)
print("\n")

# إعادة إنشاء df_cleaned_impute للخطوة التالية (الوضع)
df_cleaned_impute = df.copy()

# 3. الإدخال بالوضع (للقيم الفئوية والعددية)
# نحدد الأعمدة الفئوية
categorical_cols = df_cleaned_impute.select_dtypes(include='object').columns
imputer_mode_cat = SimpleImputer(strategy='most_frequent')
df_cleaned_impute[categorical_cols] = imputer_mode_cat.fit_transform(df_cleaned_impute[categorical_cols])

# يمكن استخدام الوضع للأعمدة العددية أيضاً إذا كانت البيانات غير متماثلة
imputer_mode_num = SimpleImputer(strategy='most_frequent')
df_cleaned_impute[numeric_cols] = imputer_mode_num.fit_transform(df_cleaned_impute[numeric_cols])

print("البيانات بعد إدخال القيم المفقودة بالوضع (القيم الأكثر تكراراً):")
print(df_cleaned_impute)
print("\n")

# مثال على استخدام fillna من Pandas
# df_fillna = df.copy()
# df_fillna['العمر'].fillna(df_fillna['العمر'].mean(), inplace=True)
# df_fillna['المدينة'].fillna('غير محدد', inplace=True)
# print("البيانات بعد الإدخال باستخدام fillna:")
# print(df_fillna)

الخطوة 4: معالجة القيم المفقودة في PySpark (مقدمة)

عند التعامل مع مجموعات البيانات الكبيرة في بيئات الحوسبة الموزعة، نحتاج إلى أدوات مثل Apache Spark. يوفر PySpark واجهات برمجة تطبيقات (API) لمعالجة القيم المفقودة بكفاءة.

ملاحظة تقنية: PySpark هو إطار عمل للحوسبة الموزعة، مثالي للبيانات الضخمة. وظائفه لمعالجة القيم المفقودة تشبه إلى حد كبير Pandas ولكنها تعمل على نطاق واسع.

# من المفترض أن يكون لديك بيئة PySpark مثبتة
from pyspark.sql import SparkSession
from pyspark.sql.functions import col

# تهيئة SparkSession
spark = SparkSession.builder.appName("MissingValuesPySpark").getOrCreate()

# تحويل DataFrame Pandas إلى Spark DataFrame
spark_df = spark.createDataFrame(df)

print("بيانات Spark DataFrame الأصلية:")
spark_df.show()

# اكتشاف القيم المفقودة (يمكن أن يكون أكثر تعقيداً في Spark)
# هنا مثال بسيط لعد القيم المفقودة لكل عمود
print("عدد القيم المفقودة في Spark DataFrame:")
for column in spark_df.columns:
    print(f"العمود '{column}': {spark_df.filter(col(column).isNull()).count()} قيم مفقودة")
print("\n")

# أ. إزالة الصفوف ذات القيم المفقودة في PySpark
spark_df_dropped = spark_df.na.drop()
print("Spark DataFrame بعد إزالة الصفوف ذات القيم المفقودة:")
spark_df_dropped.show()

# ب. إدخال القيم المفقودة في PySpark (باستخدام المتوسط للعمود 'العمر')
# حساب المتوسط
mean_age = spark_df.agg({"العمر": "avg"}).collect()[0][0]

# ملء القيم المفقودة
spark_df_filled = spark_df.na.fill(mean_age, subset=['العمر'])
spark_df_filled = spark_df_filled.na.fill('غير محدد', subset=['المدينة'])
print("Spark DataFrame بعد إدخال القيم المفقودة:")
spark_df_filled.show()

# إيقاف SparkSession
spark.stop()

الكود النهائي الكامل

هذا السكربت يجمع كل الخطوات المذكورة أعلاه في مكان واحد، مما يتيح لك تشغيلها بشكل متكامل.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.impute import SimpleImputer
from pyspark.sql import SparkSession
from pyspark.sql.functions import col

# 1. إعداد البيانات
data = {
    'الاسم': ['أحمد', 'ليلى', 'سارة', 'خالد', 'فاطمة', 'عمر', 'زينب'],
    'العمر': [25, 30, np.nan, 35, 28, np.nan, 22],
    'المدينة': ['الرياض', 'جدة', 'الدمام', np.nan, 'مكة', 'الرياض', 'جدة'],
    'الراتب': [5000, 7000, 6000, np.nan, 8000, 5500, 6500]
}
df = pd.DataFrame(data)

print("=== البيانات الأصلية ===")
print(df)
print("\n")

# 2. اكتشاف القيم المفقودة (Pandas)
print("=== اكتشاف القيم المفقودة ===")
print("عدد القيم المفقودة في كل عمود:\n", df.isnull().sum())
print("نسبة القيم المفقودة في كل عمود:\n", (df.isnull().sum() / len(df)) * 100)
print("\n")

# 3. تصوير القيم المفقودة (Pandas & Matplotlib/Seaborn)
plt.figure(figsize=(10, 6))
sns.heatmap(df.isnull(), cbar=False, cmap='viridis')
plt.title('خريطة حرارية للقيم المفقودة')
plt.show()
print("\n")

# 4. معالجة القيم المفقودة (Pandas & Scikit-learn)
# أ. الإزالة
df_dropped = df.dropna(how='any')
print("=== البيانات بعد إزالة الصفوف ذات القيم المفقودة ===")
print(df_dropped)
print("\n")

# ب. الإدخال بالمتوسط (للأعمدة العددية)
df_imputed_mean = df.copy()
numeric_cols = df_imputed_mean.select_dtypes(include=np.number).columns
imputer_mean = SimpleImputer(strategy='mean')
df_imputed_mean[numeric_cols] = imputer_mean.fit_transform(df_imputed_mean[numeric_cols])
print("=== البيانات بعد إدخال القيم المفقودة بالمتوسط ===")
print(df_imputed_mean)
print("\n")

# ج. الإدخال بالوضع (للأعمدة الفئوية والعددية)
df_imputed_mode = df.copy()
categorical_cols = df_imputed_mode.select_dtypes(include='object').columns
imputer_mode_cat = SimpleImputer(strategy='most_frequent')
df_imputed_mode[categorical_cols] = imputer_mode_cat.fit_transform(df_imputed_mode[categorical_cols])

# يمكن استخدام الوضع للأعمدة العددية أيضاً إذا كانت البيانات غير متماثلة
imputer_mode_num = SimpleImputer(strategy='most_frequent')
df_imputed_mode[numeric_cols] = imputer_mode_num.fit_transform(df_imputed_mode[numeric_cols])
print("=== البيانات بعد إدخال القيم المفقودة بالوضع ===")
print(df_imputed_mode)
print("\n")

# 5. معالجة القيم المفقودة في PySpark (مثال)
# تهيئة SparkSession
spark = SparkSession.builder.appName("MissingValuesPySpark").getOrCreate()
spark_df = spark.createDataFrame(df)

print("=== Spark DataFrame الأصلي ===")
spark_df.show()

# إزالة الصفوف ذات القيم المفقودة في PySpark
spark_df_dropped = spark_df.na.drop()
print("=== Spark DataFrame بعد الإزالة ===")
spark_df_dropped.show()

# إدخال القيم المفقودة في PySpark
mean_age_spark = spark_df.agg({"العمر": "avg"}).collect()[0][0]
spark_df_filled = spark_df.na.fill(mean_age_spark, subset=['العمر'])
spark_df_filled = spark_df_filled.na.fill('غير محدد', subset=['المدينة'])
print("=== Spark DataFrame بعد الإدخال ===")
spark_df_filled.show()

# إيقاف SparkSession
spark.stop()

النتيجة المتوقعة

عند تشغيل السكربت، ستشاهد المخرجات التالية على التوالي:

  1. عرض للبيانات الأصلية (Pandas DataFrame).
  2. جدول يوضح عدد ونسبة القيم المفقودة لكل عمود.
  3. شكل بياني (خريطة حرارية) يوضح توزيع القيم المفقودة بصرياً.
  4. البيانات بعد إزالة الصفوف التي تحتوي على أي قيم مفقودة.
  5. البيانات بعد إدخال القيم المفقودة في الأعمدة العددية باستخدام المتوسط.
  6. البيانات بعد إدخال القيم المفقودة في الأعمدة العددية والفئوية باستخدام الوضع (القيم الأكثر تكراراً).
  7. عرض لـ Spark DataFrame الأصلي.
  8. عرض لـ Spark DataFrame بعد إزالة الصفوف ذات القيم المفقودة.
  9. عرض لـ Spark DataFrame بعد إدخال القيم المفقودة (المتوسط للعمر، 'غير محدد' للمدينة).

ستلاحظ أن القيم NaN قد تم استبدالها بالقيم المحسوبة أو تم حذف الصفوف التي تحتوي عليها، مما يجعل مجموعة البيانات جاهزة لمزيد من التحليل أو بناء النماذج.