EduFlow
Python لتحليل البيانات
⁦0%⁩

الوحدة 3 · تنظيف البيانات الحقيقية

الصفوف المكرّرة ودالة تنظيف قابلة لإعادة الاستخدام

18 دقراءة 4 دتمرين Python

الدرس في لمحة

اعثر على الصفوف المكرّرة واحذفها بـ duplicated و drop_duplicates، ثم اجمع كل خطوات التنظيف في هذا القسم في دالة واحدة مع assertions تستطيع إعادة تشغيلها على ملف الشهر القادم.

  1. 1العثور على المكرّرات
  2. 2حذفها
  3. 3دالة واحدة للقسم كله

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

تسعة صفوف في الملف الفوضوي تظهر مرتين. كل نسخة مكرّرة طلب حقيقي محسوب مرة أخرى، فكل مجموع حسبته من الملف المُصدَّر حتى الآن أعلى من حقيقته: 23,500.67 دولارًا بدلًا من 22,486.69، أي مبالغة بنسبة 4.5%. تأتي المكرّرات من ملفات أُعيد تصديرها، وصفوف نُسخت ولُصقت، وأنظمة تعيد محاولة رفع ملف بعد فشله. إنها آخر مشكلة في الملف، وإصلاحها يتيح لك تحويل هذا القسم كله إلى دالة واحدة.

العثور على المكرّرات

Python
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 هما الطلب نفسه، والمنتج نفسه، والمجموع نفسه، ويفصل بينهما نحو مئة صف. هذا النمط، صفوف متطابقة متباعدة، نموذجي لملف مُصدَّر أُضيف إلى نفسه.

حذفها

Python
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 تكون هشّة: شغّل خلية مرتين أو تخطَّ واحدة، فتتغيّر النتيجة. ضعها في دالة واحدة، بالترتيب الصحيح، مع الفحوص في النهاية:

Python
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 تحافظ على العائلة القابلة للقيم المفقودة. وهما يتصرّفان كأرقام عادية في كل حساب ستجريه.

ملف مُصدَّر خام من 168 صفًا يمرّ عبر خمس خطوات: حذف المسافات وتوحيد النصوص، وتحويل نوع الكمية إلى Int64، وتحليل المبالغ، وتحليل التواريخ ووضع علامة على الملتبس، وحذف المكرّرات. ثم تفحص الـ assertions النتيجة، فيخرج 159 صفًا نظيفًا. خام 168 صفًا النصوص Int64 المبالغ التواريخ المكرّرات نظيف 159 صفًا الـ assertions تحرس المخرج clean_orders(path)
خط التنظيف: 168 صفًا فوضويًا تدخل، و159 صفًا نظيفًا تخرج

تقرأ الدالة ملفها بنفسها، فلا تعتمد أبدًا على ما تصادف أنك شغّلته سابقًا في 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 تعبّر عن معنى “نظيف”، كي توقف البيانات السيئة خط المعالجة بدلًا من أن تصل إلى تقرير.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

في جدول صفان متطابقان للطلب 1026. ماذا تعدّ df.duplicated().sum() لهما؟

السؤال 2 من 3

لماذا تكون items.drop_duplicates(subset=["order_id"]) خطيرة على order_items؟

السؤال 3 من 3

ما الفائدة الأساسية من ختم دالة التنظيف بـ assert df["order_id"].is_unique؟

تدريب

تمرين Python

اكتب clean_orders()

أكمل clean_orders(path) بحيث تحمّل الملف المُصدَّر وتعيده منظّفًا: احذف المسافات من customer؛ واحذف المسافات من country وحوّله إلى حالة العنوان؛ وحوّل mobile app إلى mobile_app والقنوات المفقودة إلى "unknown"؛ وحوّل total بالدالة المعطاة parse_money؛ وحلّل order_date بـ pd.to_datetime(..., format="mixed", dayfirst=True)؛ واحذف المكرّرات المطابقة تمامًا وأعِد ترقيم الفهرس من 0؛ واستخدم assert للتأكّد من أن order_id فريد قبل الإعادة. ثم اجعل sales_by_country قاموسًا يربط كل دولة منظّفة بإجمالي مبيعاتها، مقرّبًا إلى منزلتين عشريتين.

ملفات البيانات: orders_messy.csv

import pandas as pd

def parse_money(values):
    cleaned = (values.astype("string").str.strip()
               .str.replace("
quot;, "", regex=False).str.replace(",", "", regex=False)) return pd.to_numeric(cleaned, errors="coerce") def clean_orders(path): """Return the messy export cleaned and deduplicated.""" df = pd.read_csv(path) # strip customer; strip + title-case country # channel: "mobile app" -> "mobile_app", missing -> "unknown" # total: parse_money # order_date: pd.to_datetime(..., format="mixed", dayfirst=True) # drop exact duplicates and reset the index # assert that order_id is unique return df clean = clean_orders("orders_messy.csv") # Sales per cleaned country, as a dict {country: total rounded to 2 decimals} sales_by_country = None
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

(لا مخرجات)

الفحوص

  • في clean 159 صفًا، صف لكل طلب
  • أُعيد ترقيم الفهرس من 0 إلى 158
  • الدول والقنوات وأسماء العملاء منظّفة
  • total رقمي وorder_date من نوع datetime
  • إجمالي المبيعات 22,486.69
  • في sales_by_country 8 دول مجموعها 22,486.69
  • استدعاء clean_orders مرة أخرى يعطي الصفوف الـ 159 نفسها