إنتقل إلى المحتوى الرئيسي

الأسبوع 8: تنظيف البيانات

🧹 مجموعات البيانات الحقيقية فوضوية. هذا الأسبوع عن جعل مجموعة بيانات جديرة بالثقة قبل تحليلها.

🎯 أهداف التعلّم

بنهاية هذا الأسبوع ستكون قادرًا على:

  • كشف ومعالجة القيم المفقودة بـ .isna()، .dropna()، و.fillna().
  • إيجاد وإزالة الصفوف المكررة.
  • تحويل نوع بيانات عمود صراحة بـ .astype() وpd.to_numeric().
  • استخدام دوال .str لتنظيف الأعمدة النصية.
  • تطبيق دالة مخصصة على عمود بـ .apply() عندما لا تناسب أي دالة مدمجة.

الدرس

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

تُمثّل pandas "لا قيمة" كـ NaN (ليس رقمًا) للأعمدة الرقمية. تُعيد .isna() قناعًا منطقيًا لأماكن القيم المفقودة — نفس فكرة القناع المنطقي من الأسبوع الماضي، مُطبَّقة على سؤال مختلف:

df.isna() # True/False لكل خلية
df.isna().sum() # عدّ القيم المفقودة لكل عمود

استراتيجيتان لمعالجتها:

df.dropna() # إزالة أي صف به قيمة مفقودة واحدة على الأقل
df.dropna(subset=["quiz1"]) # حذف الصفوف المفقودة quiz1 تحديدًا فقط
df.fillna(0) # استبدال كل NaN بـ 0
df["quiz1"].fillna(df["quiz1"].mean()) # استبدال بمتوسط العمود — افتراضي شائع

لا يوجد خيار "صحيح" عالميًا — حذف صف يفقد كل بيانات ذلك الطالب الأخرى تمامًا، بينما الملء بمتوسط يمكن أن يُشوّه الإحصاءات إن كانت هناك بيانات مفقودة كثيرة. أيهما مناسب يعتمد على لماذا البيانات مفقودة وكم منها، حكم ستمارسه طوال إطار عمل التحليل الاستكشافي في المسار الصعب.

الصفوف المكررة

تحتوي مجموعات البيانات الحقيقية أحيانًا نفس السجل أكثر من مرة — خطأ في إدخال البيانات، أو ملف دُمج مع نفسه بالخطأ:

df.duplicated() # True/False لكل صف — هل هذا تكرار مطابق لصف سابق؟
df.duplicated().sum() # كم صفًا مكررًا موجود
df = df.drop_duplicates() # الاحتفاظ فقط بأول ظهور من كل مجموعة تكرار

.duplicated(subset=["name"]) تُضيّق الفحص لأعمدة محددة — مفيد عندما لا ينبغي وجود صفين لنفس الطالب، حتى لو صادف اختلاف عمود آخر (مثل طابع زمني) بينهما.

تحويل أنواع البيانات

تستنتج pd.read_csv أحيانًا نوع بيانات خاطئ — عمود من الأرقام مُخزَّن كنص إن كان بصف واحد حتى حرف شاذ، مثلًا. تُحوّل .astype() صراحة:

df["quiz1"] = df["quiz1"].astype(int)
df["quiz1"] = pd.to_numeric(df["quiz1"], errors="coerce") # القيم غير الصالحة تصبح NaN بدلًا من التعطّل

pd.to_numeric(..., errors="coerce") عادة أكثر أمانًا من .astype(int) لبيانات العالم الحقيقي الفوضوية: بدلًا من رفع خطأ لحظة مواجهة شيء غير قابل للتحويل، تُحوّل بصمت تلك القيمة إلى NaN، والتي يمكنك بعدها معالجتها بالأدوات أعلاه.

تنظيف النصوص بـ .str

تحصل الأعمدة النصية على مُصِل خاص بها، .str، يكشف نسخًا مُوجَّهة من دوال النصوص المألوفة — نظير pandas لـ.lower()/.strip() في بايثون 101، مُطبَّقة على عمود كامل دفعة واحدة:

df["name"] = df["name"].str.strip() # إزالة المسافات البيضاء الطرفية
df["name"] = df["name"].str.lower() # أحرف صغيرة، نفس سبب tokenize() في بايثون 101
df["name"].str.contains("amina") # قناع منطقي: هل يحتوي كل اسم على "amina"؟
df["name"] = df["name"].str.replace("amina", "Amina") # بحث-واستبدال داخل كل قيمة
df["first_name"] = df["name"].str.split(" ").str[0] # قسّم كل اسم، احتفظ بالقطعة الأولى

تُنتج .str.split(" ") Series حيث كل قيمة هي نفسها قائمة من القطع؛ تصل .str[0] الثانية بعدها إلى كل من تلك القوائم وتسحب العنصر الأول — تسلسل عمليات .str هكذا نمط شائع بمجرد أن لا تكفي دالة نصية واحدة تمامًا.

عندما لا تناسب أي دالة مدمجة: .apply()

لتحويل ليس له دالة .str/رقمية جاهزة، تُشغّل .apply() أي دالة من اختيارك على كل قيمة في عمود:

def grade_letter(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
else:
return "F"

df["quiz1_grade"] = df["quiz1"].apply(grade_letter)

هذه فعليًا فقط دوال بايثون 101 مجددًا — grade_letter دالة عادية، و.apply() هي ما يُشغّلها مرة واحدة لكل قيمة، المكافئ ذو الشعور المُوجَّه للتكرار على العمود بنفسك واستدعاء grade_letter على كل قيمة يدويًا.

⚠️ أخطاء شائعة

  • استدعاء .fillna()/.dropna() دون إعادة الإسناد. مثل معظم عمليات pandas، تُعيد هذه Series/DataFrame جديدًا افتراضيًا — df.fillna(0) وحدها لا تُغيّر df؛ تحتاج df = df.fillna(0) (أو inplace=True).
  • ملء القيم المفقودة قبل فهم سبب فقدانها. df.fillna(0) شاملة على عمود درجات يمكن أن تُحوّل بصمت "كان هذا الطالب غائبًا" إلى "حصل هذا الطالب على صفر"، وهما حقيقتان مختلفتان جدًا.
  • نسيان أن التكرار يمكن أن يكون جزئيًا، لا مطابقًا فقط. .duplicated() بلا subset تُعلّم فقط الصفوف المطابقة لكل عمود تمامًا — صفان لنفس الطالب بخطأ إملائي في عمود واحد لن يُلتقطا دون تضييق الفحص.
  • استخدام .apply() عندما توجد دالة موجَّهة بالفعل. تُشغّل .apply() دالة بايثون خاصتك مرة لكل صف، وهو أبطأ من دالة موجَّهة مدمجة مثل .str.lower() تقوم بنفس العمل بكود مُحسَّن — استخدم .apply() فقط عندما لا يناسب شيء مدمج.

🧩 تحديات

حمّل students-normal.csv واطبع كم قيمة مفقودة يحتوي كل عمود.

اجعل قيمة quiz1 واحدة مفقودة يدويًا (مثلًا df.loc[0, "quiz1"] = None)، ثم قارن عدد الصفوف الناتج بعد .dropna(subset=["quiz1"]) مقابل بعد .fillna(df["quiz1"].mean()).

حوّل كل قيمة في عمود name لأحرف صغيرة باستخدام .str، دون كتابة حلقة يدوية.

باستخدام .str.contains(...)، جِد كم طالبًا لديه حرف "a" في أي مكان باسمه.

كرّر صفًا واحدًا من DataFrame يدويًا (أضف نسخة من صف موجود)، تأكد أن .duplicated() تلتقطه، ثم أزله بـ.drop_duplicates().

اكتب دالتك الخاصة التي تأخذ درجة وتُعيد "Pass" أو "Fail" بناءً على عتبة 60%، ثم استخدم .apply() لإضافة عمود quiz1_result جديد مبني منها.

🤔 أسئلة سقراطية

  • تُعطي .dropna() و.fillna(mean) عدد صفوف نهائي مختلف وإحصاءات نهائية مختلفة. لمجموعة بيانات حيث 40% من قيم عمود واحد مفقودة، أي خيار تعتقد أنه أكثر احتمالًا لتضليل شخص يقرأ إحصاءاتك الملخّصة، ولماذا؟
  • تُحوّل pd.to_numeric(col, errors="coerce") بصمت القيم السيئة إلى NaN بدلًا من التعطّل. هل "بصمت" فعليًا شيء جيد هنا؟ ماذا قد يحدث خطأً لو شغّلت هذا على عمود ولم تفحص .isna().sum() أبدًا لاحقًا؟
  • لماذا تحتاج الأعمدة النصية مُصِل .str منفصل (df["name"].str.lower()) بدلًا من فقط df["name"].lower()؟ ماذا سيعني .lower() حتى لو استُدعيت مباشرة على Series كاملة بدلًا من نص واحد؟
  • .duplicated() بلا وسيطة subset تلتقط فقط الصفوف المطابقة لكل عمود. هل يمكنك التفكير في سيناريو واقعي في مجموعة البيانات هذه حيث يمثّل صفان نفس الطالب الفعلي لكن لا يُلتقطان بفحص تكرار مطابق تمامًا؟
  • يمكن لـ.apply() تشغيل أي دالة بايثون، شاملة تلك ذات التأثيرات الجانبية (مثل الطباعة، أو الكتابة لملف) — لا فقط تلك التي تُعيد قيمة مُحوَّلة. لماذا قد يكون تشغيل كود بتأثيرات جانبية داخل .apply() فكرة سيئة، بمعطى أنك لا تتحكم بالضبط كم مرة أو بأي ترتيب قد تستدعي pandas دالتك داخليًا؟

✅ اختبار الأسبوع

✅ اختبار الأسبوع

1. أي دالة تعدّ القيم المفقودة لكل عمود؟
2. ماذا تفعل pd.to_numeric(col, errors="coerce") بقيمة لا تستطيع تحويلها؟
3. لماذا نستخدم df["col"].str.lower() بدلًا من df["col"].lower()؟
4. تُزيل df.dropna(subset=["quiz1"]) الصفوف حيث:
5. .apply() على Series أفضل استخدام لها عندما: