تصفية الصفوف — طرح السؤال نفسه على كل صف
الاحتفاظ بالصفوف التي تطابق الشرط فقط: الأقنعة المنطقية (Boolean masks)، والربط باستخدام & و | و ~، ودوال isin و between وطرق .str، وتصفية الصفوف عبر .loc — وتعديل تلك الصفوف بأمان دون الوقوع في خطأ الإسناد المتسلسل.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
أرسل مدير المتجر رسالة يطلب فيها: "ما هي الطلبات الواردة من فرع الشمال (north branch) التي تجاوزت ثلاث قطع؟ أريد الاطلاع عليها."
إذا كان لديك ثمانية طلبات فقط كما في الفصل السابق، لكان بإمكانك الإجابة بمجرد النظر: مرر إصبعك على عمود branch، وتوقف عند كل north، ثم القِ نظرة سريعة على عمود quantity. ثلاثة صفوف فقط. انتهى الأمر.
لكن تخيل الآن الملف الحقيقي في بيئة العمل — اثنا عشر ألف طلب على مدار عام كامل. هنا يعجز النظر البشري، وأول فكرة تخطر ببال معظم مبرمجي بايثون هي استخدام حلقة تكرار (loop):
import pandas as pd
orders = pd.read_csv("orders.csv")
keep = []
for i in range(len(orders)):
row = orders.iloc[i]
if row["branch"] == "north" and row["quantity"] > 3:
keep.append(int(row["order_id"]))
print(keep)[1001, 1005, 1008]النتيجة صحيحة تماماً. لكن انظر إلى التكلفة البرمجية الباهظة: عداد، وقائمة فارغة، وشرط if، وعملية append، وتفكيك الجدول صفاً تلو الآخر. والأسوأ من ذلك أن النتيجة لم تعد جدولاً — بل أصبحت مجرد قائمة بأرقام الطلبات. فإذا أردت أن تعرض للمدير أسماء المنتجات وأسعارها أيضاً، فستضطر لكتابة حلقة تكرار أخرى. كما أن هذا الكود شديد البطء عند تطبيقه على اثني عشر ألف صف، لأن كل استدعاء orders.iloc[i] ينشئ كائن Series جديداً بالكامل لمجرد قراءة قيمتين منه.
توفر مكتبة بانداس طريقة مختلفة تماماً لطرح هذا السؤال، وهي المهمة الأكثر شيوعاً التي ستؤديها مع DataFrames: اطرح السؤال نفسه الذي يحتمل الإجابة بـ نعم/لا على كل الصفوف دفعة واحدة، ثم احتفظ بالصفوف التي كانت إجابتها نعم. هذه هي التصفية (filtering)، وهذا هو موضوع هذا الفصل بأكمله. تتحول حلقة التكرار السابقة إلى سطر برمجي واحد، وتبقى النتيجة جدولاً متكاملاً، وتُقرأ الشيفرة وكأنها الجملة التي قالها المدير تماماً.
بنهاية هذا الفصل ستكون قادراً على
- تحويل أي سؤال إلى قناع منطقي (boolean mask) — عمود من قيم
True/False، بمعدل قيمة لكل صف — واستخدامه للاحتفاظ بالصفوف، أو لعدّها وحساب نسبتها عبرmask.sum()وmask.mean() - دمج الشروط باستخدام المعاملات
&و|و~، وتفسير سبب كون الأقواس إلزامية تماماً - استخدام دوال
isinوbetweenوطرق.strللتعامل مع القوائم والنطاقات والأنماط النصية - تصفية الصفوف واختيار الأعمدة في خطوة واحدة باستخدام
.loc[mask, columns] - تعديل القيم في الصفوف المصفاة بأمان عبر
.loc[mask, column] = value، والتعرف على خطأChainedAssignmentErrorالذي ينتج عن الطريقة غير الآمنة في بانداس 3
المتطلبات المسبقة: اختيار الأعمدة والصفوف.
أنشئ الملف أولاً
يستخدم هذا الفصل ملف orders.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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0قبل أن تكتب الكود
التصفية هي سؤال مكتوب بلغة البرمجة. معظم أخطاء التصفية ليست أخطاء في مكتبة بانداس — بل هي أسئلة لم تُحدد بدقة، أو أعمدة تحتوي على قيم غير متوقعة. لذا، قبل كتابة أول سطر من كود التصفية، قم بهذه الخطوات الخمس:
1. قسّم السؤال إلى شروط مستقلة، شرط واحد لكل عمود. حلل عبارة المدير إلى أجزائها:
- "الطلبات الواردة من فرع الشمال" ← العمود
branch، المقارنة يساوي، القيمة"north" - "تجاوزت ثلاث قطع" ← العمود
quantity، المقارنة أكبر من، القيمة3 - "أريد الاطلاع عليها" ← لا يوجد اختبار؛ احتفظ بالصف بأكمله
يفرض عليك هذا التقسيم اتخاذ قرارين حاسمين: أولاً، عبارة "تجاوزت ثلاثة" تعني > 3 وليست >= 3 — أي أن الطلب الذي يحتوي على ثلاث قطع تحديداً لا يُحسب. ثانياً، الشرطان مرتبطان بحرف العطف و (and): يجب أن يجتاز الطلب كلا الاختبارين معاً. هذان القراران هما جوهر التصفية بأكمله؛ وما تبقى هو مجرد كتابة الكود.
2. افحص المدخلات — الأبعاد (shape)، والأنواع (dtypes)، والقيم الفعلية التي ستقارن بها.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: objectالعمود quantity من النوع int64، لذا فإن المقارنة > 3 ستقارن أرقاماً حسابية. لو كان قد قُرئ كـ str — بسبب كتابة أحدهم "3 pcs" في إحدى الخلايا — لفشلت المقارنة بخطأ TypeError، ومن الضروري معرفة ذلك الآن قبل البدء بالعمل. أما العمود branch فهو str، وستتم مقارنته كنص، ومقارنة النصوص دقيقة وحرفية: فالقيم "north" و "North" و "north " تُعتبر ثلاث قيم مختلفة تماماً. لذا، اطلع على القيم الموجودة فعلياً:
print(orders["branch"].unique())<StringArray>
['north', 'south', 'east']
Length: 3, dtype: strثلاثة فروع، مكتوبة بصيغة موحدة دون مسافات زائدة. الآن تضمن أن القيمة "north" في الكود ستطابق البيانات بشكل مؤكد.
3. تخيل شكل المخرجات قبل توليدها. في الملفات الصغيرة، أجب عن السؤال يدوياً أولاً. طلبات فرع الشمال هي: 1001 (12 قطعة)، و 1003 (قطعتان)، و 1005 (30 قطعة)، و 1008 (4 قطع). الطلبات التي تتجاوز ثلاث قطع هي: 1001 و 1005 و 1008. إذن النتيجة المتوقعة هي 3 صفوف، بجميع الأعمدة السبعة، مع معرفة أرقام الطلبات مسبقاً. فإذا أعاد الكود لاحقاً 4 صفوف أو 0 صفوف، ستعرف فوراً أن الكود خاطئ بدلاً من تصديق نتيجته دون وعي. لا يمكنك فعل هذا يدوياً في الملفات الضخمة بالطبع، لكن يمكنك وضع تقدير منطقي: "يمثل فرع الشمال حوالي نصف الطلبات، لذا يجب أن تكون النتيجة أقل بكثير من نصف عدد الصفوف الإجمالي".
4. اختر الأداة المناسبة وفقاً لطبيعة الشرط.
- مقارنة واحدة ← قناع منطقي:
df["col"] > value - شروط متعددة ← أقنعة مدمجة بواسطة
&(and)، و|(or)، و~(not) - "واحدة من هذه القيم" ←
df["col"].isin([...]) - "بين قيمتين" ←
df["col"].between(low, high) - نمط داخل نص ←
df["col"].str.contains(...)، و.str.startswith(...) - تصفية صفوف و اختيار أعمدة محددة ←
df.loc[mask, ["col1", "col2"]] - تعديل القيم في تلك الصفوف ←
df.loc[mask, "col"] = new_value
سؤال المدير عبارة عن مقارنتين مرتبطتين بـ and، إذن نحتاج: قناعين مدمجين بواسطة &.
5. حدد كيف ستتحقق من صحة الإجابة. بعد التصفية، هناك ثلاثة اختبارات سريعة تكشف معظم الأخطاء: تطابق عدد الصفوف مع تقديرك الأولي؛ وتحقق الشرط في كل صف متبقٍ ((result["quantity"] > 3).all() يجب أن تُرجع True)؛ وعدم اختفاء أي صف متوقع (مثل وجود الطلب 1008 في النتيجة).
والآن إلى الكود — خطوة تلو الأخرى.
المقارنة تُنتج قناعاً منطقياً
عندما تقارن عموداً بقيمة معينة، لا تمنحك بانداس قيمة مفردة True أو False؛ بل تقارن كل صف على حدة وتمنحك عموداً كاملاً من الإجابات:
big = orders["quantity"] > 5
print(big)0 True
1 False
2 False
3 True
4 True
5 False
6 True
7 False
Name: quantity, dtype: boolهذا كائن Series من النوع bool — ويُسمى قناعاً منطقياً (boolean mask). يمتلك نفس الفهرس لجدول orders (0 إلى 7)، ويحتوي على إجابة لكل صف: الصف 0 فيه 12 قطعة، إذن True؛ والصف 1 فيه 5 قطع، والعدد 5 ليس أكبر من 5، إذن False.
لماذا تعمل بانداس بهذه الطريقة بدلاً من إجبارك على كتابة حلقة تكرار؟ لأن العمود يُخزن في الذاكرة ككتلة متصلة من الأرقام من نوع بيانات واحد، ومقارنة كتلة كاملة بالقيمة 5 هي عملية واحدة تُنفذ عبر كود C مجمع وفائق السرعة (vectorized operation). حلقتك السابقة طلبت من بايثون فحص كل صف على حدة ثماني مرات — أو اثني عشر ألف مرة. أما القناع فيسأل السؤال مرة واحدة للجميع.
تعمل جميع معاملات المقارنة بالطريقة نفسها: ==، !=، <، <=، >، >=. كما تعمل على الأعمدة النصية أيضاً — التعبير orders["branch"] == "north" يُنتج قناعاً يحتوي على True في الصفوف 0 و 2 و 4 و 7.
حتى الآن، لم تتم تصفية أي شيء. فالقناع يمثل الإجابات فقط. والخطوة التالية هي استخدامها.
القناع يحتفظ بالصفوف
ضع القناع المنطقي داخل أقواس مربعة، وسيقوم بانداس بالاحتفاظ بالصفوف التي يقابلها True في القناع:
print(orders[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
3 1004 2024-03-02 east bottle accessories 7 120.0
4 1005 2024-03-03 north eraser stationery 30 8.0
6 1007 2024-03-04 east pen stationery 20 15.0أربعة صفوف من أصل ثمانية. انظر إلى الفهرس على اليسار: 0, 3, 4, 6. تحتفظ الصفوف المتبقية بتسمياتها (labels) الأصلية. فالتصفية لا تعيد ترقيم الصفوف إطلاقاً — فالصف 3 يظل الصف 3، وهو نفس الطلب 1004 الذي كان في الجدول الأصلي. وهذه الميزة بالغة الأهمية وسنعود إليها لاحقاً.
كيف تطابق بانداس القناع مع صفوف الجدول؟ ليس حسب الموضع — بل عبر تسمية الفهرس (index label). التسمية 0 في القناع تقرر مصير الصف 0، والتسمية 3 تقرر مصير الصف 3. هنا الفهرسان متطابقان تماماً لأن القناع بُني من جدول orders نفسه، لذا لا يظهر أي اختلاف. ويظهر ذلك فقط إذا استخدمت قناعاً مأخوذاً من جدول مختلف — وهو ما سنناقشه في قسم "عندما تفشل الشيفرة".
غالباً ما ستشاهد القناع مكتوباً مباشرة داخل الأقواس دون حفظه في متغير:
print(orders[orders["branch"] == "north"])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
2 1003 2024-03-02 north bag accessories 2 850.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0اقرأ التعبير من الداخل إلى الخارج: الجزء الداخلي `orders["branch"] == "north"` يبني القناع، بينما الجزء الخارجي `orders[ … ]` يحتفظ بالصفوف التي قيمتها True. وتسمية القناع في متغير منفصل (big = …) تكون مفيدة عندما يكون الشرط طويلاً أو عند استخدامه عدة مرات؛ أما للشروط البسيطة فالكتابة المباشرة ممتازة.
العد بواسطة القناع دون تصفية
في كثير من الأحيان، لا يحتاج المدير إلى رؤية الصفوف نفسها، بل يريد فقط معرفة العدد: كم عدد الطلبات التي تجاوزت خمس قطع؟ لا تحتاج إلى تصفية الجدول لمعرفة ذلك. في الحسابات البرمجية، تُعامل القيمة True كـ 1، والقيمة False كـ 0، لذا:
print(big.sum())
print(big.mean())4
0.5تقوم sum() بجمع الآحاد: 4 طلبات. وتقوم mean() بالقسمة على إجمالي عدد الصفوف: 4 من 8 تعطي 0.5، أي أن نصف الطلبات تجاوزت خمس قطع. لمعرفة "كم العدد"، استخدم .sum() على القناع؛ ولمعرفة "ما النسبة"، استخدم .mean() على القناع. كلاهما سطر واحد ولا ينشئ أي منهما جدولاً جديداً في الذاكرة.
وعندما تريد حساب مجموع داخل الصفوف المصفاة — مثل إجمالي عدد القطع في تلك الطلبات الكبيرة — قم بالتصفية أولاً، ثم حدد العمود، ثم احسب المجموع:
print(orders[big]["quantity"].sum())6912 + 7 + 30 + 20 = 69. قراءة عمود من جدول مصفى بهذه الطريقة أمر آمن تماماً. أما الكتابة والتعديل بهذه الطريقة فمحفوف بالمخاطر — وهو ما سنشرحه في القسم الأخير من هذا الفصل.
شروط متعددة: & و | و ~
يتضمن سؤال المدير شرطين. تدمج بانداس الأقنعة المنطقية باستخدام ثلاثة معاملات:
- المعامل
&يعني و (and) — يُحتفظ بالصف عندما يكون كلا القناعينTrue - المعامل
|يعني أو (or) — يُحتفظ بالصف عندما يكون قناع واحد على الأقلTrue - المعامل
~يعني ليس (not) — يعكس كلTrueإلىFalseوالعكس صحيح
إليك حل سؤال المدير أخيراً:
north_big = orders[(orders["branch"] == "north") & (orders["quantity"] > 3)]
print(north_big)order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0ثلاثة صفوف: 1001 و 1005 و 1008 — تماماً كما توقعنا في خطوة التخطيط. والآن لنجرِ اختبارات التحقق من الخطوة 5:
print(len(north_big))
print((north_big["quantity"] > 3).all())
print((north_big["branch"] == "north").all())3
True
Trueتطابق العدد، وتحقق كلا الشرطين في كل صف متبقٍ. الدالة .all() تسأل: "هل كل قيمة في هذا القناع هي True؟" — وهي أسرع طريقة للتأكد من أن الفلتر أدى المطلوب بدقة.
مثال على سؤال يستخدم أو (or): طلبات فرع الشرق، أو أي طلب لأي منتج بسعر 850 أو أكثر.
print(orders[(orders["branch"] == "east") | (orders["price"] >= 850)])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0أربعة صفوف: طلبا فرع الشرق، وطلبا الحقائب من فرعي الشمال والجنوب. وإذا طابق أي صف كلا الشرطين (لا يوجد هنا)، فسيظهر مرة واحدة فقط وليس مرتين — لأن الفلتر يحتفظ بالصف أو يستبعده فقط.
مثال على سؤال يستخدم النفي (not): كل المنتجات ما عدا الأدوات المكتبية (stationery).
print(orders[~(orders["category"] == "stationery")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0يقوم ~ بتحويل كل True إلى False والعكس. للمقارنة الفردية البسيطة، يمكنك كتابة != بدلاً من ذلك، وهو أبسط؛ وتتجلى قوة ~ الحقيقية عندما تنفي شرطاً مركباً أو دالة isin (كما سنرى قريباً).
لماذا تعتبر الأقواس إلزامية
كل شرط في الأمثلة السابقة محاط بقوسين مستقلين. هذا ليس مجرد تفضيل أسلوبي؛ فإذا حذفت الأقواس، سيتعطل الكود تماماً:
print(orders[orders["quantity"] > 3 & orders["price"] < 100])TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool]ما تراه أعلاه هو السطر الأخير من تتبع الخطأ (traceback)، وهو السطر الذي يحدد اسم الخطأ. والسبب في هذا الخطأ هو أسبقية المعاملات (operator precedence) — أي الترتيب الذي ينفذ به بايثون العمليات. في بايثون، يمتلك المعامل & أسبقية أعلى من معاملات المقارنة > و <. وقد وُضع هذا الترتيب قبل ظهور بانداس بوقت طويل، لأن & صُمم أصلاً للعمليات الثنائية على الأعداد الصحيحة (bitwise and)، والتي يجب أن تسبق المقارنات. لذلك يقرأ بايثون السطر على هذا النحو:
orders["quantity"] > (3 & orders["price"]) < 100يحاول بايثون حساب 3 & orders["price"] أولاً — وهي عملية bitwise and بين الرقم 3 وعمود من الأعداد العشرية — وهذا هو سبب الخطأ. وتذكر الرسالة rand_ (وهي عملية and للطرف الأيمن) ومصفوفة، ولكن السبب الحقيقي دائماً هو: نسيان الأقواس.
ظهور رسالة الخطأ هو الجانب الإيجابي. أما إذا وضعت أقواساً حول الشرط الأول فقط ونسيت الثاني، فلن يظهر أي خطأ إطلاقاً — وهنا تكمن الكارثة الحقيقية:
print(len(orders[(orders["quantity"] > 3) & orders["price"] < 100]))
print(len(orders[(orders["quantity"] > 3) & (orders["price"] < 100)]))8
4يقرأ بايثون السطر الأول هكذا: ((quantity > 3) & price) < 100. يقوم المعامل & بدمج القناع مع الأسعار، وتصادف أن جميع النتائج أقل من 100، فيحتفظ هذا "الفلتر" بجميع الصفوف الثمانية! بينما الفلتر الصحيح يحتفظ بأربعة صفوف فقط. الأعمدة نفسها، والأرقام نفسها، وفارق زوج واحد فقط من الأقواس — والنسخة الخاطئة تعمل دون أي تنبيه وتُخرج بيانات مغلوطة. احرص على إحاطة كل شرط بأقواسه الخاصة في كل مرة. وحينها لن تقع في فخ الخطأ البرمجي ولا النتيجة المضللة.
لماذا لا نستخدم كلمة and؟
من الطبيعي أن يفكر المبرمج في كتابة الكلمة الإنجليزية المعتادة:
print(orders[(orders["branch"] == "north") and (orders["quantity"] > 3)])ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().تتطلب كلمات بايثون المحجوزة and و or و not قيمة منطقية واحدة True أو False على كل جانب — لأنها صُممت لجمل if الشرطية. لاستخدام and، يسأل بايثون القناع الأول: "هل قيمتك صحيحة؟"، ولا يمكن لعمود يضم ثماني إجابات مختلفة أن يرد بإجابة واحدة. هل هو صحيح إذا كانت أي قيمة فيه True؟ أم إذا كانت جميعها كذلك؟ يرفض بانداس التخمين، وتسرد رسالة الخطأ الطرق التي يمكنك الاختيار من بينها (a.any() أو a.all()). لكنك لم تكن تطلب إجابة مفردة؛ بل أردت عملية "and" صفاً بصف. وتلك هي وظيفة &. القاعدة الذهبية: داخل عمليات التصفية، تتحول and/or/not دائماً إلى &/|/~.
واحدة من عدة قيم: isin
الطلبات الواردة من فرعي الجنوب أو الشرق. يمكنك كتابة مقارنتين متساويتين ودمجهما بـ |. ولكن مع وجود خمسة فروع، ستتحول إلى خمس مقارنات متتالية. تأخذ الدالة isin قائمة وتسأل: "هل قيمة هذا الصف موجودة داخل القائمة؟":
print(orders[orders["branch"].isin(["east", "south"])])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0وهنا تبرز الفائدة الكبيرة للمعامل ~ — لتحديد جميع الفروع ما عدا الشمال والشرق، نضع ~ مباشرة أمام isin:
print(orders[~orders["branch"].isin(["north", "east"])]["order_id"].tolist())[1002, 1006]طلبان، كلاهما من فرع الجنوب. لاحظ أن التعبير ~orders["branch"].isin([...]) لا يحتاج إلى أقواس إضافية: لأن ~ ينطبق مباشرة على ناتج استدعاء الدالة، ولا يوجد & أو | ليتنازع معها في الأسبقية. ولكن بمجرد دمجه مع شرط آخر، تعود الأقواس للظهور: (~orders["branch"].isin([...])) & (orders["quantity"] > 3).
نطاق من القيم: between
الطلبات التي يتراوح سعرها بين 15 و 120. يتطلب هذا مقارنتين: >= 15 و <= 120. تتيح لك دالة between التعبير عن ذلك بكلمة واحدة:
print(orders[orders["price"].between(15, 120)])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0تشمل الدالة between كلا الطرفين افتراضياً — فالأقلام بسعر 15.0 والزجاجات بسعر 120.0 مشمولة في النتيجة. وهذا يطابق الفهم العام لعبارة "من 15 إلى 120"، ولكن يجب التأكد من نص المسألة. إذا كان المطلوب حصراً ما يقع بينهما بدقة واستبعاد الأطراف، فحدد ذلك صراحة:
print(orders["price"].between(15, 120, inclusive="neither").sum())1الدفتر فقط بسعر 60.0 يقع بين القيمتين بدقة. وتقبل المعلمة inclusive أيضاً الخيارات "left" و "right" للنطاقات شبه المفتوحة — وهو الخيار الشائع للشرائح المالية مثل 0–100 و 100–200 لتجنب وقوع القيمة الحدية في شريحتين معاً.
الأنماط النصية: دوال .str
تتطلب المقارنة العادية للنصوص تطابقاً تاماً. يوفر الموصّل .str لعمود النصوص دوال السلاسل النصية القياسية في بايثون، ويطبقها على كل صف، وتعيد كل منها قناعاً منطقياً.
المنتجات التي يبدأ اسمها بحرف "b":
print(orders[orders["product"].str.startswith("b")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0المنتجات التي تحتوي على حرف "o"، سواء كان كبيراً أو صغيراً:
print(orders[orders["product"].str.contains("O", case=False)])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
7 1008 2024-03-04 north bottle accessories 4 120.0لا يوجد حرف O كبير في البيانات، ومع ذلك تم العثور على الدفتر والزجاجتين لأن case=False تتجاهل حالة الأحرف. وبدونها، لم تكن .str.contains("O") لتجد أي نتيجة.
أما المقارنة التامة (==) فلا تحتوي على مثل هذا الخيار. وهنا يقع الخطأ الأكثر شيوعاً الذي يجعل المبرمج يتساءل "لماذا لا يُرجع الفلتر أي صفوف؟":
print(orders[orders["branch"] == "North"])Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []لا يوجد خطأ برمجي، ولا توجد صفوف أيضاً. الجدول سليم وبانداس سليم — المشكلة ببساطة أنه لا يوجد فرع كُتب بحرف كبير "North". لهذا السبب قمنا في الخطوة 2 بطباعة unique(): يجب دائماً نسخ القيم التي تقارن بها من محتوى العمود الفعلي وليس كتابتها من الذاكرة.
التواريخ النصية تُقارن بشكل صحيح — إذا كانت بتنسيق ISO
قُرئ عمود date كنص str. فهل يمكنك تصفيته باستخدام >=؟ الطلبات من 3 مارس فصاعداً:
print(orders["date"] >= "2024-03-03")0 False
1 False
2 False
3 False
4 True
5 True
6 True
7 True
Name: date, dtype: boolهذا يعمل بالفعل، ولكن من المهم فهم السبب. هذه نصوص، لذا فإن >= تقارنها كنصوص أبجدية، حرفاً بحرف، تماماً كما تُرتّب الكلمات في القاموس. وقد أعطى ذلك نتيجة صحيحة فقط لأن التواريخ كُتبت بتنسيق السنة-الشهر-اليوم مع أصفار ملء الخانات (2024-03-03)، مما جعل الترتيب الأبجدي يطابق الترتيب الزمني صدفة. لو كُتب العمود بصيغة 3/3/2024، لأعطت المقارنة نتائج خاطئة — لأن "10/1/2024" ستأتي قبل "9/1/2024" نظراً لأن الحرف "1" يسبق الحرف "9". ولأي عمليات تتجاوز المقارنات البسيطة، يجب تحويل التواريخ إلى تواريخ فعلية عبر pd.to_datetime، وهو ما سنتناوله في فصل إضافة الأعمدة.
الصفوف والأعمدة معاً: .loc[mask, columns]
احتفظت جميع الفلاتر السابقة بكل الأعمدة. لكن المدير طلب طلبات فرع الشمال، والتقرير لا يحتاج سوى المنتج والكمية. لقد تعرفت على .loc[rows, columns] في الفصل السابق باستخدام التسميات. يقبل جزء الصفوف أيضاً قناعاً منطقياً (mask):
print(orders.loc[orders["branch"] == "north", ["product", "quantity"]])product quantity
0 pen 12
2 bag 2
4 eraser 30
7 bottle 4خطوة واحدة، وكائن واحد، ويستطيع أي شخص يقرأ كودك أن يرى السؤال كاملاً — أي الصفوف، وأي الأعمدة — في سطر واحد. وإذا مررت اسم عمود مفرد بدلاً من قائمة .loc[mask, "quantity"]، فستحصل على كائن Series جاهز للعمليات الحسابية:
print(orders.loc[orders["branch"] == "north", "quantity"].sum())4812 + 2 + 30 + 4 = 48 قطعة بيعت في فرع الشمال.
query(): الفلتر نفسه على هيئة جملة مقروءة
هناك صيغة ثانية لكتابة الفلاتر وهي دالة query()، والتي تستقبل الشرط كنص:
print(orders.query("branch == 'north' and quantity > 3"))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0الصفوف الثلاثة نفسها تماماً. داخل النص، يمكنك كتابة أسماء الأعمدة مباشرة، ويُسمح باستخدام and و or و not، لأن query تحلل النص برمجياً بنفسها ولا تسلمه لمعاملات بايثون. وإذا أردت استخدام متغير بايثون، فضع قبله الرمز @:
min_qty = 5
print(orders.query("quantity >= @min_qty")["order_id"].tolist())[1001, 1002, 1004, 1005, 1007]تتميز query بوضوح قراءتها للشروط الطويلة. ولكن لها عيوبها: فالأخطاء الإملائية داخل النص لا تُكتشف إلا عند تشغيل السطر، ومحرر الكود لا يقدم إكمالاً تلقائياً داخله، وأسماء الأعمدة التي تحتوي على مسافات تتطلب استخدام علامات الاقتباس المائلة (backticks). تعتمد هذه الدورة على الأقنعة المنطقية كأداة أساسية لأن كل شيء آخر في بانداس — مثل .loc والإسناد والحسابات — مبني عليها. ولكن يجب أن تعرف query لتتمكن من قراءة أكواد الآخرين.
الجدول المصفى يحتفظ بتسمياته القديمة
بالعودة إلى الفهرس: احفظ طلبات فرع الشمال وافحص فهرسها:
north = orders[orders["branch"] == "north"]
print(north.index.tolist())[0, 2, 4, 7]التسميات هي 0, 2, 4, 7. فماذا يحدث عند كتابة north.loc[1]؟ لا توجد تسمية 1 في هذا الجدول — فالطلب 1002 كان من فرع الجنوب واستُبعد بالتصفية:
print(north.loc[1])KeyError: 1تبحث .loc عن تسمية (label)، والتسمية 1 غير موجودة هنا. وإذا كنت تقصد "الطلب الثاني في فرع الشمال"، فهذا موضع ترتيبي (position)، والمواضع يتم الوصول إليها عبر .iloc:
print(north.iloc[1])order_id 1003
date 2024-03-02
branch north
product bag
category accessories
quantity 2
price 850.0
Name: 2, dtype: objectالصف الثاني حسب الموضع هو طلب الحقيبة — وقيمة Name الخاصة به هي 2، وهي تسميته الأصلية في الجدول الأول. هذا هو التمييز الدقيق بين loc و iloc من الفصل السابق، وتظهر أهميته بوضوح بعد التصفية لأن التسميات والمواضع تتوقف عن التطابق.
الاحتفاظ بالتسميات الأصلية مقصود تماماً في تصميم بانداس: فهو يتيح لك تتبع أي صف في النتيجة وصولاً إلى الجدول الأصلي. وعندما تنتهي من ذلك — كأن يكون الجدول المصفى هو التقرير النهائي — أعد ترقيمه:
print(north.reset_index(drop=True))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1003 2024-03-02 north bag accessories 2 850.0
2 1005 2024-03-03 north eraser stationery 30 8.0
3 1008 2024-03-04 north bottle accessories 4 120.0يتخلص drop=True من التسميات القديمة. ودونها، ستُحفظ التسميات كعمود جديد باسم index، وهو ما لا تريده عادة في التقارير.
النتيجة الفارغة ليست خطأً برمجياً
none = orders[orders["branch"] == "west"]
print(none)
print(none.shape)
print(none.empty)Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []
(0, 7)
Trueلا يوجد فرع باسم الغرب، لذا لا توجد صفوف — ولا يوجد أي خطأ برمجي. هذا سلوك صحيح تماماً (فالصفر إجابة مقبولة)، ولكنه يعني أن الفلتر الذي يحتوي على خطأ إملائي سيفشل بصمت. فالمجموع على جدول فارغ يكون 0 والمتوسط يكون nan، وقد تنتقل هذه القيم لمسافات طويلة داخل التقرير قبل أن يلاحظها أحد. عندما تعتمد خطوة هامة على نتيجة التصفية، تحقق من result.empty أو عدد الصفوف، وأوضح ذلك صراحة في المخرجات: كتابة "No orders from the west branch." أفضل بكثير من طباعة جدول فارغ.
تعديل الصفوف المصفاة: .loc[mask, column] = value
في بعض الأحيان، نقوم بالتصفية بهدف التعديل. كانت أسعار الأقلام المباعة في فرع الشرق خاطئة؛ وكان ينبغي أن تكون 13.5 بدلاً من 15.0. الكود المغري هنا هو تصفية الجدول ثم اختيار العمود — أي زوجان من الأقواس المربعة، تماماً كما فعلنا عند قراءة القيم:
import pandas as pd
orders = pd.read_csv("orders.csv")
orders[orders["branch"] == "east"]["price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment 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).
Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html#chained-assignmentانظر إلى الأسعار: لم يتغير أي شيء. وقد أصدر بانداس تحذيراً مفصلاً. (يظهر هذا التحذير في الطرفية عبر stderr قبل القائمة، ولكننا نعرضه هنا بعد المخرجات للتوضيح). يُعرف هذا بـ الإسناد المتسلسل (chained assignment) — خطوتان متتاليتان للفهرسة: orders[mask] ثم ["price"]، مع وجود علامة التعيين في نهاية السلسلة. الخطوة الأولى orders[mask] تنشئ جدولاً جديداً مؤقتاً يحتوي على صفوف فرع الشرق. والخطوة الثانية تعدل الأسعار في ذلك الجدول المؤقت الجديد. ثم يختفي هذا الجدول المؤقت من الذاكرة لأنه لم يُحفظ في أي متغير. ولم يتأثر جدول orders الأصلي على الإطلاق.
في إصدار بانداس 3، أصبح هذا السلوك مضموناً وثابتاً تحت آلية تُعرف بـ النسخ عند الكتابة (Copy-on-Write): أي جدول تحصل عليه عبر الفهرسة يتصرف كنسخة مستقلة تماماً، وبالتالي فإن التعديل عليه لا يمكن أن يصل إلى الجدول الأصلي. ويُعد ChainedAssignmentError تحذيراً وليس استثناءً يوقف البرنامج — أي أن البرنامج يواصل العمل — وهذا هو مكمن خطورته: حيث ينتهي السكريبت "بنجاح" مع بقاء الأسعار القديمة دون تعديل!
(إذا قرأت دروساً قديمة، فستجد وصفاً لهذه الحالة عبر تحذير SettingWithCopyWarning مع ملاحظة تفيد بأنه "قد يعمل أو لا يعمل". أما في بانداس 3 فالقاعدة حاسمة: لا يعمل أبداً، فلا تكتبه إطلاقاً).
الحل هو اتباع نصيحة التحذير نفسه: خطوة فهرسة واحدة فقط عبر .loc[rows, column] مع وضع التعيين عليها مباشرة:
orders.loc[orders["branch"] == "east", "price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 13.5, 8.0, 850.0, 13.5, 120.0]مهلاً — لقد غيّر ذلك سعر كلا طلبي فرع الشرق، القلم والزجاجة! ولم تكن الزجاجة مسعرة بالخطأ. لقد نفذ الكود ما طُلب منه بدقة؛ ولكن السؤال المكتوب في الكود كان غير دقيق. وهنا تتجلى أهمية الخطوة 1 من خطتنا: "الأقلام المباعة في فرع الشرق" تتكون من شرطين، بينما كتبنا شرطاً واحداً فقط. لنبدأ من جديد بنسخة نظيفة ونكتب كلا الشرطين:
import pandas as pd
orders = pd.read_csv("orders.csv")
fix = (orders["branch"] == "east") & (orders["product"] == "pen")
print(fix.sum())
orders.loc[fix, "price"] = 13.5
print(orders.loc[orders["branch"] == "east", ["product", "price"]])1
product price
3 bottle 120.0
6 pen 13.5نرى هنا عادتين برمجيتين ممتازتين: إعطاء القناع اسماً واضحاً (fix) لأنه يُستخدم في عملية هامة؛ وقبل التعديل، التحقق عبر fix.sum() من عدد الصفوف التي ستتغير — صف واحد فقط، وهو ما توقعناه تماماً. احرص دائماً على عد الصفوف قبل الكتابة عليها.
للقراءة، استخدامdf[mask]["col"]سليم تماماً. أما للكتابة، فاستخدم دائماًdf.loc[mask, "col"] = value. الصيغة المتسلسلة لا تُطلق استثناءً يوقف البرنامج، ولكن التعديل يختفي في صمت.
حفظ الجدول المصفى للعمل عليه منفصلاً أمر ممتاز
ماذا عن هذا الكود؟
import pandas as pd
orders = pd.read_csv("orders.csv")
east = orders[orders["branch"] == "east"]
east["price"] = 0.0
print(east["price"].tolist())
print(orders["price"].tolist())[0.0, 0.0]
[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]لا يظهر أي تحذير، وهذا سليم تماماً. لأنك هنا أردت عمداً إنشاء جدول مستقل: أطلقت عليه اسم east، وتعديله يغير جدول east فقط. ويظل جدول orders دون أي مساس. تجعل آلية النسخ عند الكتابة (Copy-on-Write) هذا السلوك متوقعاً وموثوقاً — فالجدول الذي تحصل عليه عبر التصفية يكون دائماً مستقلاً عن الأصل. وتحدث المشكلة فقط في الصيغة المتسلسلة حيث يكون الجدول المؤقت مجهول الاسم ويختفي التعديل معه.
مثال شامل متكامل
طلب المدير تقريراً ملخصاً. report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check what arrived before asking it anything.
print("Rows, columns:", orders.shape)
print("Branches:", list(orders["branch"].unique()))
print()
# 1. The original question: north-branch orders of more than three items.
north_big = orders.loc[
(orders["branch"] == "north") & (orders["quantity"] > 3),
["order_id", "product", "quantity"],
]
print("North orders over 3 items:")
print(north_big.reset_index(drop=True))
print()
# 2. Counting with masks: no new table needed.
accessories = orders["category"] == "accessories"
print("Accessory orders :", accessories.sum())
print("Share of all orders:", accessories.mean())
print("Accessory items :", orders.loc[accessories, "quantity"].sum())
print()
# 3. Outside north, cheap items (under 100).
cheap_outside = orders[
(orders["branch"] != "north") & (orders["price"] < 100)
]
print("Cheap orders outside north:", cheap_outside["order_id"].tolist())
# 4. A branch we do not have: say so instead of printing an empty table.
west = orders[orders["branch"] == "west"]
if west.empty:
print("No orders from the west branch.")Rows, columns: (8, 7)
Branches: ['north', 'south', 'east']
North orders over 3 items:
order_id product quantity
0 1001 pen 12
1 1005 eraser 30
2 1008 bottle 4
Accessory orders : 4
Share of all orders: 0.5
Accessory items : 14
Cheap orders outside north: [1002, 1007]
No orders from the west branch.لماذا كُتب الكود بهذه الطريقة:
- الفحص أولاً ثم التصفية. طُبعت الأبعاد
shapeوقائمة الفروع قبل تطبيق أي فلتر. فلو كان الملف قد وصل بكلمةNorthتبدأ بحرف كبير، لظهر ذلك في سطر الفروع قبل أن يطبع التقرير جدولاً فارغاً. - استخدام
.loc[mask, columns]للسؤال الأساسي. تم تحديد الصفوف والأعمدة المطلوبة في مكان واحد، واستُخدمتreset_index(drop=True)لإعادة ترقيم النتيجة لأن هذا هو التقرير النهائي. - قناع مسمى، استُخدم ثلاث مرات. تم حساب القناع
accessoriesمرة واحدة ثم استُخدم للعد (sum)، وحساب النسبة (mean)، واختيار عمود للحساب. تسمية القناع تجنبك تكرار كتابة الشرط ثلاث مرات وخطر الخطأ فيه. - إحاطة كل شرط بأقواس مستقلة، حتى في الشروط القصيرة، لتجنب أخطاء الأسبقية عند إضافة شروط لاحقة.
- التعامل مع الحالة الفارغة بوضوح. عدم وجود صفوف إجابة مقبولة، وأوضحها التقرير بنص صريح.
عندما تفشل الشيفرة
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). استخدمت and أو or أو not بين قناعين — أو استخدمت if على قناع كامل. تتطلب كلمات بايثون قيمة واحدة True/False، بينما يحتوي القناع على قيمة لكل صف. داخل الفلتر، اكتب & و | و ~. وداخل جملة if، وضح ما تقصده بدقة: استخدم if mask.any(): لتعني "هل يوجد صف واحد على الأقل صحيح؟"، أو if mask.all(): لتعني "هل كل الصفوف صحيحة؟".
TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool] (أو 'ror_' أو [int64]) نسيت وضع الأقواس حول شرط مجاور لـ & أو |. قام بايثون بتطبيق & قبل >، فحاول تنفيذ عملية bitwise and بين رقم وعمود. ضع كل شرط في أقواسه الخاصة: (a > 3) & (b < 100).
الفلتر يُرجع جدولاً فارغاً دون أي رسالة خطأ القيمة التي تبحث عنها غير موجودة في العمود بالصيغة المكتوبة تماماً. اطبع df["col"].unique() — أو الأفضل من ذلك list(df["col"].unique()) التي تعرض القيم داخل علامات اقتباس، فتظهر أي مسافة زائدة مثل 'north '. نظف البيانات عبر df["col"].str.strip()، أو استخدم المطابقة المرنة عبر .str.contains(..., case=False).
ظهور KeyError: 1 (أو أي رقم آخر) فوراً بعد التصفية استخدمت .loc[n] على الجدول المصفى مفترضاً أن تسمياته أصبحت 0, 1, 2…. تذكر أن الجدول المصفى يحتفظ بتسمياته الأصلية. استخدم .iloc[n] للوصول إلى "الصف ذي الترتيب n"، أو نفذ reset_index(drop=True) أولاً إذا لم تعد بحاجة للتسميات القديمة.
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. كتبت df[mask]["col"] = value. هذا الكود لا يغير df أبداً في بانداس 3 — يخبرك التحذير بأن هذا السطر لم يفعل أي شيء. اكتب بدلاً منه: df.loc[mask, "col"] = value.
UserWarning: Boolean Series key will be reindexed to match DataFrame index. تم إنشاء القناع من جدول مختلف عن الجدول الذي تقوم بتصفيته — مثل استخدام قناع مأخوذ من جدول orders الكامل لتصفية جدول north المصفى مسبقاً. يقوم بانداس بمطابقة الصفوف حسب التسميات ويستبعد التسميات التي لا يجدها بصمت:
import pandas as pd
orders = pd.read_csv("orders.csv")
north = orders[orders["branch"] == "north"]
big = orders["quantity"] > 5
print(north[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
UserWarning: Boolean Series key will be reindexed to match DataFrame index.(يظهر هذا التحذير في الطرفية فوق الجدول). تصادف هنا أن الإجابة صحيحة، ولكن فقط بفضل تطابق التسميات. ابنِ القناع دائماً من الجدول نفسه الذي تصفيه: north[north["quantity"] > 5].
Step 4 of 6 — Predict
Check your understanding
ما الذي سيتم طباعته؟
from io import StringIO
import pandas as pd
# Stands in for orders.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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
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))
mask = orders["quantity"] > 5
print(mask.sum(), len(orders[mask]))- A4 8
- B5 5
- C4 4
- DTrue 4
الهدف من هذا الكود هو الاحتفاظ بطلبات فرع north التي تزيد عن ثلاثة عناصر. ماذا يحدث عند تشغيله؟
from io import StringIO
import pandas as pd
# Stands in for orders.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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
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))
north_big = orders[orders["branch"] == "north" & orders["quantity"] > 3]- Aيعمل بنجاح ويحتفظ بالطلبات 1001 و 1005 و 1008
- Bيُرجع جدولاً فارغاً
- Cخطأ `TypeError`: بدون الأقواس، يتم تطبيق `&` قبل `==` و `>`
- Dخطأ `ValueError: The truth value of a Series is ambiguous`
عليك تخفيض سعر كل حقيبة (bag) إلى النصف داخل جدول orders نفسه. أي سطر يقوم بذلك؟
- Aorders[orders["product"] == "bag"]["price"] = orders["price"] / 2
- Borders.loc[orders["product"] == "bag", "price"] = orders["price"] / 2
- Cbags = orders[orders["product"] == "bag"] bags["price"] = bags["price"] / 2
- Dorders["price"][orders["product"] == "bag"] /= 2
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.
دورك الآن
استخدم نفس ملف orders.csv. اكتب سكريبت filters.py يجيب عن أربعة أسئلة، مع طباعة كل إجابة تحت عنوان قصير:
- كم عدد الطلبات التي بلغت 10 قطع أو أكثر؟ اطبع رقماً فقط، وليس جدولاً.
- طلبات الإكسسوارات خارج فرع الشمال — مع عرض أعمدة
order_idوbranchوproductوpriceفقط، وإعادة الترقيم بدءاً من 0. - الطلبات الواردة من فرع الجنوب أو الشرق وسعرها أقل من 100. استخدم دالة
isinللفروع. - تقدم الإدارة خصماً بنسبة 10% على كل طلب أدوات مكتبية يبلغ 20 قطعة أو أكثر. عدّل عمود
priceلتلك الصفوف تحديداً ليصبح 90% من السعر الحالي، ولكن اطبع أولاً عدد الصفوف التي ستتغير. ثم اطبع أعمدةorder_idوquantityوpriceلجميع طلبات الأدوات المكتبية.
قبل كتابة الكود، خطط لكل سؤال على الورق: أي الأعمدة، وأي الاختبارات، وبماذا ترتبط، وما هي الصفوف المتوقعة بمجرد النظر في الملف. عندما يعمل برنامجك بشكل صحيح، سيطبع هذه المخرجات تحديداً:
1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5ثم جرب تعطيل الكود عمداً في كل حالة لملاحظة النتيجة:
- احذف الأقواس في السؤال 3. ما هو الخطأ الذي يظهر؟
- اكتب السؤال 4 بصيغة
orders[mask]["price"] = .... ماذا يطبع بانداس، وهل تغير أي سعر؟ - غيّر
"east"إلى"East"في السؤال 3. ماذا يحدث، ولماذا يُعد ذلك أسوأ من ظهور خطأ صريح؟
الحل
التخطيط أولاً. تحليل كل سؤال:
quantity >= 10، شرط واحد. بالنظر: 12 و 30 و 20، إذن الناتج 3.category == "accessories"وbranch != "north". بالنظر: 1004 (شرق، زجاجة) و 1006 (جنوب، حقيبة).branchفي الجنوب/الشرق وprice < 100. بالنظر: 1002 (دفتر، 60.0) و 1007 (قلم، 15.0).category == "stationery"وquantity >= 20. بالنظر: 1005 (ممحاة، 30) و 1007 (قلم، 20)، إذن صفان.
يتطلب السؤال 4 انتباهاً خاصاً: عبارة "20 قطعة أو أكثر" تعني >= 20، لذا فإن الطلب 1007 الذي يحتوي على 20 قطعة تماماً مشمول. وبما أنه يقوم بـ كتابة وتعديل بيانات، فإنه يستخدم .loc[mask, "price"] مع قناع مسمى، يتم عد صفوفه أولاً.
filters.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("1. Orders of 10+ items:")
print((orders["quantity"] >= 10).sum())
print()
print("2. Accessory orders outside north:")
outside = orders.loc[
(orders["category"] == "accessories") & (orders["branch"] != "north"),
["order_id", "branch", "product", "price"],
]
print(outside.reset_index(drop=True))
print()
print("3. South or east, price under 100:")
cheap = orders[
(orders["branch"].isin(["south", "east"])) & (orders["price"] < 100)
]
print(cheap)
print()
print("4. Stationery discount:")
discount = (orders["category"] == "stationery") & (orders["quantity"] >= 20)
print("Rows to change:", discount.sum())
orders.loc[discount, "price"] = orders.loc[discount, "price"] * 0.9
print(orders.loc[orders["category"] == "stationery", ["order_id", "quantity", "price"]])1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5تطابقت كل النتائج مع الخطة بدقة. ملاحظات على الخيارات:
- السؤال 1 يستخدم
sum()للقناع، وليسlen(orders[...]). كلاهما يعطي 3، لكن القناع يعد مباشرة ولا ينشئ جدولاً في الذاكرة. - السؤال 2 يستخدم
!=للتعبير عن "خارج فرع الشمال". التعبير~(orders["branch"] == "north")يؤدي نفس الغرض؛ واستخدام!=أبسط لقيمة واحدة. - السؤال 3 يحتفظ بالتسميات الأصلية
1و6، لأن المسألة لم تطلب إعادة الترقيم. تخبرك تلك التسميات بمواضع الصفوف في جدولorders. - السؤال 4 يعد الصفوف قبل التعديل، وتُحسب القيمة الجديدة من الصفوف نفسها على الجانب الأيمن:
orders.loc[discount, "price"] * 0.9. يستخدم كلا الطرفين القناع نفسه، لذا يحصل كل صف على 90% من سعره الخاص: فتتحول 8.0 إلى 7.2، وتتحول 15.0 إلى 13.5. ويحتفظ طلب الأقلام1001(12 قطعة) والدفتر (5 قطع) بأسعارهما دون تغيير.
وتحليل حالات التعطيل الثلاث:
- بدون أقواس، يطلق السؤال 3 خطأ
TypeError: Cannot perform 'rand_' …لأن&طُبقت قبل<. - يؤدي
orders[discount]["price"] = …إلى طباعة تحذيرChainedAssignmentError، ويستمر السكريبت في العمل، ويظل الجدول النهائي يعرض الأسعار القديمة8.0و15.0. لم يفعل هذا السطر أي شيء. - مع استخدام
"East"، يفقد السؤال 3 قلم فرع الشرق بصمت ويطبع صفاً واحداً بدلاً من اثنين. لا يوجد خطأ ولا تحذير: مجرد نتيجة خاطئة تبدو صحيحة ظاهرياً. لهذا السبب قمنا بكتابة الصفوف المتوقعة قبل تشغيل الكود؛ والوحيد القادر على اكتشاف هذا الخطأ هو شخص يعرف مسبقاً أن النتيجة يجب أن تكون صفين.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz