تنظيف بيانات الاستبيانات في Pandas: معالجة الإجابات المفقودة والمكررة والمتناقضة
مقدمة
تُعدّ تنظيف بيانات الاستبيانات في Pandas: معالجة الإجابات المفقودة والمكررة والمتناقضة خطوة حاسمة قبل بناء أي تقرير إحصائي أو نموذج تنبؤي أو لوحة معلومات. فالاستبيانات الواقعية نادراً ما تصل في صورة مثالية؛ إذ قد يترك المشاركون أسئلة بلا إجابة، أو يرسلون النموذج أكثر من مرة، أو يكتبون الإجابة ذاتها بصيغ مختلفة، أو يقدمون إجابات متعارضة منطقياً. وإذا انتقلت هذه المشكلات مباشرة إلى مرحلة التحليل، فقد تقود إلى نسب مضللة واستنتاجات غير دقيقة.
توفّر مكتبة Pandas أدوات عملية ومرنة لتنظيف البيانات بصورة قابلة للتكرار والتدقيق. بدلاً من تعديل ملف الجداول يدوياً كل مرة، يمكن كتابة سلسلة خطوات واضحة تقرأ البيانات، وتفحص جودتها، وتعالج عيوبها وفق قواعد معلنة. يناقش هذا المقال منهجية متكاملة لتنظيف بيانات الاستبيانات، مع أمثلة برمجية تشمل القيم المفقودة والسجلات المكررة والإجابات غير المتسقة والتناقضات المنطقية.
فهم بنية الاستبيان وفحص البيانات قبل التعديل
يبدأ التنظيف الجيد بفهم معنى كل عمود، وليس بتطبيق أوامر الحذف مباشرة. ينبغي معرفة نوع السؤال: هل هو اختياري أم إلزامي؟ هل يقبل إجابة رقمية أم نصية؟ هل توجد قائمة قيم مسموحة؟ وهل يرتبط بسؤال آخر؟ على سبيل المثال، قد يكون السؤال عن عدد سنوات الخبرة مناسباً فقط لمن أجاب بأنه يعمل حالياً، بينما سؤال المدينة قد يكون مطلوباً من جميع المشاركين.
بعد تحميل الملف، يُستحسن إجراء فحص أولي لأسماء الأعمدة وأنواعها وعدد القيم الفريدة ومدى انتشار الفراغات. هذه الخطوة تكشف أخطاء شائعة مثل قراءة العمر كنص، أو وجود مسافات زائدة في الإجابات، أو ظهور رموز مثل لا يوجد و- بدلاً من قيمة مفقودة حقيقية.
import pandas as pd
df = pd.read_csv("survey_responses.csv")
print(df.head())
print(df.info())
print(df.describe(include="all"))
print(df.isna().sum().sort_values(ascending=False))
print(df["الحالة_الوظيفية"].value_counts(dropna=False))
لا يكفي الاعتماد على info() وحدها، لأنها تعرض القيم الفارغة التي تعرفها Pandas فقط، مثل NaN وNone. أما النصوص مثل غير متاح أو NA أو المسافة الفارغة فقد تعامل كإجابات عادية. لذلك من المفيد فحص القيم النصية الأكثر تكراراً في الأعمدة المهمة، ومراجعة عدد محدود من الصفوف يدوياً للتأكد من أن قراءة الملف تمت بالشكل المتوقع.
كما يُنصح بإنشاء قاموس بيانات يصف كل متغير: اسمه، ونوعه، وقيمه المقبولة، وما إذا كان إلزامياً، وقواعد ارتباطه بالأسئلة الأخرى. هذا القاموس يجعل قرارات التنظيف قابلة للتفسير، ويمنع حذف بيانات قد تكون ذات دلالة حقيقية.
توحيد تمثيل الإجابات قبل معالجة القيم المفقودة
في ملفات الاستبيانات، لا تظهر القيمة المفقودة دائماً في شكل موحد. فقد يكتب أحد المشاركين «لا أعلم»، ويترك آخر الخانة فارغة، بينما تستخدم المنصة رمزاً مثل N/A. قبل حساب النسب أو ملء الفراغات، يجب تحويل هذه التمثيلات إلى NA موحد، لأن وجودها كنصوص سيشوّه الإحصاءات.
ينبغي كذلك تنظيف المسافات وتوحيد حالة الأحرف في الأعمدة النصية عند الحاجة. لكن يجب التعامل بحذر مع النصوص المفتوحة، مثل خانة الملاحظات، لأن تحويلها إلى أحرف صغيرة أو إزالة رموز منها قد يغيّر معناها. الأفضل قصر التوحيد القوي على الإجابات المغلقة ذات الخيارات المعروفة.
missing_tokens = [
"", " ", "-", "--", "NA", "N/A",
"غير متاح", "لا أعلم", "لم أجب"
]
df = df.replace(missing_tokens, pd.NA)
text_columns = ["الجنس", "الحالة_الوظيفية", "مستوى_التعليم", "المدينة"]
for col in text_columns:
df[col] = df[col].astype("string").str.strip()
بعد التوحيد، يمكن قياس نسبة النقص في كل عمود وفي كل سجل. ارتفاع نسبة القيم المفقودة في عمود معين قد يشير إلى أن صياغة السؤال غير واضحة أو أنه اختياري أو لم يُعرض لبعض الفئات. أما السجل الذي يفتقد معظم الإجابات فقد يكون غير صالح للتحليل، خصوصاً إن كان الاستبيان يفترض اكتمال الحقول الأساسية.
missing_rate_by_column = df.isna().mean().mul(100)
missing_rate_by_row = df.isna().mean(axis=1).mul(100)
print(missing_rate_by_column.sort_values(ascending=False))
df["نسبة_النقص_في_السجل"] = missing_rate_by_row
يساعد هذا التحليل أيضاً في التمييز بين «مفقود عشوائياً» و«مفقود له معنى». فعدم الإفصاح عن الدخل مثلاً قد يرتبط بمستوى الدخل نفسه، ولذلك فإن استبداله بمتوسط القيم دون دراسة قد يخلق انحيازاً في النتائج.
معالجة الإجابات المفقودة بقرارات مدروسة
لا توجد قاعدة واحدة تصلح لجميع القيم المفقودة. الخيار الأول هو الحذف باستخدام dropna()، وهو مناسب عندما تكون الحقول الناقصة أساسية للتحليل أو عندما تكون نسبة النقص صغيرة. لكن الحذف الكامل لكل صف يحتوي على فراغ قد يؤدي إلى خسارة كبيرة في حجم العينة، خاصة في الاستبيانات الطويلة.
# حذف السجلات التي تفتقد المعرّف أو السؤال المحوري
df = df.dropna(subset=["معرف_المشارك", "العمر"])
# الاحتفاظ فقط بالسجلات التي تحتوي على 70% من القيم على الأقل
minimum_answers = int(df.shape[1] * 0.7)
df = df.dropna(thresh=minimum_answers)
الخيار الثاني هو الملء باستخدام fillna(). يصلح ذلك عندما يكون للفراغ معنى تحليلي واضح، مثل تحويل عدم ذكر المدينة إلى فئة «غير محدد»، أو عندما يكون الهدف الحفاظ على السجلات لأغراض التجميع. وفي المتغيرات الرقمية يمكن استخدام الوسيط غالباً بدلاً من المتوسط، لأن الوسيط أقل تأثراً بالقيم المتطرفة.
df["المدينة"] = df["المدينة"].fillna("غير محدد")
df["العمر"] = df["العمر"].fillna(df["العمر"].median())
df["الحالة_الوظيفية"] = df["الحالة_الوظيفية"].fillna("لم يفصح")
مع ذلك، ينبغي الفصل بين القيمة الأصلية والقيمة التي جرى تعويضها. من الممارسات الجيدة إنشاء عمود مؤشر يوضح أن الإجابة كانت مفقودة في المصدر. يتيح ذلك للباحث اختبار ما إذا كان النقص نفسه يرتبط بنتائج الاستبيان، كما يحافظ على الشفافية عند مشاركة البيانات المنظفة.
df["العمر_كان_مفقوداً"] = df["العمر"].isna()
df["العمر"] = df["العمر"].fillna(df["العمر"].median())
اكتشاف السجلات المكررة وإزالتها دون فقدان معلومات مهمة
قد تظهر التكرارات بسبب إرسال المشارك الاستبيان مرتين، أو دمج ملفات متعددة، أو وجود خطأ في نظام التصدير. لكن ليس كل تشابه بين صفين يعني أن أحدهما مكرر؛ فقد يتشارك مشاركان مختلفان العمر والمدينة والإجابات نفسها. لذلك يجب أولاً تحديد مفتاح منطقي للتمييز، مثل رقم المشارك أو البريد الإلكتروني بعد إخفائه، أو مزيج من معرّف الجلسة وتاريخ الإرسال.
يكشف الأمر duplicated() عن السجلات المكررة، بينما يزيل drop_duplicates() التكرار وفق الأعمدة المختارة. ومن الأفضل فحص التكرارات قبل حذفها، ثم توثيق عددها وسبب اتخاذ القرار.
# عرض جميع السجلات المتكررة بحسب معرّف المشارك
duplicates = df[df.duplicated(subset=["معرف_المشارك"], keep=False)]
print(duplicates.sort_values("معرف_المشارك"))
# الاحتفاظ بأحدث إجابة عند تكرار المشارك
df["تاريخ_الإرسال"] = pd.to_datetime(df["تاريخ_الإرسال"], errors="coerce")
df = df.sort_values("تاريخ_الإرسال")
df = df.drop_duplicates(subset=["معرف_المشارك"], keep="last")
إذا لم يتوفر معرّف موثوق، يمكن البحث عن التكرار الكامل في جميع الأعمدة أو في مجموعة أعمدة تمثل هوية الاستجابة. لكن يجب تجنب الاعتماد على الاسم وحده، لأنه قد يُكتب بتهجئات مختلفة أو يتكرر بين أشخاص متعددين. وفي الاستبيانات المجهولة، قد تكون أفضل سياسة هي إزالة الصفوف المتطابقة تماماً فقط، ووضع الحالات المشبوهة في قائمة للمراجعة بدلاً من حذفها آلياً.
توحيد الإجابات المتناقضة شكلياً والقيم غير الصالحة
تظهر عدم الاتساقات الشكلية عندما تكون الإجابة واحدة في المعنى لكنها مكتوبة بصيغ متعددة، مثل «نعم»، و«نعم»، و«نعم!»، أو «دوام كامل» و«دوام كامل ». تؤدي هذه الصيغ إلى تقسيم الفئة الواحدة عند استخدام value_counts() أو groupby(). والحل هو بناء قاموس تطبيع واضح يربط كل صيغة بالقيمة المعيارية المعتمدة.
employment_map = {
"دوام كامل": "موظف بدوام كامل",
"موظف كامل": "موظف بدوام كامل",
"full time": "موظف بدوام كامل",
"دوام جزئي": "موظف بدوام جزئي",
"طالب": "طالب",
"لا أعمل": "غير موظف"
}
df["الحالة_الوظيفية"] = (
df["الحالة_الوظيفية"]
.str.strip()
.str.lower()
.replace(employment_map)
)
تتطلب الأسئلة الرقمية فحصاً مختلفاً. فقد يظهر عمر بقيمة سالبة، أو عمر يفوق 120 سنة، أو تقييم رضى خارج المجال من 1 إلى 5. لا ينبغي دائماً حذف هذه القيم فوراً؛ إذ قد تكون نتيجة خطأ في التنسيق، مثل كتابة «25 سنة» بدلاً من «25». يمكن تحويل النص إلى رقم باستخدام to_numeric() مع جعل القيم غير القابلة للتحويل مفقودة، ثم تطبيق حدود منطقية مرتبطة بتصميم الاستبيان.
df["العمر"] = pd.to_numeric(df["العمر"], errors="coerce")
df["تقييم_الرضا"] = pd.to_numeric(df["تقييم_الرضا"], errors="coerce")
df.loc[~df["العمر"].between(15, 100), "العمر"] = pd.NA
df.loc[~df["تقييم_الرضا"].between(1, 5), "تقييم_الرضا"] = pd.NA
معالجة التناقضات المنطقية بين أسئلة الاستبيان
التناقض المنطقي يختلف عن اختلاف الصياغة. فقد يجيب المشارك بأنه غير موظف، ثم يذكر دخلاً شهرياً أو سنوات خبرة وظيفية؛ أو يصرح بعدم امتلاك سيارة ثم يحدد نوعها. هذه الحالات ليست أخطاء تنسيق، بل تحتاج إلى قواعد تربط الأعمدة ببعضها. ويجب أن تستند القواعد إلى منطق الاستبيان وأهدافه، لا إلى افتراضات عامة فقط.
في البداية، من الأفضل إنشاء أعلام للمراجعة بدلاً من تعديل الإجابات بصمت. فربما كان السؤال عن الدخل يشمل مصادر غير الوظيفة، أو كان المشارك قد فهم السؤال بطريقة مختلفة. بعد ذلك يمكن الرجوع إلى منطق الاستبيان، أو التواصل مع المصدر إذا كان ذلك ممكناً، أو اختيار سياسة معلنة لمعالجة الحالة.
df["تناقض_العمل_والخبرة"] = (
(df["الحالة_الوظيفية"] == "غير موظف") &
(df["سنوات_الخبرة"].fillna(0) > 0)
)
df["تناقض_امتلاك_السيارة"] = (
(df["يمتلك_سيارة"] == "لا") &
(df["نوع_السيارة"].notna())
)
inconsistent_rows = df[
df["تناقض_العمل_والخبرة"] |
df["تناقض_امتلاك_السيارة"]
]
قد تتمثل المعالجة في تحويل الإجابة التابعة إلى قيمة مفقودة عندما لا تنطبق على إجابة السؤال الرئيسي. مثال ذلك: إذا أجاب شخص بأنه لا يمتلك سيارة، يمكن جعل «نوع السيارة» مفقوداً، لأن هذا السؤال لا ينطبق عليه. أما إذا كان التناقض يمس متغيراً أساسياً، فالأفضل غالباً استبعاد السجل من تحليل محدد لا من مجموعة البيانات كلها.
mask = df["يمتلك_سيارة"] == "لا"
df.loc[mask, "نوع_السيارة"] = pd.NA
بناء خط تنظيف قابل للتكرار والتحقق من الجودة
تنظيف البيانات ليس عملية منفصلة تنتهي بمجرد تشغيل بعض الأوامر، بل هو خط عمل ينبغي أن يكون قابلاً للتكرار. من الأفضل الاحتفاظ بالبيانات الخام دون تعديل، ثم إنشاء نسخة منظفة وحفظ قواعد التحويل في ملف برمجي أو دفتر تحليلي. بهذه الطريقة يمكن تشغيل العملية نفسها عند وصول دفعة جديدة من الردود، ومراجعة أثر كل قرار بسهولة.
ينبغي أيضاً قياس أثر التنظيف: كم سجلاً حُذف؟ كم قيمة حُوّلت إلى مفقودة؟ كم تكراراً أزيل؟ وما توزيع الفئات قبل التنظيف وبعده؟ إذا تغيّر توزيع إجابة مهمة بشكل كبير، فيجب مراجعة القواعد للتأكد من عدم انحيازها إلى فئة بعينها.
report = {
"عدد_السجلات_النهائي": len(df),
"القيم_المفقودة_النهائية": int(df.isna().sum().sum()),
"التكرارات_المتبقية": int(df.duplicated().sum()),
"السجلات_ذات_التناقض": int(
(df["تناقض_العمل_والخبرة"] |
df["تناقض_امتلاك_السيارة"]).sum()
)
}
print(report)
df.to_csv("survey_responses_clean.csv", index=False, encoding="utf-8-sig")
ومن المفيد إضافة اختبارات تحقق بسيطة إلى الخط، مثل التأكد من عدم وجود معرف مشارك مكرر، ومن أن الأعمار تقع ضمن الحدود المحددة، ومن أن جميع الإجابات المغلقة تنتمي إلى القائمة المعتمدة. هذا النهج يحوّل التنظيف من مجموعة تعديلات يدوية هشة إلى عملية موثوقة يمكن مراجعتها وتطويرها.
خاتمة
تتطلب بيانات الاستبيانات معالجة واعية للسياق، لأن القيمة الفارغة أو السجل المتكرر أو الإجابة المتناقضة قد يحمل معنى مختلفاً حسب تصميم السؤال. تتيح Pandas أدوات فعالة مثل replace() وfillna() وdropna() وdrop_duplicates() والتصفية الشرطية لبناء عملية تنظيف دقيقة. لكن جودة النتيجة لا تعتمد على الأوامر وحدها، بل على تعريف قواعد واضحة، وتوثيقها، وفحص أثرها على العينة. وعندما يُنفّذ التنظيف بصورة قابلة للتكرار، تصبح التحليلات اللاحقة أكثر موثوقية وعدالة وقابلية للدفاع العلمي.
تعليقات
إرسال تعليق