الوحدة 3 · تنظيف البيانات الحقيقية
الصفوف المكرّرة ودالة تنظيف قابلة لإعادة الاستخدام
18 دقراءة 4 دتمرين Python
الدرس في لمحة
اعثر على الصفوف المكرّرة واحذفها بـ duplicated و drop_duplicates، ثم اجمع كل خطوات التنظيف في هذا القسم في دالة واحدة مع assertions تستطيع إعادة تشغيلها على ملف الشهر القادم.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
تسعة صفوف في الملف الفوضوي تظهر مرتين. كل نسخة مكرّرة طلب حقيقي محسوب مرة أخرى، فكل مجموع حسبته من الملف المُصدَّر حتى الآن أعلى من حقيقته: 23,500.67 دولارًا بدلًا من 22,486.69، أي مبالغة بنسبة 4.5%. تأتي المكرّرات من ملفات أُعيد تصديرها، وصفوف نُسخت ولُصقت، وأنظمة تعيد محاولة رفع ملف بعد فشله. إنها آخر مشكلة في الملف، وإصلاحها يتيح لك تحويل هذا القسم كله إلى دالة واحدة.
العثور على المكرّرات
import pandas as pd
messy = pd.read_csv("orders_messy.csv")
dupes = messy.duplicated()
print(dupes.sum(), "duplicate rows")
both_copies = messy[messy.duplicated(keep=False)].sort_values("order_id")
print(both_copies.head(4).to_string())تعيد duplicated() قناعًا قيمته True لكل صف مطابق لصف سابق. لا توضع علامة على الظهور الأول (keep="first")، وهذا ما تريده عند الحذف. أما للفحص، فتضع keep=False علامة على كل النسخ، والترتيب بحسب المعرّف يضع كل زوج معًا. انظر إليها قبل حذف أي شيء: الصفان 22 و113 هما الطلب نفسه، والمنتج نفسه، والمجموع نفسه، ويفصل بينهما نحو مئة صف. هذا النمط، صفوف متطابقة متباعدة، نموذجي لملف مُصدَّر أُضيف إلى نفسه.
حذفها
import pandas as pd
messy = pd.read_csv("orders_messy.csv")
deduped = messy.drop_duplicates()
print(len(messy), "->", len(deduped))
print(deduped["order_id"].is_unique)بعد حذف المكرّرات المطابقة تمامًا، يظهر كل order_id مرة واحدة، وهذا يؤكّد أن المكرّرات كانت نسخًا كاملة لا نسختين مختلفتين من الطلب نفسه. لو كانت is_unique تساوي False، لكان لديك صفوف متعارضة: المعرّف نفسه مع مجموعين مختلفين مثلًا. وتلك تحتاج قرارًا، مثل الاحتفاظ بأحدث نسخة، وتطبّقه drop_duplicates(subset=["order_id"], keep="last") بعد أن تتّخذ ذلك القرار.
نظّف أولًا، ثم أزِل المكرّرات
اكتشاف المكرّرات يقارن القيم مقارنة دقيقة. "Ali Evans" و"Ali Evans " نصان مختلفان، و$22.00 و22.00 مجموعان مختلفان. في هذا الملف المُصدَّر تصادف أن النسخ متطابقة حرفًا بحرف، لكن بشكل عام تحذف المسافات، وتوحّد حالة الأحرف، وتحوّل الأرقام قبل البحث عن المكرّرات، وإلا نجت النسخ شبه المتطابقة.
دالة واحدة للقسم كله
لديك الآن خمس خطوات تنظيف، مكتوبة عبر خمسة دروس. وحين تكون متفرّقة في notebook تكون هشّة: شغّل خلية مرتين أو تخطَّ واحدة، فتتغيّر النتيجة. ضعها في دالة واحدة، بالترتيب الصحيح، مع الفحوص في النهاية:
import pandas as pd
COUNTRIES = {"Egypt", "United Arab Emirates", "Saudi Arabia", "Jordan",
"United Kingdom", "Germany", "United States", "Canada"}
def parse_money(values):
cleaned = (values.astype("string").str.strip()
.str.replace("$", "", regex=False).str.replace(",", "", regex=False))
return pd.to_numeric(cleaned, errors="coerce")
def parse_dates(raw):
iso = pd.to_datetime(raw, format="%Y-%m-%d", errors="coerce")
dmy = pd.to_datetime(raw, format="%d/%m/%Y", errors="coerce")
mdy = pd.to_datetime(raw, format="%m/%d/%Y", errors="coerce")
ambiguous = dmy.notna() & mdy.notna() & (dmy != mdy)
return iso.fillna(dmy).fillna(mdy), ambiguous
def clean_orders(path):
"""Load the messy export and return a clean, deduplicated DataFrame."""
df = pd.read_csv(path)
df["customer"] = df["customer"].str.strip()
df["country"] = df["country"].str.strip().str.title()
df["channel"] = df["channel"].str.strip().str.replace(" ", "_").fillna("unknown")
df["quantity"] = df["quantity"].astype("Int64")
df["total"] = parse_money(df["total"])
df["order_date"], df["date_is_ambiguous"] = parse_dates(df["order_date"])
df = df.drop_duplicates().reset_index(drop=True)
assert df["order_id"].is_unique, "duplicate order ids remain"
assert df["order_date"].notna().all(), "unparsed dates"
assert df["total"].notna().all() and (df["total"] > 0).all(), "bad totals"
assert set(df["country"]) <= COUNTRIES, f"unknown countries: {set(df['country']) - COUNTRIES}"
return df
clean = clean_orders("orders_messy.csv")
print(clean.shape)
print(clean.dtypes)
print(round(clean["total"].sum(), 2), "total sales,", clean["date_is_ambiguous"].sum(), "ambiguous dates")السطر df["order_date"], df["date_is_ambiguous"] = parse_dates(...) يفكّ القيمتين اللتين تعيدهما الدالة المساعدة إلى عمودين دفعة واحدة. وتعيد reset_index(drop=True) ترقيم الصفوف من 0 إلى 158 بعد الحذف، فتتّفق المواقع والتسميات من جديد. لاحظ الأنواع أيضًا: quantity من النوع Int64 وtotal من النوع Float64، وكلاهما قابل للقيم المفقودة، لأن parse_money تحوّل عبر النوع string في pandas وto_numeric تحافظ على العائلة القابلة للقيم المفقودة. وهما يتصرّفان كأرقام عادية في كل حساب ستجريه.
تقرأ الدالة ملفها بنفسها، فلا تعتمد أبدًا على ما تصادف أنك شغّلته سابقًا في notebook. المُدخل نفسه، المُخرج نفسه، في كل مرة. وحين يصل ملف الشهر القادم، تشغّل clean_orders("orders_messy_2026_01.csv") القسم كله في سطر واحد، وإذا احتوى الملف الجديد على كتابة تاسعة لدولة أو مجموع باليورو، يوقفه assertion برسالة تسمّي المشكلة.
أين تضع الدالة؟ أثناء الاستكشاف، في أول خلية من الـ notebook. وحين يحتاجها notebook ثانٍ أو زميل، انقلها إلى ملفها الخاص، مثل cleaning.py بجانب ملفات الـ notebook، واكتب from cleaning import clean_orders حيثما احتجتها. تعريف واحد مستورَد في كل مكان يعني أن الإصلاح الذي تجريه مرة يصل إلى كل تحليل يعتمد عليه.
أصبح الملف المُصدَّر نظيفًا: 159 طلبًا، و22,486.69 دولارًا من المبيعات، و25 تاريخًا عليها علامة الالتباس (كانت 30 قبل حذف المكرّرات). ينتقل القسم 4 من التنظيف إلى الإجابة، بدءًا بأكثر أدوات pandas استخدامًا: التجميع (groupby).
أهم النقاط
- تضع
df.duplicated()علامة على التكرارات المطابقة تمامًا بعد ظهورها الأول؛ وkeep=Falseتضع علامة على كل النسخ لتفحصها جنبًا إلى جنب. - تزيل
drop_duplicates(subset=[...])المكرّرات بحسب أعمدة مفتاحية، لكن استخدم فقط مفتاحًا فريدًا فعلًا لكل صف، ولا تستخدم أبدًاorder_idفي جدول بنود الطلبات. - نظّف النصوص قبل البحث عن المكرّرات، لأن
"Ali Evans"و"Ali Evans "نصان مختلفان. - دالة التنظيف تحوّل جلسة notebook لمرة واحدة إلى خطوة قابلة للتكرار - المُدخل نفسه، المُخرج نفسه، كل شهر.
- اختم الدالة بجمل
assertتعبّر عن معنى “نظيف”، كي توقف البيانات السيئة خط المعالجة بدلًا من أن تصل إلى تقرير.