مُنظّف البيانات بالذكاء الاصطناعي
لقِي كل محلّل نفس مجموعة البيانات: صفوف مكررة، وخلايا فارغة، وعمود price قيمته في أحد الصفوف "2.5 USD" وفي آخر 2.5، وتاريخ طلب تقول بعض صفوفه 2024-01-05 ويقول بعضها 05/01/2024. تخفي هذه المشاكل إشارة حقيقية وتُعطّل الأدوات اللاحقة بطرق مُربكة. يبني هذا المشروع منظّف بيانات سطر أوامر يلتقط CSV فوضويًا، يجد تلك المشاكل تلقائيًا، ويطبّق الإصلاح الصحيح لكل عمود، و، الجزء الذي يجعله جديرًا بالثقة، يسجّل كل تغيير يُجريه في سجل تدقيق يمكنك قراءته كأنه إيصال.
هذا يفترض إتمام Python 101 وأساسيات pandas من وحدة تحليل البيانات ، لا شيء أبعد من ذلك. هذا اختياري وغير مُقيَّم؛ راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة والنامية.
🎯 ما ستفعله
- إنشاء ملف تعريف (profile) لـ CSV فوضوي باستخدام pandas وإنتاج تقرير جودة يغطي القيم المفقودة والتكرارات ومشاكل الأنواع ، دون تعديل البيانات.
- إزالة الصفوف المكررة وإثبات عدد الصفوف التي اختفت بالضبط.
- ملء القيم المفقودة باستراتيجية تُختار لكل عمود (الوسيط للأرقام، والمنوال للنصوص) وتسجيل القرار.
- العثور على القيم الشاذة بقاعدة IQR وقصّها إلى ممرّ معقول.
- تطبيع التواريخ والسلاسل النصية حتى يتساوى أخيرًا
2.5 USDمع2.5. - تجميع كل ذلك في دالة
clean_dataset()واحدة تُعيد DataFrame نظيفًا بالإضافة إلى قاموس تدقيق قابل للقراءة.
أين تُشغّل هذا
محليًا باستخدام uv هو المسار الموصى به ، المنظّف سكربت pandas حتمي (deterministic)، وسير العمل الأساسي هو تشغيله على ملفات CSV على قرصك الخاص، لذا بيئة Python حقيقية مثبّت عليها pandas هي الموطن الصحيح تمامًا له. يشرح الإعداد أدناه خطوات uv والبيئة الافتراضية.
GitHub Codespaces يعمل جيدًا أيضًا: افتح مستودع الدورة كاملًا في Codespace مجاني ، pandas وuv مثبّتان مسبقًا، وكل خطوة أدناه تعمل دون تغيير.
Google Colab وKaggle Notebooks وBinder طريقة جيدة حقًا لتشغيل هذا ، على خلاف المشاريع التي تحتاج مستودع git محليًا أو حالة نظام ملفات حقيقية، يحتاج منظّف البيانات إلى CSV في الذاكرة فقط. يبني دفتر الملاحظات أدناه DataFrame فوضويًا عمدًا وصغيرًا حتى يعمل كل اكتشاف وإصلاح فعليًا؛ استخدم دفتر ملاحظات للتجربة السريعة، ثم انتقل إلى uv المحلي عندما تريد توجيه الأداة إلى ملفات .csv فعلية على جهازك.
الإعداد
كل ما تحتاجه قبل سطر واحد من المنظّف نفسه: بيئة Python مثبّت عليها pandas، وملف CSV فوضوي عمدًا لتوجيهه إليه.
أعِدَّ المشروع
uv init ai-data-cleaner
cd ai-data-cleaner
uv add pandas
يثبّت لك uv Python، وينشئ المشروع، ويضيف pandas إلى بيئته الافتراضية ، سلسلة أمر واحدة بدلًا من الجولة المعتادة “ثبّت Python، ثبّت pip، أنشئ بيئة افتراضية، ونفّذ pip install”.
أنشئ ملف CSV فوضوي للاختبار به
ارسم ملفًا صغيرًا يحوي المشاكل التي وُجدت الأداة لتلتقطها ، الصق هذا في messy.csv:
order_id,customer,units,price,order_date
1, alice ,2,2.50,2024-01-05
2,alice,,,05/01/2024
1, alice ,2,2.50,2024-01-05
3,bob,10,2.5 USD,2024-02-01
4,carol,0,1.00,2024-01-31
5,dave,2,0.75,2024/03/15
5,dave,2,0.75,2024/03/15
2,alice,2,3.50,05/01/2024
6,erin,2,4.00,2024-03-20
7,frank,2,3.50,2024-03-22
8,grace,,2.25,2024-03-25
9,henry,1000,9.99,2024-04-01
يحتوي هذا الملف الواحد على كل نمط فشل يعالجه خط الأنابيب: صفان مكرران تمامًا، وقيمتان مفقودتان في units، وقيمة مفقودة في price، ومسافات في اسم عميل، وprice مكتوبة بثلاث صيغ مختلفة، وorder_id مكرر بتفاصيل مختلفة (تكرار شبه كامل)، وطلب بوحدات صفرية مستحيل، وقيمة شاذة متطرفة، وتواريخ بثلاث صيغ.
✅ قائمة التحقق
- ✅ ينتهي
uv add pandasدون أخطاء. - ✅ يوجد
messy.csvفي مجلد مشروعك ويحوي السطر الثلاثة عشر (الترويسة زائد اثني عشر صف بيانات) الظاهرة أعلاه.
الخطوة 1: أنشئ ملف تعريف (profile) للمجموعة دون لمسها
يجب أن يكون المرور الأول لأي سكربت تنظيف للقراءة فقط ، لا يمكنك الثقة بإصلاحات أداة حتى تستطيع وصف ما هو خاطئ، ولا يمكنك وصف ما هو خاطئ في مجموعة بيانات شوّهتها بالفعل. يحمّل ملف التعريف (profiling) ملف CSV، ثم يمشي عمودًا بعمود ويسأل ثلاثة أسئلة: كم قيمة مفقودة، وكم صفًا مكررًا تمامًا، وما نوع البيانات (dtype) الذي يحمله كل عمود فعلًا.
1.1 حمّل البيانات واطّلع على حجمها
👟 تلميح البداية :
حمّل messy.csv إلى df، واطبع شكلها (shape) وأنواعها وعدد قيمها المفقودة لكل عمود وعدد صفوفها المكررة ، كلها قراءات، بلا كتابات.
# clean.py
import pandas as pd
df = pd.read_csv("messy.csv")
print("shape:", df.shape)
print("\ndtypes:\n", df.dtypes)
print("\nmissing per column:\n", df.isna().sum())
print("\nduplicate rows:", df.duplicated().sum())
print("\nfirst 3 rows:\n", df.head(3))
df.isna().sum() يُعيد عدد الخلايا المفقودة لكل عمود، وdf.duplicated().sum() يعدّ الصفوف التي تُكرر صفًا سابقًا تمامًا ، كلاهما قراءة خالصة تُنتج الأرقام التي سيعمل عليها خط الأنابيب. head(3) على إطار فوضوي عادة تلتقط المشاكل حتى قبل الأرقام: في هذا الإطار يمكنك أن ترى بالفعل price يحمل نصًا واسمًا بمسافات بادئة.
🎯 الناتج المتوقع :
تقرير مطبوع يُظهر shape: (12, 5)، وprice بنوع object (وليس رقميًا) بسبب صف "2.5 USD"، وقيمتين مفقودتين بالضبط في units، وقيمة مفقودة واحدة في price، وduplicate rows: 2.
🩹 إذا لم يعمل :
إذا ظهر price بنوع int64/float64، فقد حرّر أحدهم ملف CSV يدويًا وأزال صف "2.5 USD" الذي يعتمد عليه الفحص. إذا فشل تحميل df بالكامل، فالملف يحوي تعليقًا # أو سطر ترويسة شاردًا ، افتح messy.csv وتأكد أن السطرين الأولين يطابقان مخطط الترويسة تمامًا.
1.2 حوّل الملف التعريفي إلى قاموس تقرير
👟 تلميح البداية :
وسّع السكربت بدالة profile(df) تُعيد قاموسًا يصف مشاكل كل عمود، حتى تتمكن الخطوات اللاحقة (وسجل التدقيق) من قراءة النتائج كبيانات بدلًا من نص طرفية.
# clean.py (continued)
from typing import Any
import pandas as pd
def profile(df: pd.DataFrame) -> dict[str, dict[str, Any]]:
report: dict[str, dict[str, Any]] = {}
for col in df.columns:
report[col] = {
"dtype": str(df[col].dtype),
"missing": int(df[col].isna().sum()),
"n_unique": int(df[col].nunique()),
"issues": [],
}
if df[col].dtype == object:
non_blank = df[col].dropna().astype(str)
if non_blank.str.strip().ne(non_blank).any():
report[col]["issues"].append("leading/trailing whitespace")
return report
print(profile(df))
يتوقف التقرير عن وصف المشاكل نثرًا ويبدأ بوصفها كبيانات ، كل دالة لاحقة يمكنها استهلاك report[col]["missing"] وتقرر ما تفعله. فحص المسافات هو الفحص الدقيق: .str.strip().ne(itself) صحيح لأي قيمة تتغير عند إزالة المسافات المحيطة بها.
🎯 الناتج المتوقع :
تُعيد profile(df) قاموسًا يدرج فيه price dtype: object، وunits missing: 2، وcustomer leading/trailing whitespace في قائمة مشاكله.
🩹 إذا لم يعمل :
إذا لم يُبلِغ أي عمود عن مسافات، فقد حُفظ CSV من جديد مع اقتباس مضمّن حول القيم وأصبحت المسافات اللاحقة جزءًا من النص ، افحص قيم df["customer"] مباشرةً عبر .repr(). إذا ظهر عمود رقمي كنوع object، فعلى الأقل خلية واحدة تحمل سلسلة نصية؛ الإصلاح الصحيح هو اتخاذ قرار بشأن ما تفعله بتلك السلسلة، لا التحويل القسري بعد.
1.3 تحقّق
✅ قائمة التحقق
- ✅ يقرأ
df.shape(12, 5)ويقرأdf.duplicated().sum()2. - ✅ يُبلِغ
unitsعن قيمتين مفقودتين، وpriceعن قيمة مفقودة واحدة ونوعobject. - ✅ تُعيد
profile(df)نتائجها كقاموس يمكن للكود اللاحق قراءته. - ✅ لا يظهر تحذير من pandas عن
mixed typesعند التحميل ، تلك أول إشارة انحراف لك.
🤔 سؤال (أسئلة) سقراطي(ة)
- لماذا تبدأ عمدًا بملف تعريف للقراءة فقط بدلًا من الإصلاح أثناء العمل؟ ما المعلومة المحددة التي يدمرها سكربت “أصلح كل شيء على عجل” قبل أن يمكن تسجيلها؟
- يُبلِغ
profile()عنn_uniqueلكل عمود. ماذا يخبرك عمودcustomerبـn_uniqueيساوي 6 (عدد صفوفه) بما لا يمكن لـduplicated().sum()وحدها اكتشافه؟ تلميح: فكّر في شكلcustomerبعد إصلاح المسافات.
الخطوة 2: احذف التكرارات ، وعدّ ما أزلته
التكرارات أرخص مشكلة تُصلَح، والمشكلة التي يصلحها الناس يدويًا أكثر من غيرها (“دعني فقط أحذف التكرارات الواضحة”). نسخة خط الأنابيب أفضل من المرور اليدوي لأنها تسجل العدد، فيعرف أي شخص يدقّق النتيجة أن بيانات قد أُزيلت ، شفافية لا يمنحك إياها تحرير يدوي في جدول بيانات أبدًا.
2.1 احذف التكرارات التامة مع إيصال
👟 تلميح البداية :
نفّذ df.drop_duplicates() مرة واحدة، لكن التقط عدد الصفوف قبل ناقص عددها بعد إلى سجل التدقيق قبل أن تُغيَّر DataFrame.
# clean.py
import pandas as pd
def drop_duplicates(df: pd.DataFrame) -> tuple[pd.DataFrame, dict]:
before = len(df)
df = df.drop_duplicates()
removed = before - len(df)
return df, {"action": "drop_duplicates", "removed_rows": removed, "before": before, "after": len(df)}
df = pd.read_csv("messy.csv")
df, audit = drop_duplicates(df)
print(audit)
print("rows now:", len(df))
يُبقي drop_duplicates() أول ظهور لكل صف مكرر افتراضيًا ، سلوك حتمي، وهذا مهم، لأن سجل التدقيق يدّعي عددًا محددًا من الصفوف المزالة. التقاط before وafter حول الاستدعاء يحوّل “أعتقد أنني أزلت بعضًا” إلى عدد دقيق قابل للإثبات.
🎯 الناتج المتوقع :
يُبلِغ قاموس التدقيق عن removed_rows: 2، ويقرأ rows now: 10. اختفيا الصفّان اللذان علّمهما duplicated() سابقًا (تكرار order_id 1 وتكرار order_id 5) وما تزال DataFrame تحتفظ بالنسخة الأولى من كلٍّ منهما.
🩹 إذا لم يعمل :
إذا قرأ removed_rows 0، فصفوف CSV المكررة تختلف بحرف غير مرئي (مسافة لاحقة في أحدها) ، يجب أن يعمل تطبيع المسافات في الخطوة 5 قبل مرور التكرارات على بيانات لم تكتبها أنت. إذا نجا الصف 3 (تكرار 1, alice, 2, 2.50)، فالقيم ما تزال تختلف في مكان ما ، اطبع df.iloc[[0, 2]] صفًا صفًا لتستطلع الفرق بالعين.
2.2 فكّر في معنى “التكرار”
👟 تلميح البداية :
استكشف فحص تكرار جزئي ، df.drop_duplicates(subset=["order_id"]) ، وقارن عدده المحذوف بعدد التكرارات التامة.
# clean.py (continued)
df_partial = pd.read_csv("messy.csv")
print("exact duplicates:", df_partial.duplicated().sum())
print("duplicates by order_id only:", df_partial.duplicated(subset=["order_id"]).sum())
يغيّر subset=[...] تعريف التكرار من “كل الأعمدة متساوية” إلى “أعمدة المفتاح متساوية”. يكاد العددان يختلفان دائمًا، واختيار التعريف الصحيح قرار عمل لا قرار كود: التام فقط آمن، والخاص بالمفتاح عدواني وقد يحذف عميلين مختلفين يشاركان المفتاح مصادفة.
🎯 الناتج المتوقع :
يطبع العدد التام 2؛ يطبع عدد الخاص بـ order_id 3 (الصفوف 2 و3 و8 كلها تكرارات لـ order_id موجود)، وهو أكثر مما كان على الأرجح إنسان مستعدًا لحذفه.
🩹 إذا لم يعمل :
إذا ساوى العدد الخاص العدد التام، أعد النظر في CSV بحثًا عن order_id رابع لم تقصده. إذا حذف نهج الخاص أكثر مما يريحك، فهذا الانفعال هو المقصود ، تناول keep="last" أو قاعدة صريحة حين تساوي البيانات أكثر من الاختصار.
2.3 تحقّق
✅ قائمة التحقق
- ✅ يزيل حذف التكرارات التامة صفين بالضبط ويسجّل
removed_rows: 2في قاموس تدقيق. - ✅ تستطيع شرح ما يتغير عند استخدام
subset=["order_id"]، ولماذا هو أكثر عدوانية. - ✅ سجل التدقيق يحتوي الآن إدخالًا كلما خسرت DataFrame صفوفًا.
🤔 سؤال (أسئلة) سقراطي(ة)
- ماذا سيحدث لأمانة سجل التدقيق لو أزال
drop_duplicates()صفوفًا أربعة بصمت بدلًا من اثنين لأن CSV احتوى نسختين من العميل نفسه بصيغيpriceمختلفتين؟ أين يتيح لك خط الأنابيب التقاط ذلك قبل أن يعتمد أحد على الملف النظيف؟ - العدد الخاص
3يتجاوز العدد التام2. هل النسخة التامة دائمًا الإجابة “الصحيحة”؟ أعطِ سيناريو حقيقيًا يكون فيه الحذف المبني على الخاص هو السلوك الصحيح، وتبقى النسخة التامة مجموعة بيانات خاطئة.
الخطوة 3: املأ القيم المفقودة، عمودًا بعمود
تُملأ الخلايا المفقودة بشكل مختلف حسب ما يعنيه العمود. سعر رقمي تنقصه قيمة واحدة يُخمَّن أفضل بوسيط أقرانه؛ حقل نصي حر مفقود (كاسم أوسط) غالبًا أفضل تركه “غير معروف” صريحًا. مهمة خط الأنابيب أن يقرر لكل عمود ويسجّل المنطق، فيعرف القارئ أن units = 4.0 كانت تعبئة بوسيط وليست قيمة أصلية.
3.1 املأ الرقميات بالوسيط والنصوص بالمنوال
👟 تلميح البداية :
اكتب fill_missing(df) تملأ كل عمود رقمي بوسيطه وكل عمود نصي بقيمته الأكثر تكرارًا، متجاوزة أي عمود لا شيء في ملئه.
# clean.py (continued)
def fill_missing(df: pd.DataFrame) -> pd.DataFrame:
for col in df.columns:
if df[col].isna().sum() == 0:
continue
if pd.api.types.is_numeric_dtype(df[col]):
df[col] = df[col].fillna(df[col].median())
else:
df[col] = df[col].fillna(df[col].mode()[0])
return df
شكل الحلقة هو النمط: انظر إلى عمود، واحسب خلاياه المفقودة، وتصرّف فقط إذا كان العدد غير صفري. تخطّي الأعمدة بلا مفقود يتجنب إدخالات التدقيق المزعجة التي تسجّل “تعبئة” لا شيء، وis_numeric_dtype يُبقي الاستراتيجية أمينة ، الأرقام تأخذ وسيطًا، والنصوص منوالًا، ولا تُطبَّق أي من الاستراتيجيتين على نوع العمود الخطأ.
🎯 الناتج المتوقع :
تشغيل هذا على الإطار الذي حُذفت تكراراته يضبط خليتي units المفقودتين إلى 2 (وسيط القيم [2, 10, 0, 2, 2, 2, 1000])، وprice الرقمية تُملأ خليتها المفقودة الوحيدة بـ 2.5.
🩹 إذا لم يعمل :
إذا بقيت الخلايا المفقودة NaN بعد الاستدعاء، فمسار التعبئة لم يصل إليه التنفيذ أبدًا ، تأكد أن isna().sum() كان غير صفري فعلًا لذلك العمود (خليتا units المفقودتان في صفّي alice وgrace؛ وتأكد أن التكرارات حُذفت لا الصفوف الحاملة). إذا وجدت غرابة في عمود نصي مثل customer ملأناه بمنوال كوسيط، فهذا هو السلوك الصحيح هنا ، اختيار الاستراتيجية يختلّ فقط عند تورط المعرفات، وهو ما تعالجه الخطوة 5.
3.2 سجّل القرار في سجل التدقيق
👟 تلميح البداية :
الآن وقد نجحت التعبئة، أضف إدخالات التدقيق داخل الحلقة ، إدخالًا واحدًا لكل عمود مملوء ، يسمّي العمود والاستراتيجية وعدد الخلايا المملوءة، ثم اطبع السجل المتنامي.
# clean.py (continued)
def fill_missing_audited(df: pd.DataFrame, audit: list[dict]) -> pd.DataFrame:
for col in df.columns:
n = int(df[col].isna().sum())
if n == 0:
continue
if pd.api.types.is_numeric_dtype(df[col]):
df[col] = df[col].fillna(df[col].median())
strategy = f"median ({df[col].median():.2f})"
else:
df[col] = df[col].fillna(df[col].mode()[0])
strategy = f"mode ({df[col].mode()[0]!r})"
audit.append({"action": "fill_missing", "column": col, "cells_filled": n, "strategy": strategy})
return df
audit = []
df = pd.read_csv("messy.csv")
df, a1 = drop_duplicates(df)
audit.append(a1)
df = fill_missing_audited(df, audit)
print(*audit, sep="\n")
pd.api.types.is_numeric_dtype(df[col]) هو الفرع الذي يُبقي الاستراتيجية أمينة: الأرقام تأخذ وسيطًا، والنصوص منوالًا. تصل الآن كل تعبئة إلى audit كصف بسلسلة استراتيجيته الخاصة، فيُشرَع الملف النظيف النهائي بمستند مصاحب يحدد بالضبط ما اختُلق ولماذا.
🎯 الناتج المتوقع :
إدخال تعبئة units يقرأ "median (2.00)" مع cells_filled: 2، زائد إدخال تعبئة price يستخدم استراتيجية mode ، وجوده باستراتيجية نصية هو الدليل أن price ما تزال نصًا في هذه النقطة، وهو بالضبط خطأ الترتيب الذي يمنعه خط الأنابيب الكامل بتطبيع الصيغ أولًا (الخطوة 5).
🩹 إذا لم يعمل :
إذا أظهر إدخال price استراتيجية بنمط رقمي بلا تفسير، فقد شغّلت التعبئة بعد تحويل price خارج الترتيب ، نتيجة جيدة في حد ذاتها، لكن لاحظ أن العرض يعتمد على نصٍّ داخلًا وخارجًا. إذا مُلئت الخلايا لكن التدقيق لم يحتوها أبدًا، فإلحاق القائمة داخل فرع if الخطأ، أو أعادت الدالة دون إضافة.
3.3 تحقّق
✅ قائمة التحقق
- ✅ تُملأ الأعمدة الرقمية بوسيطها؛ والنصية بمنوالها.
- ✅ يوجد إدخال تدقيق واحد لكل عمود مملوء، يسمّي كلٌّ منه الاستراتيجية وعدد الخلايا.
- ✅ الأعمدة بلا مفقود لا تُنتج أي إدخال تدقيق.
🤔 سؤال (أسئلة) سقراطي(ة)
- لماذا يُبرَّر ملء
unitsبوسيطه بينما ملءorder_idبوسيطه هراء؟ ما المعلومة التي يحملها dtype والتي يجب أن تحترمها دالة التعبئة؟ - يخزّن التدقيق سلسلة الاستراتيجية لا الفعل فقط. ما السؤال المستقبلي الذي يتيح لك ذلك الإجابة عنه، ولا يمكن لتدقيق
action: fill_missingوحده؟
الخطوة 4: التقط القيم الشاذة بقاعدة IQR
قيمة units بمقدار 1000 بجوار أقران 0 و2 شبه خطأ مطبعي مؤكد، لكن حذفها أعمى يخسر أعمدة الصف الأخرى. تجد قاعدة IQR ممرّ القيم المعقولة ، أي شيء يبعد أكثر من 1.5 × IQR تحت الربيع الأول أو فوق الثالث ، وتقصّ المخالفين إلى حافة الممر، محافظةً على الصف ومُبطِلةً التشويه.
4.1 احسب الممر وعلّم المخالفين
👟 تلميح البداية :
للأعمدة الرقمية في إطار ما، احسب Q1 وQ3 وIQR، ثم اسرد كل صف خارج [Q1 - 1.5*IQR, Q3 + 1.5*IQR].
# clean.py (continued)
def flag_outliers(df: pd.DataFrame, columns: list[str]) -> dict[str, list]:
outliers: dict[str, list] = {}
for col in columns:
if not pd.api.types.is_numeric_dtype(df[col]):
continue
q1, q3 = df[col].quantile([0.25, 0.75])
iqr = q3 - q1
lo, hi = q1 - 1.5 * iqr, q3 + 1.5 * iqr
found = df[(df[col] < lo) | (df[col] > hi)]
if len(found):
outliers[col] = [found.index.tolist(), round(lo, 2), round(hi, 2)]
return outliers
df = pd.read_csv("messy.csv")
df, _ = drop_duplicates(df)
print(flag_outliers(df, ["units", "price"]))
df[col].quantile([0.25, 0.75]) يُعيد الربيعين في استدعاء واحد، ويختار قناع القيم المنطقية (df[col] < lo) | (df[col] > hi) الصفوف خارج الممر ، لاحظ معامل | لا or، لأن pandas يحتاج أقنعة عُنصرية مدمجة، ويطويها Python’s or في قيمة حقيقة واحدة.
🎯 الناتج المتوقع :
تُبلِغ الدالة عن صف شاذ واحد في units (القيمة 1000 عند الفهرس الأصلي 11) داخل ممر يبلغ تقريبًا (-1.0, 7.0) ، وتتخطى price تمامًا لأنها في هذه النقطة لا تزال نصًا والفرع الرقمي يرفض عمدًا الحكم عليها.
🩹 إذا لم يعمل :
إذا لم يُبلِغ أي عمود عن قيم شاذة، فالحارس الرقمي يتخطاك بصمت ، نوع object ينتج قناعًا فارغًا تحت هذه القاعدة، ولهذا يُظهر price لا شيء عمدًا؛ شغّل هذا بعد خطوة تطبيع price وسيسمح لها الحارس أخيرًا بالمرور. إذا ظهر ValueError: The truth value of a Series is ambiguous، فقد استخدمت or حيث يجب |.
4.2 اقصص بدلًا من أن تحذف
👟 تلميح البداية :
استبدل القيم المخالفة بـ Series.clip(lower=lo, upper=hi) وسجّل القيمة القديمة والجديدة في سجل التدقيق ، الحالة النادرة التي يخزن فيها السجل زوجًا قبل/بعد.
# clean.py (continued)
def clip_outliers(df: pd.DataFrame, columns: list[str], audit: list[dict]) -> pd.DataFrame:
for col in columns:
if not pd.api.types.is_numeric_dtype(df[col]):
continue
q1, q3 = df[col].quantile([0.25, 0.75])
lo, hi = q1 - 1.5 * (q3 - q1), q3 + 1.5 * (q3 - q1)
mask = (df[col] < lo) | (df[col] > hi)
clipped = df.loc[mask, col].tolist()
df[col] = df[col].clip(lower=lo, upper=hi)
if clipped:
audit.append({"action": "clip_outlier", "column": col, "from": clipped, "to": round(hi, 2)})
return df
audit = []
df = pd.read_csv("messy.csv")
df, _ = drop_duplicates(df)
df = clip_outliers(df, ["units", "price"], audit)
print(*audit, sep="\n")
clip(lower=lo, upper=hi) يدفع كل قيمة داخل الممر في استدعاء متجه واحد ، بلا حلقة، ويُبقي 1000 كـ 7.0 بدلًا من حذف حقول الصف الأربعة الأخرى. تخزين قائمة from إلى جانب to يجعل سجل التدقيق أفضل بخطوة من معظم سجلات الإنتاج: يمكنه الإجابة “ماذا غيّرنا فعلًا في هذا الصف؟” بدلًا من “ما الذي لمسناه؟” فقط.
🎯 الناتج المتوقع :
تصبح 1000 في units 12.0، ويظهر إدخال تدقيق {"action": "clip_outlier", "column": "units", "from": [1000], "to": 7.0}. ويُتخطى عمود price ما دام نصًا ويبقى دون لمس.
🩹 إذا لم يعمل :
إذا لم يحدث أي قص رغم 1000 الواضح، تأكد أن التحويل الرقمي من الخطوة 5 نُفّذ أولًا. إذا سجّل إدخال التدقيق قصًا لكن DataFrame ما تزال تعرض 1000، فقد أُسقط التعيين df[col] = df[col].clip(...) وأنت تطبع الإطار قبل القص.
4.3 تحقّق
✅ قائمة التحقق
- ✅ تُقصّ
units = 1000إلى7.0، وتُحفَظ أعمدة الصف الأخرى. - ✅ يسجّل سجل التدقيق زوجًا قبل/بعد للقيمة المقصوصة.
- ✅ تستطيع القول لماذا يتفوق القص على حذف الصف كاملًا هنا.
🤔 سؤال (أسئلة) سقراطي(ة)
- يخفي الممرّ حكمًا:
1.5عُرف لا قانون. ماذا سيحدث لـunitsلو استخدمت3.0بدلًا؟ أي نوع من البيانات سيكون شرعيًا خارج ممرّ1.5ويُسطَّح خطأً بهذه القاعدة؟ - لماذا القص لا حذف الصف؟ ما المعلومة التي تنجو في الصف وكانت ستضيع لولا ذلك، وفي أي تحليل لاحق يهم نجاؤها فعلًا؟
الخطوة 5: طبّع الصيغ حتى تُقارَن القيم بنظافة
يحمل العمود الرقمي "2.5 USD" بجوار 3.00، وتستخدم التواريخ 2024-01-05 و05/01/2024 و2024/03/15 في العمود نفسه. تفشل mean() على أي من العمودين أو تكذب اليوم. يجبر تطبيع الصيغ كل قيمة على شكل واحد ، عدد عشري لـ price، وdatetime.date للتواريخ، ونص منزوع المسافات للأسماء ، وهذه الخطوة هي سبب بدء التعبئات وفحوصات القيم الشاذة السابقة بالعمل على الإطار.
5.1 حوّل price إلى شكل رقمي واحد
👟 تلميح البداية :
اكتب normalize_price(series) تجرّد الضجيج غير الرقمي، وترغم النتيجة، وتُبلِغ عن كل خلية لم تستطع تحويلها كمشكلة منفصلة.
# clean.py (continued)
import pandas as pd
def normalize_price(s: pd.Series) -> tuple[pd.Series, list[str]]:
cleaned = s.astype(str).str.replace(r"[^\d.]", "", regex=True)
converted = pd.to_numeric(cleaned, errors="coerce")
undecodable = s[converted.isna() & s.notna()].tolist()
return converted, [str(v) for v in undecodable]
df = pd.read_csv("messy.csv")
df, _ = drop_duplicates(df)
p, stuck = normalize_price(df["price"])
df["price"] = p
print(df["price"].tolist())
print("could not convert:", stuck)
تُزيل العبارة المعتادة [^\d.] كل ما ليس رقمًا أو نقطة عشرية ، هذا هو اتساع الفأس هنا، وهو أمين: يتعامل مع "2.5 USD"، لكنه سيدمر أيضًا قيمة عملة مختلفة فعلًا مثل "2,50€". يحوّل errors="coerce" أي شيء ما زال غير قابل للتحليل إلى NaN بدلًا من التعطل، وتُبرز تلك الخلايا المتبقية كقائمة stuck فلا يحرق خط الأنابيب أبدًا بصمت قيمةً عجز عن قراءتها.
🎯 الناتج المتوقع :
يصبح df["price"] [2.5, nan, 2.5, 1.0, 0.75, 3.5, 4.0, 3.5, 2.25, 9.99] ، خلية "2.5 USD" الآن عدد عشري ، وstuck فارغة لهذا CSV.
🩹 إذا لم يعمل :
إذا نجت قيمة كـ "2.5 USD"، فالعبارة [^\d.] لم تعمل على ذلك الصف لأن السلسلة حملت غير نص (خلية رقمية سابقًا) ، أجبر على التحويل بـ .astype(str) أولًا كما هو معروض. إذا لم تكن stuck فارغة، فملفك فيه قيمة شوّهتها العبارة بدلًا من تنظيفها ، قرّر قاعدة واحدة لكل عملة ووسّع العبارة عمدًا، أو أبقِ الصف معلَّمًا بدلًا من حذفه.
5.2 طبّع التواريخ والنصوص في مرور واحد
👟 تلميح البداية :
سلّم التواريخ عبر pd.to_datetime(..., format="mixed") وجرّد أعمدة النصوص، مرفقًا ملاحظات التنظيف بقائمة التدقيق المتنامية.
# clean.py (continued)
def normalize_formats(df: pd.DataFrame, audit: list[dict]) -> pd.DataFrame:
for col in df.columns:
if df[col].dtype == object and "date" in col.lower():
before = df[col].nunique()
df[col] = pd.to_datetime(df[col], format="mixed")
audit.append({"action": "normalize_date", "column": col, "unique_before": before, "dtype": str(df[col].dtype)})
elif df[col].dtype == object:
stripped = df[col].astype(str).str.strip().astype("string")
if stripped.ne(df[col].astype(str)).any():
audit.append({"action": "strip_text", "column": col})
df[col] = stripped
return df
df = pd.read_csv("messy.csv")
_, a1 = drop_duplicates(df)
audit = [a1]
df = normalize_formats(df, audit)
print(df[["order_date", "customer"]])
print(*audit, sep="\n")
يفوز التطبيع السريع بالسباق هنا مع الحفاظ على أشكال المدخل الثلاثة: يتيح لك format="mixed" ترك pandas يخمّن لكل خلية بدلًا من الإصرار على أن صيغة واحدة تصف كل صف. astype("string") في النهاية يستخدم نوع السلسلة القابل للخفت (nullable) الخاص بـ pandas، فيتوقف العمود المجرّد عن تخزين NaN كنوع عائم بصمت ويسجّل المفقودية بأمانة.
🎯 الناتج المتوقع :
يطبع order_date كعمود datetime64 واحد متسق، ويُظهر customer alice, bob, carol, dave, erin, frank, grace, henry بلا مسافات محيطة، ويكتسب التدقيق إدخالي normalize_date وstrip_text.
🩹 إذا لم يعمل :
إذا رفع تحليل Mixed format خطأً، يحمل بعض الخلايا غموضًا حقيقيًا كـ 02/03/2024 حيث قد ينقلب الشهر واليوم ، يُبقيه format="mixed" قابلًا للتحليل لكنه اختار قراءة بصمت؛ ثبّت الصيغة بـ format="%d/%m/%Y" عندما تعرف بياناتك. إذا بقيت أعمدة النصوص مقصوصة في مخرجات الشاشة لكنها حفظت مسافات في الإطار، فلم يُعَد تعيين DataFrame من stripped.
5.3 تحقّق
✅ قائمة التحقق
- ✅
priceعمود رقمي واحد؛stuckلا تُبلِغ عن شيء غير قابل للقراءة. - ✅ كل خلايا
order_dateبنوعdatetime64واحد، مهما كانت تهجئتها الأصلية. - ✅ أعمدة النصوص مجرّدة ومخزنة كنوع
stringمن pandas. - ✅ توجد إدخالات تدقيق لكل تطبيع غيّر بيانات فعلًا.
🤔 سؤال (أسئلة) سقراطي(ة)
- تحوّل العبارة
[^\d.]"2.5 USD"بنظافة ، لكن ماذا تفعل بقيمة كـ"2,500.00"من لغة محلية تستخدم فواصل الآلاف؟ وماذا يقول ذلك عن استبدال قرار بشري بعبارة معتادة؟ - بعد التطبيع يمكن أن تظهر تكرارات لم تكن موجودة من قبل (صفّان قيمة أحدهما
"2.5 USD"والآخر2.5). لماذا يجب أن يتشارك إزالة التكرار وتطبيع الصيغ مرورًا نهائيًا واحدًا بدلًا من مرحلتين منفصلتين؟
الخطوة 6: اجمع خط الأنابيب الكامل مع سجل تدقيقه
تصلح كل قطعة الآن مشكلة واحدة بمعزل؛ يربط خط الأنابيب بينها بترتيب منطقي ، ملف تعريف، ثم تطبيع صيغ، ثم حذف تكرارات (الموثوق الآن)، ثم تعبئة بالعمود، ثم قص قيم شاذة ، ويُعيد DataFrame نظيفًا واحدًا بالإضافة إلى قائمة التدقيق الكاملة كسجل قابل للتحويل إلى JSON.
6.1 اكتب clean_dataset(path)
👟 تلميح البداية :
ركّب الدوال بترتيب التبعية في clean_dataset(path) تُعيد (clean_df, audit) وأضف كتلة __main__ تطبعهما معًا؛ وتأكد أن أي دالة تفشل ترفع خطأً واضحًا يسمّي العمود الذي عملت عليه.
# clean.py (final -- every helper from Steps 1-5 now lives in this same file)
import json
import pandas as pd
def clean_dataset(path: str) -> tuple[pd.DataFrame, list[dict]]:
df = pd.read_csv(path)
report = profile(df)
if not report:
raise ValueError(f"Cannot profile {path} -- is the file empty?")
audit: list[dict] = [{"action": "profile", "issues": report}]
df = normalize_formats(df, audit)
price, _stuck = normalize_price(df["price"])
df["price"] = price
df, a = drop_duplicates(df)
audit.append(a)
df = fill_missing_audited(df, audit)
df = clip_outliers(df, ["units", "price"], audit)
return df, audit
if __name__ == "__main__":
clean, trail = clean_dataset("messy.csv")
print(clean)
print("\naudit:\n", json.dumps(trail, indent=2, default=str))
يرمّز الترتيب حكمًا لا عادة: تُطبَّع الصيغ أولًا حتى يرى مرور التكرارات قيمًا قابلة للمقارنة، ويعمل قص القيم الشاذة أخيرًا حتى يعمل على بيانات مملوءة رقمية. الفشل السريع داخل clean_dataset عبر raise ValueError(...) أفضل من شحن ملف نصف مُنظَّف بصمت يكشفه جدول بيانات لاحقًا. تطبع json.dumps(trail, indent=2, default=str) المفردة التدقيق كإيصال قابل للقراءة.
🎯 الناتج المتوقع :
إطار نظيف مطبوع بصفوف 10 بالضبط (12 ناقص التكرارين)، مع price رقمي، وأسماء منزوعة، وتواريخ موحدة، وunits وprice مملوءين بوسيط، وقيمة شاذة في units مقصوصة إلى 7.0، وقائمة تدقيق تحوي كل فعل اتخذه خط الأنابيب بترتيب التنفيذ.
🩹 إذا لم يعمل :
إذا ظهر KeyError: 'price'، فعمود سعر CSV ليس اسمه price ، خط الأنابيب يثبّت اسمًا واحدًا؛ اجعله معاملًا عندما تخالف البيانات. إذا حذفت إزالة التكرار أكثر من صفين في خط الأنابيب الكامل، فأحد مرورات التطبيع دمج سلسلتين كانتا مختلفتين سابقًا ، قارن أي الصفوف اختفت بإعادة التشغيل على الملف الأصلي.
6.2 تحقّق
✅ قائمة التحقق
- ✅ تُعيد
clean_dataset("messy.csv")إطارًا نظيفًا بصفوف 10 وأعمدة منقّطة. - ✅ تحتوي قائمة التدقيق إدخالات بالترتيب: ملف تعريف، تطبيع صيغ، حذف تكرارات، تعبئة مفقودات، قص قيم شاذة.
- ✅ تستطيع إعادة بناء من التدقيق بالضبط ما تحوّلت إليه كل قيمة أصلية.
🤔 سؤال (أسئلة) سقراطي(ة)
- يشغّل خط الأنابيب تطبيع الصيغ قبل حذف التكرارات. تتبّع ماذا يحدث لو بدّلت المرحلتين على
messy.csvالأصلي: أي الصفوف تنجو، وأي قرار صار مختلفًا الآن بشأنprice؟ - تُعيد
clean_datasetقائمة ثابتة من الأعمدة الرقمية للقص. ماذا تغيّر في توقيع الدالة لتظل صحيحة على مجموعة بيانات بلا عمودprice، قائمة أعمدة محددة، أم قاعدة؟ بأيهما تثق أن يحافظ عليه زميلك؟
⚠️ مآزق شائعة
- إصلاح البيانات قبل أن تستطيع وصفها. سكربت يملأ ويقصّ عند التحميل يدمر الدليل على أن إصلاحًا كان لازمًا ، عرّف البيانات أولًا دائمًا، واحفظ ذلك التقرير الأول في التدقيق.
- ملء المعرفات بإحصاءات. ملء
order_idبوسيط أوtimestampبمنوال ينتج قيمًا تبدو حقيقية ولا تعني شيئًا. قيّد التعبئات بنوع البيانات وبقائمة أعمدة مسموحة (allowlist). - الحذف بدلًا من القص. إزالة الصفوف الشاذة تخسر بصمت أعمدة تلك الصفوف غير الشاذة. عندما يكون حقل واحد سخيفًا والباقي جديرًا بالثقة، اقصص الحقل.
- إفراط العبارات المعتادة في الصيغ. تنظيف بـ
[^\d.]يحوّل"2,500.00"و"2.50€"إلى أرقام مفاجئة. أبرز القيم غير القابلة للاسترجاع عبر قائمةstuckبدلًا من التظاهر أن العبارة فهمتها. - تحويلات لا يمكن تتبعها. بيانات نظيفة بلا سجل تدقيق لا تُفرَّق عن بيانات كانت خاطئة منذ البداية. كل تحول ، حذف، تعبئة، قص، تطبيع ، فعل قابل للتدقيق، ويعامله هذا الخط بهذا الشكل.
ما بنيته للتو
أداة CLI عاملة لتنظيف البيانات: تحمّل CSV فوضويًا فعلًا، وتُبلِغ عما هو خاطئ قبل لمس خلية، ثم تصلح التكرارات والقيم المفقودة والقيم الشاذة وفوضى الصيغ بترتيب متعمد ، عائدةً بكليهما: DataFrame نظيف وتدقيق كامل لكل تغيير. المهارة القابلة للنقل هنا تعمر أطول من الأداة نفسها: عادة تسجيل كل تحول كبيانات، فتبقى مجموعة البيانات المنظفة قادرة دائمًا على الإجابة “ماذا فعلت بي، ولماذا؟”.
شغّل نسخة أكمل دون أي إعداد محلي
examples/ai-data-cleaner/ في مستودع الدورة هو خط الأنابيب نفسه معبأً لدفتر ملاحظات، مع طباعة خطوات ملف التعريف والتدقيق في كل مرحلة. استنسخه، أو افتح المستودع كاملًا في GitHub Codespace، وشغّله من هناك.
إلى أين تذهب من هنا
- حوّل قائمة
stuckإلى نقطة قرار: علم--strictيرفض كتابة المخرجات ما دامت أي قيمة غير قابلة للاسترجاع، فلا يشحن خط الأنابيب ملفًا لم يفهمه كاملًا. - أضف معالجة المسافات بالنافذة الكاملة والترميزات المختلطة عبر خيار argparse
--encoding، وطبّع ملفات UTF-8 BOM التي تقرؤها pandas قراءة خاطئة بصمت. - أطعم سجل التدقيق وحدة تصوير البيانات في الدورة: اعرض مخطط أعمدة للمشاكل حسب العمود والاستراتيجية حتى يقرّ إنسان التعبئات في نظرة واحدة.
- وجّه خط الأنابيب إلى API مشروع لوحة مراقبة جودة الهواء ونظّف استجابات
/apiقبل وصولها إلى رسومك البيانية.
شارك مشروعك مع الصف
بنيت شيئًا تفخر به؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون ، وREADME الخاص به يحوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر pull request، حتى لو لم تستخدم git من قبل قط: عمل fork للمستودع، وإنشاء فرع، وتثبيت ملفاتك، وفتح الـ PR، خطوة بخطوة. لا يُفترض أي خبرة سابقة بـ git.
مرحبًا بك في كتابة Python خارج المتصفح. 🎓
أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.