البيانات المفقودة — العثور على الفجوات وتحديد معناها
العثور على كل فجوة في الجدول، بما في ذلك القيم المخفية كنصوص مثل - أو ؟، وفهم كيف يغير NaN عمليات الجمع والمتوسطات والأنواع، واتخاذ قرار مدروس عموداً بعمود — بحذف الفجوة أو ملئها أو الإبقاء عليها.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
تم إدخال طلبات المتجر للأسبوع الأول من شهر مارس يدوياً في ثلاثة فروع، وتصديرها في ملف باسم orders_raw.csv. ويطلب مالك المتجر معرفة رقمين بحلول المساء: كم عدد الوحدات التي بيعت، وكم من المال تم تحصيله.
تقوم بقراءة الملف، وبدافع العادة من الفصول السابقة، تبدأ بحساب مجموع عمود السعر أولاً لمعرفة حجم المبيعات تقريبياً:
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(orders["price"].sum())15.060.0850.08.0-15.0120.0هذا الناتج ليس رقماً على الإطلاق. بل هو كل الأسعار ملتصقة ببعضها كنص واحد — ولم يظهر بانداس أي خطأ لتنبيهك بذلك. والآن ننتقل إلى عمود الكمية:
print(orders["quantity"].sum())
print(orders["quantity"].mean())76.0
10.857142857142858يبدو هذا المخرج سليماً للوهلة الأولى، وذلك تحديداً ما يجعله أكثر خطورة. يحتوي الملف على ثمانية طلبات، ومع ذلك فإن ناتج 76 / 8 هو 9.5، وليس 10.86. كانت إحدى خانات الكمية فارغة، فقام بانداس باستبعادها بهدوء من المجموع ومن المتوسط الحسابي معاً. ولم يخبرك أحد بذلك أيضاً.
إذن، يحتوي الملف على فجوات (gaps)، وتأتي هذه الفجوات بنوعين. بعضها مرئي: خلية فارغة يحولها بانداس إلى علامة خاصة تدل على "قيمة مفقودة" ثم يتخطاها بصمت في العمليات الحسابية. والبعض الآخر مخفي: قام شخص ما بكتابة - بدلاً من ترك الخلية فارغة، فتعامل معها بانداس كقيمة نصية عادية، وتحول عمود السعر بأكمله إلى نصوص.
كلا النوعين لا يُصدر أي رسالة خطأ. وكلاهما يغير الإجابة النهائية. يدور هذا الفصل حول كيفية العثور على كل فجوة، وفهم ما يفعله بانداس معها، ثم اتخاذ قرار مدروس — بحذفها (drop)، أو ملئها (fill)، أو الإبقاء عليها (keep) — بدلاً من ترك السلوك الافتراضي يقرر بالنيابة عنك.
بنهاية هذا الفصل ستكون قادراً على
- شرح ماهية
NaN، ولماذا تكون نتيجةNaN == NaNهيFalse، ولماذا يتحول عمود الأعداد الصحيحة إلى أعداد عشرية بمجرد فراغ خلية واحدة - عدّ القيم المفقودة في كل عمود باستخدام
isna().sum()واستخراج الصفوف غير المكتملة - تحويل العلامات المخفية مثل
-أو?إلى قيم مفقودة حقيقية باستخدامna_values=أوpd.to_numeric(errors="coerce") - توقع كيفية تعامل
sumوmeanوcountوالعمليات الحسابية بين الأعمدة مع القيم المفقودة - إزالة القيم المفقودة باستخدام
dropna()، مع تحديدsubset=وhow=لحذف ما تقصده تحديداً - استبدال القيم المفقودة باستخدام
fillna()— بقيمة واحدة، أو بقيم مختلفة لكل عمود، أو بقيمة إحصائية - اتخاذ قرار مدروس لكل عمود على حدة بشأن الحذف أو الملء أو الإبقاء — مع بيان السبب
- تجنب فخ
inplace=Trueالذي لا يقوم بأي تغيير في بانداس 3
المتطلبات المسبقة: تصفية الصفوف.
أنشئ الملف أولاً
أنشئ في مجلد مشروعك ملفاً باسم orders_raw.csv بهذه الأسطر تحديداً. الفجوات موجودة عن قصد — انتبه جيداً للأسطر الثالث والخامس والسابع:
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0هذه هي الطلبات الثمانية نفسها من ملف orders.csv في الفصول السابقة، مع أربعة عيوب: الطلب 1002 ليس له كمية، والطلب 1004 ليس له فرع وسعره مسجل كـ N/A، والطلب 1006 يحمل علامة - كسعر له.
قبل كتابة الكود
البيانات المفقودة هي الموضوع الوحيد الذي تصبح فيه كتابة الكود أولاً خطوة محفوفة بالمخاطر: فكلتا الدالتين dropna() و fillna() تعملان دون أي اعتراض وتغيران النتيجة في الوقت نفسه. لذا يبدأ العمل دائماً بالفحص والتقرير.
1. صِغ السؤال في جملة واحدة. "ما إجمالي الكمية المباعة، وإجمالي الإيرادات (الكمية × السعر) لهذا الأسبوع — وكم عدد الطلبات التي تعذر احتسابها؟" الجزء الأخير بالغ الأهمية. فالإجمالي الذي يستبعد طلبين بصمت ليس كالإجمالي الذي يوضح أن "6 من أصل 8 طلبات تم احتسابها".
2. عاين ما وصلك أولاً وقبل أي شيء آخر. تخبرك الدالتان shape و info() معاً بكل ما تحتاج معرفته عن كل عمود:
print(orders.shape)
orders.info()(8, 7)
<class 'pandas.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 8 non-null int64
1 date 8 non-null str
2 branch 7 non-null str
3 product 8 non-null str
4 category 8 non-null str
5 quantity 7 non-null float64
6 price 7 non-null str
dtypes: float64(1), int64(1), str(5)
memory usage: 580.0 bytesقارن هذه المخرجات بما كنت تتوقعه، عموداً بعمود:
branch— كان المتوقع 8 قيم غير فارغة من النوعstr، لكنك حصلت على 7 فقط. هناك فرع مفقود.quantity— كان المتوقع 8 قيم غير فارغة من النوعint64، لكنك حصلت على 7 من النوعfloat64. هناك كمية مفقودة، وهذا هو سبب تحوله إلىfloat64(كما سنوضح أدناه).price— كان المتوقع 8 قيم غير فارغة من النوعfloat64، لكنك حصلت على 7 من النوعstr. هناك سعر مفقود كما تسربت نصوص إلى داخل العمود.
هناك أمران في تلك القائمة يمثلان قرائن وأدلة: ظهور عمود الأعداد الصحيحة بنوع float64 يعني بنسبة شبه مؤكدة وجود فجوة. وظهور عمود رقمي بنوع str يعني دائماً اختلاط نصوص بالأرقام — والنصوص في عمود الأرقام تكون عادة علامات خفية تدل على قيم مفقودة.
3. خطط لشكل المخرجات التي تريدها. مثل هذا الشكل تقريباً:
Missing values per column: branch 1, quantity 1, price 2
Orders counted for revenue: 6 of 8
Total quantity (known): 76
Total revenue (counted): ...لاحظ أن التقرير يعرض الفجوات إلى جانب المجاميع. يجب على قارئ التقرير أن يعلم أن الإيرادات المعروضة هي حد أدنى (lower bound).
4. حدد الأداة المناسبة لكل فجوة — قبل كتابة الكود. اسأل نفسك لكل عمود: ما معنى وجود خلية فارغة هنا، وهل يمكن الإجابة عن السؤال بدونها؟
branch— الفجوة تعني أن الطلب تم ولكن لم يُسجل الفرع. القرار: املأها بـ"Unknown"، لأن الطلب حقيقي ومبلغه يدخل في الحساب.quantity— الفجوة تعني أننا لا نعلم عدد الوحدات المباعة. القرار: أبقِها مفقودة واستبعد الصف من حساب الإيرادات، لأن ابتكار رقم من عندك (0؟ أو المتوسط؟) سيخلق مبيعات وهمية.price— الفجوة تعني أننا لا نعلم سعر البيع. القرار: أبقِها مفقودة واستبعد الصف من حساب الإيرادات للسبب نفسه — مع توضيح عدد الصفوف المستبعدة في التقرير.
5. حدد ما ستفحصه بعد كل خطوة. بعد كل خطوة تحذف قيماً أو تستبدلها، اطبخ أعداد القيم المفقودة والأبعاد مجدداً. إذا أدت دالة dropna() إلى تقليص البيانات من 8 صفوف إلى صفين، فمن الأفضل أن تكتشف ذلك فوراً، وليس بعد إصدار التقرير.
تشرح بقية أجزاء هذا الفصل كل أداة بالترتيب، وفي النهاية سنجمع هذه الخطة في برنامج واحد متكامل.
كيف تبدو القيمة "المفقودة": NaN
عندما يجد بانداس خلية فارغة، فإنه لا يخزن نصاً فارغاً ولا صفراً. بل يخزن قيمة عشرية خاصة تُعرف باسم NaN — أي "not a number" (ليس رقماً). يمكنك إنشاء واحدة بنفسك بتمرير None إلى كائن Series:
import pandas as pd
quantity = pd.Series([12, None, 2])
print(quantity)0 12.0
1 NaN
2 2.0
dtype: float64انظر إلى نوع البيانات dtype. لقد مررت عددين صحيحين وفجوة، وحصلت في المقابل على float64 مع القيمتين 12.0 و 2.0. هذا هو السبب الكامن وراء القرينة التي لاحظناها في info(): فقيمة NaN هي قيمة عشرية (float)، ولا يمكن لعمود الأعداد الصحيحة التقليدي احتواؤها. لذا، بمجرد أن تصبح خلية واحدة مفقودة، يقوم بانداس بترقية العمود بالكامل إلى float64 ليفسح المجال لقيمة NaN. وهذا يفسر طباعة quantity في ملفنا بالأشكال 12.0 و 2.0 وما إلى ذلك.
أما الأعمدة النصية فتتصرف بطريقة مختلفة؛ إذ يمكن لعمود str أن يحمل قيمة مفقودة دون تغيير نوعه:
branch = pd.Series(["north", None, "east"])
print(branch)0 north
1 NaN
2 east
dtype: strلا تزال تُطبع كـ NaN، ويظل النوع str. لذا ستقابل NaN في بانداس 3 في كل من أعمدة الأرقام والنصوص، وتستطيع الأدوات نفسها العثور عليها في الحالتين.
NaN لا تساوي أي شيء — ولا حتى نفسها
هذه الحقيقة هي الأكثر إثارة للدهشة حول القيم المفقودة، وهي تفسد الطريقة الأكثر تلقائية للبحث عنها:
import numpy as np
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(np.nan == np.nan)
print((orders["branch"] == np.nan).sum())False
0تعني NaN "قيمة ما لا نعرفها". وقيمتان غير معروفتين لا يُفترض تساويهما، لذا تُرجع المقارنة False — في كل مرة. هذا يجعل التعبير == np.nan اختباراً لا يطابق أي شيء على الإطلاق: فعلى الرغم من وجود فجوة في عمود الفرع، فإن العدّ يعطي 0. اسأل بانداس مباشرة بدلاً من ذلك.
إياك والبحث عن القيم المفقودة باستخدام معامل المقارنة==. فالتعبير== np.nanينفذ دون أي خطأ ولا يعثر على شيء أبداً، مما يجعل العمود المليء بالفجوات يبدو مكتملاً تماماً. استخدم الدالةisna().
البحث عن القيم المفقودة: isna()
تطرح الدالة isna() السؤال الذي يعجز عنه المعامل ==: "هل هذه الخلية مفقودة؟" عند تطبيقها على عمود، تُرجع عموداً من قيم True/False — قناعاً (mask) يشبه تماماً الأقنعة التي استخدمناها في فصل التصفية:
print(orders["branch"].isna())0 False
1 False
2 False
3 True
4 False
5 False
6 False
7 False
Name: branch, dtype: boolوعلى مستوى جدول DataFrame بالكامل، تقوم بالشيء نفسه لكل خلية. ونادراً ما ستحتاج لقراءة ثمانية صفوف من قيم True/False؛ بل ستحتاج عادة إلى عدّها، وبما أن True تُحسب كـ 1، فإن الدالة .sum() تفي بالغرض:
print(orders.isna().sum())order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 1
dtype: int64هذا هو السطر الذي ينبغي تشغيله بعد info() على أي ملف جديد. إنه يمثل نفس معلومات Non-Null Count، ولكن بصيغة معكوسة تعدّ الفجوات مباشرة.
ولكن انظر إلى price: يظهر الرقم 1. ونحن نعلم أن هناك سعرين معطوبين — N/A و -. احتفظ بهذه النقطة في ذهنك؛ سنعود إليها في القسم التالي.
ما هي الصفوف غير المكتملة؟
يخبرك العدد بالكمية؛ ولمعرفة أي الصفوف هي، استخدم القناع كعامل تصفية. يطرح التعبير isna().any(axis=1) سؤالاً لكل صف: "هل هناك أي قيمة مفقودة في هذا الصف؟":
incomplete = orders[orders.isna().any(axis=1)]
print(incomplete)order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery NaN 60.0
3 1004 2024-03-02 NaN bottle accessories 7.0 NaNالمعامل axis=1 يعني "البحث أفقياً عبر الصف" بدلاً من البحث عمودياً إلى الأسفل. ولاحظ من ليس في القائمة: الطلب 1006، الذي يحمل السعر -. بالنسبة لبانداس، هذا الصف مكتمل تماماً. احتفظ بهذه الفكرة أيضاً. إن فحص الصفوف الفعلية يستحق كتابة سطر إضافي: فهكذا تلاحظ، على سبيل المثال، أن كل الكميات المفقودة تخص فرعاً واحداً بعينه، وهو سؤال يُوجه للمسؤولين في ذلك الفرع وليس لمكتبة بانداس.
كنسبة مئوية وليس مجرد عدد
مع ثمانية صفوف، تكون الأعداد كافية. ولكن مع ثمانين ألف صف، لا يعني وجود "312 قيمة مفقودة" الكثير حتى تعرف أنها تمثل 0.4%. دالة .mean() المطبقة على عمود من قيم True/False تُعطي نسبة ظهور True:
print(orders.isna().mean())order_id 0.000
date 0.000
branch 0.125
product 0.000
category 0.000
quantity 0.125
price 0.125
dtype: float64خلية واحدة من كل ثماني خلايا في أعمدة branch و quantity و price مفقودة — أي بنسبة 12.5% لكل منها. العمود الفارغ بنسبة 90% يتطلب قرارات تختلف تماماً عن العمود الفارغ بنسبة 0.1%، وهذه هي الطريقة التي تميز بها بينهما.
القيم المفقودة المخفية: عندما تُكتب الفجوة كنص
عدّت الدالة isna() سعراً مفقوداً واحداً، لكن هناك سعرين غير صالحين للاستخدام. السعر الثاني هو علامة - في الطلب 1006. بالنسبة لبانداس، ليست علامة - قيمة مفقودة — بل هي قيمة نصية صالحة تماماً. ووجود قيمة نصية واحدة في عمود أرقام كافٍ لجعل العمود بأكمله من النوع str، وهو ما أظهرته دالة info() تماماً.
إليك كيفية معرفة ما يحتويه العمود فعلياً:
print(orders["price"].unique())<StringArray>
['15.0', '60.0', '850.0', nan, '8.0', '-', '120.0']
Length: 7, dtype: strكل قيمة محاطة بعلامات اقتباس — كلها نصوص، بما فيها '15.0'. وهناك نوعان من الفجوات متجاوران: nan، التي تعرف عليها بانداس، و '-'، التي لم يتعرف عليها.
لماذا تحولت N/A إلى nan بينما لم تتحول -؟ لأن الدالة read_csv تأتي بقائمة مدمجة من النصوص التي تعاملها كقيم مفقودة افتراضياً: الخلية الفارغة، و NA، و N/A، و n/a، و NaN، و null، و None وبعض النصوص الأخرى. أما - و ? و missing و 0 و unknown فليست مدرجة في تلك القائمة. وما كتبه مدخل البيانات للتعبير عن القيمة المجهولة هو ما يحدد نوع الفجوة التي ستحصل عليها.
هناك طريقتان لتحويل هذه العلامات المخفية إلى قيم NaN حقيقية.
الحل 1: إخبار read_csv بالعلامة — na_values=
إذا كنت تعرف العلامة مسبقاً، فإن أفضل حل وأنظفه يكون عند لحظة قراءة الملف. حيث يضيف المعامل na_values= نصوصك المحددة إلى القائمة المدمجة:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
print(orders["price"].dtype)
print(orders.isna().sum())float64
order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 2
dtype: int64قارن هذه النتيجة بالعدد السابق. بقي عمودا branch و quantity دون تغيير — لأن na_values= يضيف إلى القائمة ولا يستبدلها، مما يحافظ على التعرف على الخلايا الفارغة و N/A. ما تغير هو عمود price: أصبح الآن float64، ويُظهر وجود 2 من القيم المفقودة — حيث تحولت كل من N/A و - إلى قيم NaN حقيقية الآن. وأصبح العمود رقمياً مجدداً، مما يعني أن العمليات الحسابية عليه ستتصرف كحسابات فعلية.
لاحظ أن na_values=["-"] تنطبق على كل الأعمدة. وهذا ما نريده هنا. أما إذا كانت علامة - يمكن أن تمثل قيمة صحيحة في عمود آخر — كرمز لمنتج مثلاً — فمرر قاموساً بدلاً من ذلك، مثل na_values={"price": ["-"]}، وبذلك سيتأثر عمود price فقط.
الحل 2: التحويل لاحقاً — pd.to_numeric(errors="coerce")
في بعض الأحيان لا تعرف مسبقاً النصوص الغريبة الموجودة في العمود، أو قد تكون قرأت الملف عبر كود كتبه شخص آخر. في هذه الحالة، تقوم بتحويل العمود بعد القراءة. لنرَ أولاً ما يفعله التحويل العادي:
raw = pd.read_csv("orders_raw.csv")
price = pd.to_numeric(raw["price"])ValueError: Unable to parse string "-" at position 5هذا الخطأ مفيد للغاية: فهو يذكر القيمة المخالفة وموقعها بدقة. وإضافة errors="coerce" تعني "أي قيمة تعجز عن تحويلها إلى رقم، اجعلها NaN":
price = pd.to_numeric(raw["price"], errors="coerce")
print(price)0 15.0
1 60.0
2 850.0
3 NaN
4 8.0
5 NaN
6 15.0
7 120.0
Name: price, dtype: float64لتعديل الجدول فعلياً، أعد إسناد النتيجة إلى العمود نفسه — فالدالة to_numeric تُرجع كائن Series جديداً وتترك raw دون تعديل:
raw["price"] = pd.to_numeric(raw["price"], errors="coerce")
print(raw["price"].dtype, raw["price"].isna().sum())float64 2أي الحلين تختار
المعامل errors="coerce" قوي وحاسم، ولكنه قد يكون عشوائياً بعض الشيء. فهو يحول أي شيء غير رقمي إلى NaN — بما في ذلك السعر الحقيقي الذي كُتب خطأً مثل 1O0 (بحرف O الإنجليزي) أو 1,200 مع فاصلة الآلاف. هذه ليست قيماً مفقودة، بل هي قيم تحتوي على أخطاء إملائية، وإجبارها على التحويل يتخلص من البيانات بصمت.
لذا اجعل عادتك دائماً: عُدّ القيم قبل التحويل، وعُدّها بعده، وفَسّر الفرق.
- إذا كنت تعرف العلامة (
-أو?أوmissing): استخدمna_values=فيread_csv. إنه خيار دقيق — إذ يحول ذلك النص بعينه فقط إلى قيمة مفقودة. - إذا كانت هناك شوائب غير معروفة، ويجب أن يكون العمود رقمياً: استخدم
pd.to_numeric(errors="coerce"). إنه يلتقط كل شيء، لذا افحص ما التقطه بعد الانتهاء. - إذا كانت بعض القيم عبارة عن أخطاء كتابية يمكن تصحيحها: افحص
unique()أولاً، وأصلح الأخطاء، ثم أجرِ التحويل. فالإجبار على التحويل قد يدمر بيانات حقيقية.
قبل التحويل الإجباري، أظهرت دالة raw["price"].unique() نصاً واحداً غير رقمي ('-') وقيمة nan واحدة. وبعد الإجبار، أصبح هناك اثنان من NaN تحديداً. الأرقام متطابقة، لذا لم نفقد أي شيء آخر. هذا الفحص البسيط هو الفارق الحاسم بين تنظيف البيانات وإتلافها.
من هنا فصاعداً سنعتمد النسخة المقروءة باستخدام na_values=["-"].
كيف تتعامل العمليات الحسابية مع NaN
الآن بعد أن أصبحت الفجوات قيماً حقيقية من نوع NaN، تحتاج إلى معرفة كيف يتعامل بانداس معها — لأنه لن يتوقف ليسألك في كل مرة.
ملخصات الأعمدة تتجاهل NaN
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
q = orders["quantity"]
print("len :", len(q))
print("count:", q.count())
print("sum :", q.sum())
print("mean :", q.mean())
print("check:", q.sum() / q.count())len : 8
count: 7
sum : 76.0
mean : 10.857142857142858
check: 10.857142857142858ثلاثة سلوكيات يجب تذكرها:
lenتحسب عدد الصفوف؛ بينما تحسبcount()عدد القيم الموجودة. الفرق بينهما هو عدد الفجوات. وهذا يطابق مفهومNon-Null Countفيinfo().sum()تتجاهلNaN. تجمع الكميات السبع المعلومة فقط. هذا منطقي — ولكنه يعني أن "76" هو مجموع الكميات المعلومة، وليس مجموع كل الطلبات.mean()تقسم علىcount()وليس علىlen. يُحسب المتوسط على القيم السبع المعلومة فقط. وهذا ما تريده عادة؛ فمعاملة الفجوة كصفر كانت ستخفض المتوسط دون مبرر.
إذا كنت تفضل أن يتم تنبيهك بوجود قيمة مفقودة، فإن تمرير skipna=False يجعل النتيجة NaN بمجرد وجود أي قيمة مفقودة:
print(q.sum(skipna=False))nanالعمليات الحسابية بين الأعمدة تنشر NaN
تتجاهل الدوال التلخيصية الفجوات؛ أما العمليات الحسابية فتنشرها. أي عملية تجمع بين قيمة عادية و NaN تنتج NaN، لأن "7 مضروبة في سعر مجهول" تظل قيمة مجهولة:
revenue = orders["quantity"] * orders["price"]
print(revenue)
print("total:", revenue.sum())
print("rows counted:", revenue.count(), "of", len(revenue))0 180.0
1 NaN
2 1700.0
3 NaN
4 240.0
5 NaN
6 300.0
7 480.0
dtype: float64
total: 2900.0
rows counted: 5 of 8اجمع بين هذين السلوكين وسترى الفخ المذكور في بداية الفصل بوضوح. طبعت الدالة revenue.sum() القيمة 2900.0 دون أي تحذير — لكن ثلاثة من الطلبات الثمانية لم تساهم بشيء في الناتج. الطلب 1002 تنقصه كمية، والطلبان 1004 و 1006 ينقصهما سعر. لا يكون المجموع نزيهاً إلا إذا وضحت أنه يمثل "5 من أصل 8 طلبات". لهذا السبب طلبت خطتنا وضع عداد بجانب كل مجموع.
إزالة القيم المفقودة: dropna()
تحذف الدالة dropna() الصفوف التي تحتوي على NaN. وبدون أي وسائط، تقوم بحذف أي صف يحتوي على أي فجوة في أي عمود:
print(orders.shape)
print(orders.dropna().shape)(8, 7)
(5, 7)اختفت ثلاثة صفوف من أصل ثمانية — وتم استبعاد الطلب 1004 لأن فرعه كان مفقوداً، رغم أنه كان صفاً صالحاً تماماً لحساب إجمالي الكمية. هذه هي مشكلة استخدام dropna() بدون تحديد: إنها تجيب عن سؤال "احذف أي شيء غير مثالي"، وهو سؤال نادراً ما نسعى إليه.
subset= — الأعمدة التي يحتاجها السؤال فقط
حدد الأعمدة التي يجب توفرها بالضرورة:
known_qty = orders.dropna(subset=["quantity"])
print(known_qty.shape)
print(known_qty["quantity"].sum())(7, 7)
76.0تم حذف الطلب 1002 فقط — وهو الصف الوحيد الذي كانت كميته مجهولة بالفعل. أما لحساب الإيرادات، فيلزم وجود كلا العمودين:
countable = orders.dropna(subset=["quantity", "price"])
print(countable[["order_id", "quantity", "price"]])order_id quantity price
0 1001 12.0 15.0
2 1003 2.0 850.0
4 1005 30.0 8.0
6 1007 20.0 15.0
7 1008 4.0 120.0انتبه للفهرس: 0, 2, 4, 6, 7. تحتفظ dropna() بالفهارس الأصلية تماماً كما تفعل عمليات التصفية، بحيث يمكنك تتبع الصفوف في الملف الأصلي دائماً. وإذا كنت بحاجة إلى ترقيم متسلسل 0, 1, 2… مجدداً، أضف .reset_index(drop=True).
how="all" — الصفوف الفارغة تماماً فقط
غالباً ما تنتهي ملفات الجداول المصدرة بعدد قليل من الصفوف الفارغة في كل الأعمدة. يحذف الخيار how="all" تلك الصفوف بالذات ويُبقي على أي صف يحتوي ولو على قيمة واحدة:
print(orders.dropna(how="all").shape)(8, 7)لم يُحذف شيء هنا — فليس لدينا أي صف فارغ تماماً — وهذا تحديداً ما تحتاج للتأكد منه.
dropna() تُرجع جدولاً جديداً
كما هي الحال مع معظم دوال بانداس، لا تقوم dropna() بتعديل جدول orders الأصلي؛ بل تمنحك جدول DataFrame جديداً. إذا كتبت orders.dropna() في سطر منفصل، فلن يتغير شيء في orders. عليك إسناد النتيجة — ويفضل إسنادها لاسم جديد إذا كنت بحاجة للجدول الكامل لاحقاً، وهو ما ستحتاجه في تقرير مثل تقريرنا (لتوضيح عدد الصفوف المحذوفة).
استبدال القيم المفقودة: fillna()
في بعض الأحيان يكون هناك بديل منطقي للفجوة. تضع الدالة fillna() قيمة مكان NaN.
قيمة واحدة لعمود واحد
الفرع المفقود هو المثال الأوضح على ذلك. فالطلب حدث وأمواله حقيقية؛ كل ما في الأمر أننا لا نعرف الفرع الذي نفذه. وملء الفراغ بتسمية محددة يحافظ على الصف ويجعل الفجوة واضحة في أي إحصائية لاحقة حسب الفروع:
orders["branch"] = orders["branch"].fillna("Unknown")
print(orders["branch"].tolist())['north', 'south', 'north', 'Unknown', 'north', 'south', 'east', 'north']صيغة ذلك السطر مهمة للغاية: خذ العمود، وطبق عليه fillna، ثم أعد إسناده للعمود نفسه. تُرجع دالة fillna كائن Series جديداً؛ وبدون وجود orders["branch"] = في البداية، لن يُحفظ أي شيء.
قيم مختلفة لأعمدة مختلفة: استخدام dict
مرر قاموساً بصيغة {column: value} لملء عدة أعمدة معاً، كل منها بقيمته الخاصة. وتبقى الأعمدة غير المذكورة دون أي تعديل:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
filled = orders.fillna({"branch": "Unknown", "quantity": 0})
print(filled.isna().sum())order_id 0
date 0
branch 0
product 0
category 0
quantity 0
price 2
dtype: int64لا يزال عمود price يحتوي على فجوتيه، لأننا لم نذكره في القاموس. وهذه هي ميزة القاموس مقارنة بـ orders.fillna(0)، التي كانت ستملأ كل الأعمدة — بما فيها الأسعار — بالرقم 0، وسعر 0 يعني حقيبة مجانية.
ملء الفراغات يغير النتيجة
بدا ملء الكمية بالرقم 0 تصرفاً غير ضار. قارن المتوسطات الناتجة:
print("skip the gap :", orders["quantity"].mean())
print("fill with 0 :", orders["quantity"].fillna(0).mean())
print("fill with median:", orders["quantity"].fillna(orders["quantity"].median()).mean())skip the gap : 10.857142857142858
fill with 0 : 9.5
fill with median: 10.375ثلاثة متوسطات مختلفة لعمود واحد، والفارق الوحيد هو ما قررته لمعنى الكمية المجهولة:
- 0 يعني "لم يبع هذا الطلب أي شيء". وهذا غير صحيح بالنسبة للطلب 1002 (طلب دفاتر بيع منه بالتأكيد شيء ما)، مما يسحب المتوسط إلى الأسفل ظلماً.
- الوسيط (median) (وهو 7 هنا) يعني "افتراض طلب نموذجي معتاد". هذا يحافظ على المتوسط قريباً من وضعه الأصلي، لكن الإجمالي أصبح يشمل الآن 7 دفاتر لم يحسبها أحد في الواقع.
- التجاهل يعني "لا نعرف، لذا دعونا نستبعدها". فيكون المتوسط محسوباً على الطلبات السبعة المعروفة فقط.
لا يوجد خيار من هذه الخيارات "صحيح" بشكل مطلق؛ فكل منها يجيب عن سؤال مختلف قليلاً. ولهذا السبب يجب أن يكون القرار مكتوباً في خطتك ومدروساً بعناية، بدلاً من الاعتماد على أي قيمة افتراضية متوفرة.
قاعدة عامة تعمل بنجاح في معظم الحالات:
- التسميات والتصنيفات (الفرع، الفئة، الاسم): املأها بـ
"Unknown"أو ما شابه ذلك. يُحفظ الصف، وتظل الفجوة مرئية كمجموعة قائمة بذاتها. - الأعداد التي يعني فراغها الصفر فعلياً (المرتجعات، الشكاوى): املأها بـ
0— ولكن فقط عندما يقصد مصدر البيانات الصفر فعلاً بالخانة الفارغة. - القياسات أو المبالغ المالية (الكمية المباعة، السعر، الدرجات): احتفظ بـ
NaN، واستبعدها من الحساب، وأوضح عدد المستبعدين في تقريرك. فالأرقام المصطنعة تصبح مجاميع مصطنعة. - العمود الذي يتطلبه السؤال وموجود في معظمه: طبق
dropna(subset=[...])على ذلك السؤال تحديداً. خسارة طفيفة ومقبولة في سبيل إجابة أمينة وصادقة.
استعادة الأعداد الصحيحة
بعد الملء، لا يحتوي عمود quantity على أي فجوات، ولكنه يظل من النوع float64 — فلا يعيده بانداس إلى أعداد صحيحة تلقائياً. وبمجرد التخلص من كل NaN، يعمل التحويل astype(int) بنجاح:
filled["quantity"] = filled["quantity"].astype(int)
print(filled["quantity"].tolist())
print(filled["quantity"].dtype)[12, 0, 2, 7, 30, 1, 20, 4]
int64جرب تشغيله قبل الملء وسيفشل، لأنه لا يوجد عدد صحيح في بايثون يعبر عن "قيمة مجهولة" — انظر قسم "عندما تفشل البرمجية" أدناه.
مثال تطبيقي متكامل
يضع الملف clean_orders.py الخطة التي وضعناها في "قبل كتابة الكود" في برنامج واحد متكامل: يقرأ الملف بالعلامة المعروفة، ويعرض كل فجوة، ويملأ الفرع، ويستبعد الصفوف التي يتعذر احتسابها، ويُخرج المجاميع بوضوح مع عدد الطلبات التي تغطيها.
import pandas as pd
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
# 1. Look first
print("Rows, columns:", orders.shape)
missing = orders.isna().sum()
print("Missing per column:")
print(missing[missing > 0])
print()
# 2. Decide per column: branch -> label; quantity and price -> keep, exclude
orders["branch"] = orders["branch"].fillna("Unknown")
# 3. Only rows with both numbers can be counted for revenue
countable = orders.dropna(subset=["quantity", "price"])
skipped = orders[orders[["quantity", "price"]].isna().any(axis=1)]
revenue = countable["quantity"] * countable["price"]
print("Orders counted for revenue:", len(countable), "of", len(orders))
print("Total quantity (known): ", int(orders["quantity"].sum()))
print("Total revenue (counted): ", revenue.sum())
print()
print("Not counted - check these with the branch:")
print(skipped[["order_id", "branch", "quantity", "price"]])Rows, columns: (8, 7)
Missing per column:
branch 1
quantity 1
price 2
dtype: int64
Orders counted for revenue: 5 of 8
Total quantity (known): 76
Total revenue (counted): 2900.0
Not counted - check these with the branch:
order_id branch quantity price
1 1002 south NaN 60.0
3 1004 Unknown 7.0 NaN
5 1006 south 1.0 NaNلماذا كُتب البرنامج بهذه الطريقة:
na_values=["-"]موجودة في السطر الأول تماماً. تم اكتشاف العلامة مسبقاً بفحصunique(). ووضعها فيread_csvيجعل العمود رقمياً منذ البداية، ويرى أي شخص يقرأ الكود العلامة المستخدمة في الملف بوضوح.missing[missing > 0]تطبع فقط الأعمدة التي تحتوي على فجوات. في ملف يضم أربعين عموداً، هذا هو الفارق بين تقرير سهل القراءة وجدار من الأصفار.- تم ملء الفرع ولم تُملأ الأرقام. هذا تطبيق مباشر لقائمة القرارات الواردة في الخطة البرمجية. قد يختلف القارئ مع القرار، لكنه يراه بوضوح ولا يُخفى عنه شيء.
- حددت
dropna(subset=...)العمودين اللذين تتطلبهما الإيرادات، لذا استُبعد الطلب 1004 لغياب سعره، وليس لغياب فرعه. - طُبعت الصفوف المستبعدة ولم يُكتفَ بعدّها. عبارة "3 طلبات لم تُحسب" تثير تلقائياً السؤال "أي طلبات؟" — فأجبنا عن ذلك في التقرير. هذه الطلبات الثلاثة قائمة يمكن لأي شخص التحقق منها وتصحيحها في الفرع.
- تسميتا "Total quantity (known)" و "(counted)" توضحان طبيعة الأرقام بدقة. فكلاهما حد أدنى، والتسميات تمنع أي شخص من اعتبارهما حساباً مكتملاً.
عندما تفشل البرمجية
TypeError: Cannot perform reduction 'mean' with string dtype لقد طلبت حساب المتوسط الحسابي لعمود قرأه بانداس كنص — والسبب الشائع هو احتواء خلية على علامة مثل - أو ? أو missing. نفّذ df["col"].unique() لتحديد القيمة المخالفة، ثم أعد قراءة الملف باستخدام na_values=["-"] أو حول العمود باستخدام pd.to_numeric(df["col"], errors="coerce").
raw = pd.read_csv("orders_raw.csv")
print(raw["price"].mean())TypeError: Cannot perform reduction 'mean' with string dtypeأعادت دالة sum() سلسلة نصية طويلة من الأرقام، مثل 15.060.0850.0... السبب نفسه مع مظهر أسوأ: في الأعمدة النصية تقوم دالة sum() بدمج النصوص معاً بدلاً من جمع الأرقام، ولا تصدر أي رسالة خطأ. أي مجموع يظهر بهذا الشكل مصدره عمود من النوع str. افحص df.dtypes دائماً قبل جمع أي شيء.
ValueError: Unable to parse string "-" at position 5 واجهت دالة pd.to_numeric نصاً لم تستطع تحويله، وأخبرتك بالقيمة وموقعها بدقة. إما أن تضيف errors="coerce" (بعد التأكد عبر unique() من أن القيم الغريبة هي علامات للقيم المفقودة وليست أخطاء كتابية)، أو تصحح الأخطاء الكتابية أولاً.
تُرجع (df["col"] == np.nan).sum() القيمة 0 رغم وجود فجوات قيمة NaN لا تساوي أي شيء على الإطلاق، حتى نفسها، لذا فإن == np.nan لا تطابق أي خلية. استخدم دائماً df["col"].isna().
IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer لقد استدعيت astype(int) على عمود لا يزال يحتوي على قيم NaN. لا يوجد في بايثون عدد صحيح يعبر عن "قيمة مجهولة". املأ الفجوات أو احذفها أولاً، ثم أجرِ التحويل:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"] = orders["quantity"].astype(int)pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')(المسافات المفقودة في "integer.Replace" و "typethat" موجودة في رسالة بانداس الأصلية نفسها). تشير الرسالة أيضاً إلى النوع 'Int64' بحرف I كبير: وهو نوع أعداد صحيحة خاص ببانداس يمكنه احتواء القيم المفقودة. وهو مفيد بالتأكيد، إلا أن ملء الفجوات أو حذفها أولاً هو الحل الأبسط والأكثر شيوعاً.
ظهور خطأ ChainedAssignmentError — مع بقاء الفجوة دون تغيير لقد استخدمت أسلوب inplace=True الذي تجده في العديد من الشروحات القديمة:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"].fillna(0, inplace=True)
print(orders["quantity"].isna().sum())1
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.
Such inplace method never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' instead, to perform the operation inplace on the original object, or try to avoid an inplace operation using 'df[col] = df[col].method(value)'.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.htmlلا يزال العدد 1: لم يُملأ أي شيء. في بانداس 3، يتصرف orders["quantity"] دائماً كنسخة مستقلة بذاتها (وفق آلية النسخ عند الكتابة Copy-on-Write)، لذا فإن ملأه "في مكانه" يُعدل تلك النسخة المستقلة التي سرعان ما تُهمل وتُحذف. كانت إصدارات بانداس القديمة تسمح بذلك أحياناً، ولهذا السبب ينتشر هذا النمط بكثرة على الإنترنت. اكتب التعيين صراحة بدلاً من ذلك — فهو يعمل في كل الإصدارات ويوضح بدقة ما يتم تغييره:
orders["quantity"] = orders["quantity"].fillna(0)
print(orders["quantity"].isna().sum())0حذفت دالة dropna() صفوفاً أكثر بكثير مما توقعت تحذف الدالة dropna() العادية الصف عند وجود فجوة في أي عمود — بما في ذلك عمود الملاحظات notes أو الخصومات discount الفارغ في معظمه والذي قد لا تستخدمه أصلاً في تحليلك. اطبع df.isna().sum() لتحديد العمود الذي يحتوي على فجوات كثيرة، ثم استخدم dropna(subset=[...]) مع تحديد الأعمدة التي يحتاجها سؤالك فقط.
TypeError: can't multiply sequence by non-int of type 'float' حاولت ضرب عمود رقمي في عمود نصي (quantity * price بينما price لا يزال نصاً str). الحل يطابق الخطأ الأول: اجعل عمود price رقمياً أولاً.
Step 4 of 6 — Predict
Check your understanding
إحدى خانات الكمية quantity فارغة. ما الذي سيتم طباعته؟
from io import StringIO
import pandas as pd
# Stands in for orders_raw.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
print(orders["quantity"].sum(), orders["quantity"].count(), len(orders))- A76 8 8
- B76.0 7 8
- Cnan 7 8
- D76.0 8 8
الهدف هو العثور على الطلبات التي ليس لها سعر. هناك سعران مفقودان. ما الذي سيتم طباعته؟
from io import StringIO
import pandas as pd
# Stands in for orders_raw.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW), na_values=["-"])
missing_price = orders[orders["price"] == float("nan")]
print(len(missing_price))- A2
- B1
- C0 — `NaN` لا يساوي أي شيء، ولا حتى نفسه، لذا فإن `==` لا يعثر عليه أبداً
- Dخطأ `TypeError`: لا يمكن المقارنة مع `nan`
تمت قراءة عمود السعر price كنص str لأن إحدى الخلايا تحتوي على - وأخرى على N/A. ما السطر الذي يحوله إلى أرقام، مع تحويل كلتا الفجوتين إلى NaN؟
- Aorders["price"] = orders["price"].astype(float)
- Borders["price"] = pd.to_numeric(orders["price"], errors="coerce")
- Corders["price"] = orders["price"].fillna(0)
- Dorders["price"] = orders["price"].dropna()
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
دورك الآن
بدأ المتجر خدمة التوصيل المنزلي، ووصل سجل السائقين للأسبوع الأول في ملف باسم deliveries.csv. أنشئه على هذا النحو تحديداً:
order_id,branch,distance_km,fee,rider
2001,north,3.5,60,R1
2002,north,,60,R2
2003,south,8.0,?,R1
2004,east,12.0,120,
2005,north,2.0,40,R2
2006,south,6.5,90,?
2007,east,,110,R3
2008,north,4.0,60,R3يسأل المالك: كم بلغ إجمالي رسوم التوصيل المحصلة، وكم عدد عمليات التوصيل التي تفتقد إلى الرسوم، وما متوسط الرسوم لكل كيلومتر؟
قبل كتابة أي كود، افحص الملف باستخدام unique() وحدد العلامة المخفية. ثم اكتب برنامج deliveries_report.py الذي يقوم بما يلي:
- قراءة الملف بحيث تتحول العلامة المخفية إلى قيمة مفقودة حقيقية في كل عمود تظهر فيه
- طباعة عدد القيم المفقودة في كل عمود، والصفوف غير المكتملة
- ملء قيم السائق
riderالمفقودة بـ"unassigned" - طباعة إجمالي الرسوم المعلومة، وعدد التوصيلات بدون رسوم، والرسوم لكل كيلومتر — إجمالي الرسوم مقسوماً على إجمالي المسافة — محسوبة فقط من عمليات التوصيل التي تكون فيها الرسوم والمسافة معاً معلومتين
يجب أن تكون الأسطر الثلاثة الأخيرة التي يطبعها برنامجك مطابقة لهذا النص تماماً:
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesاكتب أيضاً، قبل الكود، قائمة قرارات مطابقة لتلك الواردة في قسم "قبل كتابة الكود": لكل من distance_km و fee و rider، ما معنى الفجوة، وهل ستملؤها أم تحذفها أم تبقيها؟
ثم جرب تجربة واحدة: احسب الرسوم لكل كيلومتر باستخدام dropna() بدون وسائط بدلاً من dropna(subset=...). هل يتغير الرقم؟ وكم عدد الصفوف التي استخدمها، ولماذا؟
الحل
الخطة أولاً. يتكون السؤال من ثلاثة أجزاء: إجمالي، وعدّ للفجوات، ونسبة. تُظهر قراءة info() أن عمود fee من النوع str — وينبغي أن يكون عمود الرسوم رقمياً، مما يعني وجود نصوص مختبئة بداخله. القرارات لكل عمود:
rider— تمت عملية التوصيل، لكن السائق لم يُسجل. نملأه بـ"unassigned".fee— لا نعلم ما تم تحصيله. نحتفظ بـNaN؛ ونجمع الرسوم المعلومة ونعدّ غير المعلومة منها.distance_km— لا نعلم المسافة المقطوعة. نحتفظ بـNaN؛ ونستبعد تلك الصفوف من حساب نسبة التكلفة لكل كيلومتر.
لحساب النسبة، يجب أن يكون كلا الرقمين معلومين لنفس عملية التوصيل — وإلا فستقسم رسوم سبع توصيلات على مسافات ست توصيلات أخرى.
الخطوة 1 — قراءة الملف بشكل عادي وفحصه للعثور على العلامة:
import pandas as pd
raw = pd.read_csv("deliveries.csv")
print(raw.shape)
print(raw.dtypes)
print(raw["fee"].unique())
print(raw["rider"].unique())(8, 5)
order_id int64
branch str
distance_km float64
fee str
rider str
dtype: object
<StringArray>
['60', '?', '120', '40', '90', '110']
Length: 6, dtype: str
<StringArray>
['R1', 'R2', nan, '?', 'R3']
Length: 5, dtype: strعمود fee نصي بسبب ظهور ?. وتظهر ? أيضاً في عمود السائق — العلامة نفسها مستخدمة في عمودين. أما عمود distance_km فهو بالفعل float64: فجواته عبارة عن خلايا فارغة، وتعرف عليها بانداس تلقائياً. وبما أن العلامة واحدة في كل مكان، فإن استخدام na_values=["?"] عند القراءة يصلح كلا العمودين معاً.
البرنامج الكامل، deliveries_report.py:
import pandas as pd
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
print("Types after reading:")
print(deliveries.dtypes)
print()
print("Missing per column:")
print(deliveries.isna().sum())
print()
print("Incomplete rows:")
print(deliveries[deliveries.isna().any(axis=1)])
print()
deliveries["rider"] = deliveries["rider"].fillna("unassigned")
fee_total = deliveries["fee"].sum()
fee_missing = deliveries["fee"].isna().sum()
print("Fees collected (known):", fee_total)
print("Deliveries with no fee:", fee_missing)
both = deliveries.dropna(subset=["fee", "distance_km"])
per_km = both["fee"].sum() / both["distance_km"].sum()
print("Fee per km:", round(per_km, 2), "from", len(both), "of", len(deliveries), "deliveries")Types after reading:
order_id int64
branch str
distance_km float64
fee float64
rider str
dtype: object
Missing per column:
order_id 0
branch 0
distance_km 2
fee 1
rider 2
dtype: int64
Incomplete rows:
order_id branch distance_km fee rider
1 2002 north NaN 60.0 R2
2 2003 south 8.0 NaN R1
3 2004 east 12.0 120.0 NaN
5 2006 south 6.5 90.0 NaN
6 2007 east NaN 110.0 R3
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesتحقق من الأرقام يدوياً مرة واحدة، فهكذا تبني الثقة في المجاميع بعد تنظيفها. الرسوم المعلومة: 60 + 60 + 120 + 40 + 90 + 110 + 60 = 540، من سبع توصيلات — فالطلب 2003 ليس له رسوم. وبالنسبة للنسبة، فإن dropna(subset=["fee", "distance_km"]) تحذف الطلب 2003 (لا رسوم له) والطلبين 2002 و 2007 (لا مسافة لهما). اسأل بانداس عن الصفوف المتبقية ومجموعها بدلاً من الاعتماد على الحساب الذهني:
print(both["order_id"].tolist())
print(both["fee"].sum(), both["distance_km"].sum())[2001, 2004, 2005, 2006, 2008]
370.0 28.0370 / 28.0 = 13.21، وهو الرقم الذي طبعه البرنامج تماماً. ولاحظ ما لم تفعله النسبة: لم تقسم الـ 540 الناتجة عن سبع توصيلات على مسافات ست توصيلات. فكلا الرقمين مأخوذان من الصفوف الخمسة نفسها، وهي الطريقة الوحيدة التي تجعل لحساب "الرسوم لكل كيلومتر" أي معنى سليم.
هناك قراران آخران جديران بالملاحظة. تم ملء السائق rider قبل الحساب، لكنه لم يؤثر على الناتج مطلقاً — لأن subset= نظر فقط إلى fee و distance_km. كما كُتب على مجموع الرسوم وصف "(known)"، مع طباعة عدد الرسوم المفقودة تحته مباشرة، حتى لا يظن أحد أن 540 تمثل كل ما تم تحصيله.
التجربة. يؤدي استخدام dropna() بدون وسائط إلى حذف الطلبين 2004 و 2006 أيضاً — واللذين كانت رسومهما ومسافاتهما سليمة تماماً — وذلك لمجرد أن السائق كان مفقوداً. إذا قمت بتشغيله على الجدول قبل ملء rider:
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
strict = deliveries.dropna()
print(strict["order_id"].tolist())
print(round(strict["fee"].sum() / strict["distance_km"].sum(), 2))[2001, 2005, 2008]
16.84تتبقى ثلاث توصيلات فقط، وتقفز النسبة من 13.21 إلى 16.84 — ليس بسبب تغير أي رسوم، بل لأن أطول رحلتين (12 و 6.5 كم)، واللتين تصادف أنهما أرخص لكل كيلومتر، تم استبعادهما بسبب اسم مفقود. ليس لعمود السائق أي علاقة بالسؤال المطروح، لذا يجب ألا يقرر أي الصفوف تُحسب. وهذا هو الغرض الأساسي من استخدام subset=.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz