ترتيب الصفوف — وتحديد كيفية التعامل مع حالات التعادل
ترتيب الصفوف باستخدام sort_values: لعمود واحد أو أعمدة متعددة، وباتجاه مستقل لكل عمود، والتعامل مع القيم المفقودة والنصوص الشبيهة بالأرقام — بالإضافة إلى nlargest لأعلى القيم، ولماذا يجب أن يكون التعادل عند حد القطع قراراً مقصوداً لا مجرد صدفة.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
أرسل صاحب المتجر رسالة قصيرة: "ما هي أكبر طلباتنا لهذا الشهر؟ أرسل لي أعلى ثلاثة طلبات."
البيانات هي ملف orders.csv المألوف — ثمانية طلبات من فروع المتجر الثلاثة: north و south و east. مع ثمانية صفوف فقط، يمكنك الإجابة بالنظر المجرد: تفقد عمود quantity، وابحث عن الرقم 30، ثم 20، ثم 12. انتهى الأمر.
والآن تخيل الملف الحقيقي: أربعة آلاف طلب. لم تعد قراءة العمود للعثور على أكبر ثلاثة أرقام طريقة مجدية، بل أصبحت مجرد أمنية مستحيلة. ورسالة صاحب المتجر التالية في طريقها إليك بالفعل: "هل يمكنك إرسال القائمة مجمعة حسب الفرع، بحيث يظهر أكبر طلب أولاً داخل كل فرع؟ واجعل أحدث الطلبات في الأعلى من فضلك."
كل واحد من هذه الطلبات هو في جوهره نفس العملية: وضع الصفوف في ترتيب مفيد، ثم القراءة من الأعلى. هذا هو الترتيب (sorting)، وهو في مكتبة بانداس دالة واحدة: sort_values().
لكن استدعاء الدالة هو الجزء السهل. الأسئلة الحقيقية تحيط بهذه الدالة، وهي السبب وراء الأخطاء الفادحة في التقارير:
- "الأكبر" وفقاً لماذا؟ الكمية (quantity)، أم السعر (price)، أم الإيراد المالي الذي حققه الطلب؟ كل معيار يعطيك ثلاثة طلبات مختلفة تماماً.
- عندما يتعادل طلبان، أيهما يأتي أولاً — وهل تتغير الإجابة في كل مرة تشغل فيها الكود؟
- ماذا يحدث للطلب الذي تكون كميته فارغة؟
- إذا تمت قراءة عمود أرقام كنص بالخطأ، فسيتم ترتيب
"9"بعد"30"، دون أن يظهر لك أي تحذير ينبهك.
يعلمك هذا الفصل استخدام هذه الدالة، والأهم من ذلك، يعلمك كيفية اتخاذ تلك القرارات الحاسمة.
بنهاية هذا الفصل ستكون قادراً على
- ترتيب الجدول وفق عمود واحد أو أعمدة متعددة باستخدام
sort_values()، مع تحديد اتجاه مستقل لكل عمود، وحفظ النتيجة بدلاً من فقدانها - شرح سبب انتقال تسميات الفهرس (index labels) مع صفوفها، والاختيار بثقة بين الإبقاء عليها، أو استخدام
sort_index()، أوreset_index(drop=True) - فك حالات التعادل بصورة مقصودة، وتحديد مكان ظهور القيم المفقودة باستخدام
na_position - الإجابة عن أسئلة "أعلى N" باستخدام
head()وnlargest()وnsmallest()— بما في ذلك "أعلى N ضمن مجموعة فرعية" وحالات التعادل عند حد القطع - ترتيب النصوص دون تأثر بحالة الأحرف (كبيرة أو صغيرة) باستخدام
key=، والتعرف على الأرقام المخزنة كنصوص من طريقة ترتيبها
المتطلبات المسبقة: البيانات المفقودة والقيم الناقصة — اكتشاف الفراغات وحذفها وملؤها.
أنشئ الملف أولاً
تقرأ جميع الأمثلة في هذا الفصل ملف 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يستخدم قسم لاحق ملفاً ثانياً باسم orders_gaps.csv. وهو نفس الجدول مع ترك كميتين فارغتين — للطلبين 1002 و 1007 — وهو نوع الفراغات الذي تعلمت اكتشافه في الفصل السادس:
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,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,,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0قبل أن تكتب الكود
تبدو عملية الترتيب بسيطة للغاية لدرجة قد تجعلك تظن أنها لا تحتاج تخطيطاً مسبقاً. لكن العكس هو الصحيح؛ فالترتيب لا يفشل أبداً — إنه ينتج دائماً ترتيباً ما. فإذا رتبت بناءً على العمود الخطأ، فستبدو النتيجة منسقة ومنظمة تماماً مثل النتيجة الصحيحة. لذا يجب أن يحدث التفكير قبل كتابة الكود.
1. اكتب السؤال في جملة واحدة واضحة، متضمنةً اسم العمود. "أكبر الطلبات" ليس سؤالاً يمكن لبانداس الإجابة عنه مباشرة. بينما "الطلبات الثلاثة ذات أكبر quantity" هو سؤال محدد يمكن حله برمجياً. وكذلك "الطلبات الثلاثة ذات أعلى price". فكلاهما يعطي نتائج مختلفة؛ فالحقيبة بسعر 850 هي المنتج الأغلى، لكن لم يُبع منها سوى حقيبتين فقط. حدد بدقة ما يقصده صاحب المتجر. وإذا لم تكن متأكداً، فاسأله؛ فالأمر يتطلب رسالة واحدة فقط، بينما يؤدي تقديم قائمة خاطئة إلى فقدان الثقة.
2. تفحص المدخلات قبل الترتيب. هناك ثلاثة أمور أساسية تؤثر في الترتيب: عدد الصفوف، وأنواع بيانات الأعمدة التي سترتب بناءً عليها، وما إذا كانت تلك الأعمدة تحتوي على فراغات.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)
print(orders["quantity"].isna().sum())(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: object
0ثمانية صفوف وسبعة أعمدة. نوع quantity هو int64 ونوع price هو float64، لذا سيتم ترتيبهما كـ أرقام. ونوع branch و product هو str — أي نصوص، وسيتم ترتيبهما أبجدياً. ونوع date هو أيضاً str. وهذا لا يمثل مشكلة هنا لسبب يستحق المعرفة: التواريخ المكتوبة بصيغة YYYY-MM-DD تترتب كنصوص بشكل صحيح لأن الجزء الأهم (السنة) يأتي أولاً. بينما التواريخ المكتوبة بصيغة مثل 4/3/2024 لن تترتب ترتيباً زمنياً صحيحاً كنصوص. وعمود quantity لا يحتوي على فراغات، لذا لا نحتاج لتحديد موضع القيم المفقودة — حالياً.
3. ارسم مسودة للمخرجات المتوقعة. لسؤال "أعلى ثلاثة طلبات حسب الكمية"، يجب أن تكون الإجابة ثلاثة صفوف من الأكبر إلى الأصغر: 30 (ممحاة، الطلب 1005)، ثم 20 (قلم، 1007)، ثم 12 (قلم، 1001). تدوين هذا التوقع قبل تشغيل أي كود يمنحك معياراً لمقارنة النتائج البرمجية به.
4. حدد تفاصيل الترتيب المطلوبة.
- المفتاح (Key) — ما العمود أو الأعمدة التي تحدد الترتيب؟ وهذا ما يُمرر إلى المعامل
by=. - الاتجاه (Direction) — من الأصغر إلى الأكبر أم من الأكبر إلى الأصغر لكل مفتاح؟ وهذا ما يُمرر إلى
ascending=. - التعادل (Ties) — إذا تساوى صفان في القيمة، فأيهما يسبق الآخر؟ يحدد ذلك بمفتاح ثانٍ، أو عبر
kind="stable". - القيم الفارغة (Blanks) — هل تظهر القيم المفقودة في البداية أم في النهاية؟ يُحدد ذلك بالمعامل
na_position=. - العدد المطلوب (How many) — هل تريد كل الصفوف أم أعلى N فقط؟ يُحدد ذلك عبر
.head(n)أوnlargest(). - الفهرس (Index) — هل لتسميات الصفوف القديمة أي دلالة بعد الترتيب؟ احتفظ بها أو احذفها وأعد الترقيم بـ
reset_index(drop=True).
5. اختر الأداة المناسبة.
- لكل الصفوف، مرتبة وفق عمود أو أكثر:
sort_values() - لأعلى أو أدنى N صفوف فقط وفق عمود رقمي:
nlargest()/nsmallest() - لإعادة الصفوف إلى ترتيب الفهرس الأصلي:
sort_index() - لأعلى N صفوف ضمن مجموعة فرعية ("في فرع north"): التصفية أولاً، ثم الترتيب، ثم الاقتطاع بـ
head()
6. اعرف كيف ستتحقق من النتيجة. بعد الترتيب، يجب أن تطابق الصفوف الأولى مسودتك، ويجب أن يبقى shape كما هو دون تغيير (فالترتيب لا يضيف ولا يحذف صفوفاً أبداً)، وفي حالة المفتاح الرقمي، يجب أن تقرأ قيم العمود بتزايد أو تناقص مستمر على طول الصفحة.
sort_values() — عمود واحد
للحفاظ على عرض المخرجات مناسباً، تعرض الأمثلة خمسة من الأعمدة السبعة. المتغير cols هو مجرد قائمة بأسماء الأعمدة، تُستخدم مع اختيار الأقواس المزدوجة التي تعلمتها في الفصل الرابع.
cols = ["order_id", "branch", "product", "quantity", "price"]
by_qty = orders.sort_values("quantity")
print(by_qty[cols])order_id branch product quantity price
5 1006 south bag 1 850.0
2 1003 north bag 2 850.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
3 1004 east bottle 7 120.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0الوضع الافتراضي هو تصاعدي (ascending) — من الأصغر إلى الأكبر. اقرأ عمود quantity من الأعلى إلى الأسفل: 1، 2، 4، 5، 7، 12، 20، 30. هذا هو التحقق المذكور في الخطة: يجب أن تتصاعد أرقام المفتاح الرقمي باستمرار.
والآن انظر إلى الحافة اليسرى. لم يعد الفهرس متسلسلاً 0, 1, 2, …، بل أصبح: 5, 2, 7, 1, 3, 0, 6, 4.
لماذا تنتقل تسميات الفهرس مع صفوفها
لم يُعد بانداس ترقيم الصفوف، وهذا تصرف متعمد ومقصود. تسمية الفهرس (index label) هي اسم الصف وهويته، وليست موضعه. فالتسمية 5 كانت تشير إلى الطلب 1006 قبل الترتيب، وما زالت تشير إلى الطلب 1006 بعده. ولو قام الترتيب بإعادة ترقيم الصفوف تلقائياً، لأشارت كل تسمية إلى طلب مختلف تماماً، ولأصبحت أي ملاحظة دونتها عن "الصف 5" خاطئة بصمت.
لذا، بعد إجراء الترتيب، يصبح هناك طريقتان مختلفتان لقول "الصف الأول"، ولم تعودا متطابقتين:
print(by_qty.iloc[0]["order_id"])
print(by_qty.loc[0]["order_id"])1006
1001الخاصية iloc[0] تعني الموضع صفر — الصف الأول على الصفحة، وهو الطلب 1006. أما loc[0] فتعني الصف الذي يحمل التسمية 0 — وهو الطلب 1001، الذي أصبح الآن سادس صف على الصفحة. أوضح الفصل الرابع هذا التمييز بدقة؛ والترتيب هو العملية التي تجعل هذا الفارق جوهرياً. عندما تريد "الصف الأعلى بعد الترتيب"، استخدم iloc أو head()، فكلاهما يعد المواضع.
الأكبر أولاً: ascending=False
print(orders.sort_values("quantity", ascending=False)[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0أعلى ثلاثة طلبات هي 1005 و 1007 و 1001 — تماماً كما رسمنا في المسودة. ولإرسال هذه الطلبات الثلاثة فقط، أضف head(3):
top3 = orders.sort_values("quantity", ascending=False).head(3)
print(top3[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0قراءة هذا التسلسل البرمجي توضح المنطق مباشرة: خذ orders، وضع الكميات الأكبر أولاً، ثم احتفظ بأول ثلاثة صفوف. هذا النمط "رتِّب ثم استدعِ head" يجيب عن معظم أسئلة "أعلى N".
الترتيب الأبجدي للنصوص
يتم ترتيب عمود str وفق الترتيب الأبجدي المعتاد:
print(orders.sort_values("product")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
4 1005 north eraser 30 8.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0تأتي كلمة bag قبل bottle لأن الحرف a يسبق o. لدينا حقيبتان وزجاجتان وقلمان — هذه حالات تعادل (ties)، وسنعود قريباً لمسألة تحديد أي العنصرين المتعادلين يسبق الآخر.
عملية الترتيب تُرجع جدولاً جديداً
هذا هو الخطأ الشائع الذي يقع فيه الجميع تقريباً في البداية، ولا ينتج عنه أي تنبيه أو خطأ:
orders.sort_values("quantity", ascending=False)
print(orders[cols].head(3))order_id branch product quantity price
0 1001 north pen 12 15.0
1 1002 south notebook 5 60.0
2 1003 north bag 2 850.0لم يتغير شيء ولم يتم الترتيب! دالة sort_values() لا تعدل جدول orders الأصلي؛ بل تُنشئ وتُرجع DataFrame جديداً بالترتيب المطلوب. السطر الأول أنشأ هذا الجدول المرتب ثم أهمله وتخلص منه لعدم تخزينه في متغير. فبقي orders كما أنتجته دالة read_csv تماماً.
لماذا يعمل بانداس بهذه الطريقة؟ لأن الترتيب الأصلي يمثل معلومة بحد ذاته؛ ففي orders.csv يمثل ترتيب وصول الطلبات. ولو كان كل ترتيب يغير نسختك الوحيدة من البيانات، فلن تتمكن أبداً من العودة إلى الوضع الأصلي للملف، ولكان الترتيب المكتوب لتقرير معين يعدل بصمت مدخلات التقرير الذي يليه. إن إرجاع كائن جديد يجعل تجربة الترتيب آمنة تماماً في أي وقت.
دالة sort_values() لا تعدل الجدول الأصلي أبداً. وإذا لم تسند النتيجة لمتغير، فسيحدث الترتيب ويُهمل في الذاكرة دون أن ينبهك أي خطأ.لذا، احرص دائماً على حفظ النتيجة، إما في اسم متغير جديد أو بإعادة إسنادها لنفس الاسم:
by_qty_desc = orders.sort_values("quantity", ascending=False) # keep both
print(by_qty_desc["order_id"].head(3).tolist())
print(orders["order_id"].head(3).tolist())[1005, 1007, 1001]
[1001, 1002, 1003]تحتوي النسخة المرتبة على الطلبات الكبيرة في الأعلى؛ بينما بقي الأصل دون مساس. كتابة orders = orders.sort_values(...) مقبولة تماماً عندما تكون متأكداً أنك لم تعد بحاجة إلى الترتيب القديم.
قد تصادف inplace=True في الأكواد القديمة. حيث تقوم بتعديل الجدول في مكانه وتُرجع None، مما يتسبب في أخطاء خاصة به (انظر قسم الأخطاء الشائعة وحلولها). ومع ميزة النسخ عند الكتابة (Copy-on-Write) في بانداس 3، لم يعد خيار inplace=True يوفر أي ذاكرة أيضاً. لذا فإن إسناد النتيجة لمتغير هو الأسلوب الأوضح دائماً.
أعمدة متعددة: الأولوية والاتجاه
طلب صاحب المتجر الثاني: "مجمعة حسب الفرع، بحيث يظهر أكبر طلب أولاً داخل كل فرع." هذا يتطلب مفتاحين للترتيب، وترتيب عناصر القائمة يحدد الأولوية (priority):
by_branch = orders.sort_values(["branch", "quantity"], ascending=[True, False])
print(by_branch[cols])order_id branch product quantity price
6 1007 east pen 20 15.0
3 1004 east bottle 7 120.0
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
1 1002 south notebook 5 60.0
5 1006 south bag 1 850.0اقرأ الجدول بالطريقة التي بناها بها بانداس. أولاً، وُضع كل صف حسب عمود branch أبجدياً من A إلى Z: فرع east، ثم north، ثم south. أما المفتاح الثاني quantity، فيتم الرجوع إليه فقط عندما تتساوى قيم المفتاح الأول — أي داخل east، وداخل north، وداخل south. فداخل فرع north تظهر الكميات: 30، 12، 4، 2؛ من الأكبر إلى الأصغر، لأن القيمة الثانية في ascending هي False.
يأخذ المعامل ascending قيمة منطقية (True أو False) لكل مفتاح، بنفس ترتيب قائمة by. ولو مررت قيمة واحدة False، فستنطبق على كلا المفتاحين وتضع فرع south أولاً.
وعكس ترتيب الأعمدة في القائمة يغير طبيعة السؤال كلياً:
print(orders.sort_values(["quantity", "branch"], ascending=[False, True])[cols].head(4))order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0الآن أصبحت السيادة للكمية، ولن يتم الالتفات للفرع branch إلا إذا تساوى طلبان في الكمية تماماً — وهو ما لم يحدث هنا. فأصبحت الفروع مشتتة. لذا فإن السؤال الأساسي قبل كتابة القائمة هو: "ما المعيار الأساسي لتنظيم التقرير؟" ذلك العمود هو الذي يوضع أولاً.
حالات التعادل: اجعل الترتيب قراراً مقصوداً لا صدفة
رتِّب بناءً على price، الأغلى سعراً أولاً:
print(orders.sort_values("price", ascending=False)[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
7 1008 north bottle 4 120.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0انظر إلى الزجاجتين بسعر 120. في الملف الأصلي، يسبق الطلب 1004 الطلب 1008. لكن في الجدول المرتب، جاء 1008 أولاً. لم يحدث عطل: ففي الترتيب بمفتاح واحد، لا تضمن خوارزمية الترتيب الافتراضية في بانداس (quicksort) الحفاظ على الترتيب الأصلي للصفوف المتعادلة. بقيت الحقيبتان بسعر 850 بترتيب الملف، بينما تبادلت الزجاجتان المواضع — لا توجد قاعدة يمكن التنبؤ بها هنا.
غالباً لا يهم ذلك كثيراً، حتى اللحظة التي تقرر فيها اقتطاع القائمة. فإذا كان المطلوب "أغلى ثلاثة طلبات"، فالمركز الثالث يمثل تعادلاً بين 1004 و 1008، وتحديد أيهما يدخل القائمة صار قراراً اتخذته الخوارزمية بالصدفة، وليس أنت. وإذا شغلت الكود على ملف آخر بتنسيق مختلف قليلاً، فقد تختلف النتيجة.
هناك طريقتان لاستعادة السيطرة على هذا القرار.
الخيار 1: الترتيب المستقر (stable sort). يضمن تمرير kind="stable" احتفاظ الصفوف المتعادلة بالترتيب الذي كانت عليه قبل الفرز — وهو ترتيب الملف الأصلي هنا:
stable = orders.sort_values("price", ascending=False, kind="stable")
print(stable["order_id"].tolist())[1003, 1006, 1004, 1008, 1002, 1001, 1007, 1005]الخيار 2 (وهو الأفضل عادةً): مفتاح صريح لفك التعادل (tie-breaker). حدد بوضوح المعيار الذي يحسم التعادل. في تقارير المبيعات، تعد قاعدة "إذا تساوى السعر، فالطلب ذو الكمية الأكبر يأتي أولاً" قاعدة عملية ومنطقية:
tie_broken = orders.sort_values(["price", "quantity"], ascending=[False, False])
print(tie_broken[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
4 1005 north eraser 30 8.0انظر إلى الزجاجتين مجدداً: أصبح 1004 فوق 1008 لأن الكمية 7 تتفوق على 4. وانظر للقلمين بسعر 15: سابقاً جاء 1001 أولاً؛ والآن فاز 1007 لأن 20 تتفوق على 12 — وسيفوز دائماً على أي جهاز ومهما كان ترتيب وصول الملف. يحرص مفتاح فك التعادل على تحويل الصدفة العشوائية إلى قاعدة يمكنك تفسيرها بثقة. (عند الترتيب بأكثر من عمود، يكون ترتيب بانداس مستقراً بطبيعته، لذا لا تتشتت حالات التعادل الحقيقية أبداً).
وإذا لم تكن لديك قاعدة عمل محددة، فإن عمود order_id يعد مفتاح حسم ممتازاً: فهو فريد بطبيعته، مما يضمن انتفاء أي تعادل بعده.
القيم المفقودة: na_position
اقرأ نسخة الملف التي تحتوي على كميتين فارغتين:
gaps = pd.read_csv("orders_gaps.csv")
print(gaps["quantity"].isna().sum())
print(gaps.sort_values("quantity", ascending=False)[cols])2
order_id branch product quantity price
4 1005 north eraser 30.0 8.0
0 1001 north pen 12.0 15.0
3 1004 east bottle 7.0 120.0
7 1008 north bottle 4.0 120.0
2 1003 north bag 2.0 850.0
5 1006 south bag 1.0 850.0
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0لاحظ أمرين هنا: أولاً، تُطبع الكميات الآن بصيغة 30.0 و 12.0 — فقد حولت الفراغات نوع العمود إلى float64 كما أوضح الفصل السادس، لأن NaN تصنف كرقم عشري. ثانياً، انتقلت صفوف NaN إلى أسفل الجدول، على الرغم من أننا طلبنا الترتيب التنازلي (الأكبر أولاً). هذا هو السلوك الافتراضي: na_position="last"، وينطبق في كلا الاتجاهين؛ فالقيمة المفقودة ليست "صغيرة" ولا "كبيرة"، بل مجهولة، لذا يضعها بانداس جانباً بدلاً من التخمين.
لاحظ أثر ذلك على أعلى ثلاثة طلبات: الطلب 1007 بيع فيه 20 قلماً بالفعل، لكن كميته فارغة في هذا الملف، فاستُبعد من أعلى ثلاثة طلبات وحل 1004 محله. لا يمكن لعملية الترتيب إصلاح البيانات المفقودة؛ بل تكشف لك فقط أين استقرت تلك الفراغات. لهذا السبب تضمنت الخطوة 2 من التخطيط إحصاء الفراغات قبل الترتيب.
عند تدقيق البيانات، غالباً ما تريد العكس تماماً — وضع المشاكل في المقدمة لتراها بوضوح:
print(gaps.sort_values("quantity", na_position="first")[cols].head(3))order_id branch product quantity price
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0
5 1006 south bag 1.0 850.0إذن na_position ليس مجرد خيار تنسيقي؛ فالخيار "last" يناسب التقارير حتى لا تحجب الفراغات القيم الحقيقية، بينما يناسب "first" عمليات الفحص والتدقيق حيث تكون الفراغات هي ما تبحث عنه تحديداً.
العودة إلى البداية: sort_index() و reset_index()
نظراً لأن كل صف يحتفظ بتسميته، فإن الترتيب الأصلي لا يضيع أبداً. والترتيب بناءً على الفهرس يعيده كما كان:
restored = by_qty.sort_index()
print(restored["order_id"].tolist())[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008]تقبل الدالة sort_index() المعامل ascending=False أيضاً، وهو ما يتيح لك عرض الجدول بـ "أحدث التسميات أولاً".
في بعض الأحيان تفقد التسميات القديمة معناها. فإذا قمت بالترتيب لإنشاء قائمة متصدرين ومشاركتها، فإن ظهور التسميات 4, 6, 0 على الجانب يكون مربكاً للقارئ. يؤدي استخدام reset_index(drop=True) إلى التخلص منها وإعادة ترقيم الصفوف إلى 0, 1, 2, … وفق ترتيبها الجديد:
ranked = orders.sort_values("quantity", ascending=False).reset_index(drop=True)
print(ranked[cols].head(3))
print(ranked.loc[0, "order_id"])order_id branch product quantity price
0 1005 north eraser 30 8.0
1 1007 east pen 20 15.0
2 1001 north pen 12 15.0
1005الآن يتطابق loc[0] و iloc[0] من جديد: فكلاهما يشير إلى الطلب 1005 ذي الكمية الأكبر. وبدون drop=True، سيحتفظ بانداس بالتسميات القديمة في عمود جديد باسم index — وهو أمر مفيد إذا أردت تتبع أصل الصفوف، ومربك إذا كنت لا تحتاجه.
أيهما تختار؟ احتفظ بالتسميات طالما أنك لا تزال تعمل على معالجة البيانات وتحتاج لربط الصفوف بأصلها. وأعد ضبطها عندما يكون الترتيب بحد ذاته هو النتيجة النهائية — مثل قوائم التصنيف والترتيب.
الطريق المختصر لـ "أعلى N": nlargest() و nsmallest()
طلب "أعلى ثلاثة طلبات حسب الكمية" متكرر جداً، لدرجة أن بانداس يوفر دالة مخصصة تعبر عن ذلك مباشرة:
print(orders.nlargest(3, "quantity")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0وهي تُرجع نفس صفوف sort_values("quantity", ascending=False).head(3) تماماً. لماذا نفضلها؟ لأنها تعبر عن الهدف المباشر — "أكبر ثلاثة" — بدلاً من تفاصيل الآلية. كما أنها أسرع في الجداول الكبيرة، لأنها لا تحتاج لترتيب جميع الصفوف فقط للاحتفاظ بثلاثة منها.
ودالة nsmallest() هي المقابل المباشر لها: أرخص ثلاثة منتجات في المتجر:
print(orders.nsmallest(3, "price")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0التعادل عند حد القطع: keep=
اطلب أغلى ثلاثة منتجات وسيحدث أمر محرج:
print(orders.nlargest(3, "price")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0المركز الثالث بسعر 120 — لكن طلبين يحملان هذا السعر: 1004 و 1008. أعاد بانداس أحدهما فقط — وفق القيمة الافتراضية keep="first" التي تختار الأسبق ظهوراً في الجدول — واستبعد الآخر بصمت. فإذا كنت تمنح جوائز لـ "أفضل ثلاثة"، فهذا ظلم بيّن للطلب 1008. يضمن الخيار keep="all" الاحتفاظ بكل صف يتعادل عند حد الاقتطاع، حتى لو أدى ذلك لإرجاع صفوف أكثر من N:
print(orders.nlargest(3, "price", keep="all")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0أربعة صفوف لطلب "أعلى ثلاثة". هذا ليس خطأ برمجياً، بل هو التعبير الأمين عن وجود تعادل في المركز الثالث. وتحديد ما إذا كنت تقبل ذلك يرجع لقرارات التخطيط.
تعمل دالتا nlargest و nsmallest على الأعمدة الرقمية فقط. بالنسبة للنصوص، استخدم sort_values مع head.
أعلى N في مجموعة فرعية: صفِّ، ثم رتِّب، ثم اقتطع
"ما هما أكبر طلبين في فرع north؟" يدمج هذا السؤال بين ما تعلمته في الفصل الخامس وهذا الفصل، وترتيب الخطوات حاسم للغاية:
north = orders[orders["branch"] == "north"]
top_north = north.sort_values("quantity", ascending=False).head(2)
print(top_north[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0قم بالتصفية أولاً، لأن السؤال يدور حول فرع north حصراً. فلو قمت بترتيب الجدول بأكمله وأخذت أول صفين عبر head(2) أولاً، لحصلت على 1005 و 1007 — والطلب 1007 يتبع فرع east. والتصفية بعدها كانت ستبقي صفاً واحداً فقط، فتقدم تقريراً عن "أعلى اثنين" بعنصر واحد يتيم! القاعدة الذهبية: احصر البيانات في الصفوف المطلوبة أولاً، ثم رتبها، ثم اقتطع العدد المطلوب.
ينطبق المنطق نفسه مع دالة nlargest: orders[orders["branch"] == "north"].nlargest(2, "quantity").
ترتيب النصوص دون التحسس لحالة الأحرف: key=
تقارن النصوص عادةً بناءً على رموز الحروف البرمجية، وتأتي جميع الحروف الكبيرة قبل الحروف الصغيرة. والبيانات التي يدخلها البشر تخلط بين الاثنين حتماً:
names = pd.DataFrame({"product": ["pen", "Notebook", "bag", "Bottle", "eraser"]})
print(names.sort_values("product"))product
3 Bottle
1 Notebook
2 bag
4 eraser
0 penقفزت Bottle و Notebook قبل bag فقط لأنهما تبدآن بحرف كبير. لا يمكن لأي قارئ اعتبار هذا ترتيباً أبجدياً سليماً.
يمكنك تنظيف البيانات، ولكن في كثير من الأحيان لا يُحبذ ذلك — فقد تكون طريقة الكتابة هي نفسها التي أدخلها العميل. يتيح لك المعامل key= الترتيب وفق نسخة معدلة من العمود مع الاحتفاظ بالقيم الأصلية كما هي. حيث يأخذ دالة تستقبل العمود بأكمله وتُرجع النسخة التي سيتم الترتيب بناءً عليها:
print(names.sort_values("product", key=lambda col: col.str.lower()))product
2 bag
3 Bottle
4 eraser
1 Notebook
0 penتُستخدم النسخة ذات الأحرف الصغيرة للمقارنة فقط ثم تُهمل. ولا يزال الجدول يعرض Bottle و Notebook تماماً كما كُتبتا. (الدالة lambda col: col.str.lower() هي دالة بسطر واحد: تأخذ العمود col وتُرجعه بحروف صغيرة).
عندما تُخزّن الأرقام كنصوص
هذا هو الفخ الذي وُضعت خطوة فحص أنواع البيانات dtypes لاكتشافه مسبقاً. افترض أن عمود quantity ورد كنصوص — بسبب وجود قيمة "N/A" نصية في الملف، أو لأن نظام التصدير يصدر كل شيء كنصوص. لننشئ عموداً مماثلاً عمداً باستخدام astype لنرى ما يحدث:
as_text = orders.astype({"quantity": "str"})
print(as_text["quantity"].dtype)
print(as_text.sort_values("quantity")[["order_id", "quantity"]])str
order_id quantity
5 1006 1
0 1001 12
2 1003 2
6 1007 20
4 1005 30
7 1008 4
1 1002 5
3 1004 7النتيجة: 1, 12, 2, 20, 30, 4, 5, 7. لم يظهر أي خطأ برمجياً، لكن الترتيب غير سليم؛ حيث تُقارن النصوص حرفاً بحرف: فالنص "12" يبدأ بالحرف 1، وهو أقل من 2، لذا تأتي "12" قبل "2" — تماماً كما تأتي "ab" قبل "b" في المعجم. نفذ الترتيب ما طُلب منه بدقة، لكن ما طُلب منه كان غير صحيح.
هناك علامتان تكشفان ذلك: محاذاة أرقام quantity لليسار في الطباعة (وهي طريقة عرض النصوص)، وظهور نوع البيانات كـ str في dtype. الحل الجذري هو تحويل العمود لأرقام باستخدام pd.to_numeric (كما تعلمنا في الفصل السادس). وإذا كنت مضطراً لترك العمود كما هو، يمكنك استخدام key= للترتيب وفق القيمة الرقمية:
fixed = as_text.sort_values("quantity", key=pd.to_numeric)
print(fixed["quantity"].tolist())['1', '2', '4', '5', '7', '12', '20', '30']الترتيب صحيح الآن — لكن القيم لا تزال نصوصاً، لذا لن تتمكن من جمعها. الأفضل دائماً هو تحويل العمود مرة واحدة فور قراءة الملف، لتضمن دقة كل العمليات التالية من ترتيب وتصفية وحساب.
مثال تطبيقي متكامل
إليك إجابة طلبات صاحب المتجر كاملة في برنامج واحد: (1) أعلى ثلاثة طلبات من حيث الإيراد المالي، (2) أحدث الطلبات أولاً، (3) قائمة مجمعة حسب الفرع مع إبراز أكبر طلب أولاً في كل فرع.
"الإيراد المالي" هو حاصل ضرب الكمية في السعر. سنتناول إضافة الأعمدة بالتفصيل لاحقاً في الدورة؛ وحالياً يكفي هذا السطر لإنشاء عمود جديد بمجرد إسناد ناتج الضرب إليه.
الملف sorted_report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check the input before ranking anything
print("Rows, columns:", orders.shape)
print("Blank quantities:", orders["quantity"].isna().sum())
print()
orders["revenue"] = orders["quantity"] * orders["price"]
cols = ["order_id", "date", "branch", "product", "revenue"]
# 1. Top three by revenue, ties broken by the larger quantity, renumbered as a ranking
top3 = (
orders.sort_values(["revenue", "quantity"], ascending=[False, False])
.head(3)
.reset_index(drop=True)
)
print("Top three orders by revenue")
print(top3[cols])
print()
# 2. Newest first; within the same day, the bigger order first
newest = orders.sort_values(["date", "revenue"], ascending=[False, False])
print("Newest first")
print(newest[cols].head(4))
print()
# 3. By branch A-Z, biggest order first inside each branch
by_branch = orders.sort_values(["branch", "revenue"], ascending=[True, False])
print("By branch")
print(by_branch[cols])
print()
print("Still eight rows:", by_branch.shape[0] == orders.shape[0])Rows, columns: (8, 7)
Blank quantities: 0
Top three orders by revenue
order_id date branch product revenue
0 1003 2024-03-02 north bag 1700.0
1 1006 2024-03-03 south bag 850.0
2 1004 2024-03-02 east bottle 840.0
Newest first
order_id date branch product revenue
7 1008 2024-03-04 north bottle 480.0
6 1007 2024-03-04 east pen 300.0
5 1006 2024-03-03 south bag 850.0
4 1005 2024-03-03 north eraser 240.0
By branch
order_id date branch product revenue
3 1004 2024-03-02 east bottle 840.0
6 1007 2024-03-04 east pen 300.0
2 1003 2024-03-02 north bag 1700.0
7 1008 2024-03-04 north bottle 480.0
4 1005 2024-03-03 north eraser 240.0
0 1001 2024-03-01 north pen 180.0
5 1006 2024-03-03 south bag 850.0
1 1002 2024-03-01 south notebook 300.0
Still eight rows: Trueلماذا كُتب البرنامج بهذه الطريقة:
- التحقق يأتي أولاً. تمت طباعة الأبعاد
shapeوعدد الفراغات قبل أي ترتيب؛ فالاعتماد على ملف ناقص أو به قيم مفقودة كان سيبدو واثقاً من نفسه ظاهرياً ولكنه خاطئ عملياً. - تحويل "الأكبر" إلى عمود رقمي صريح. الترتيب حسب الكمية
quantityكان سيضع 30 ممحاة في القمة (بقيمة 240 فقط). بينما الترتيب حسب الإيرادrevenueوضع حقيبتين بقيمة 1,700 في الصدارة. خطوة التخطيط الأولى — صياغة السؤال مع ذكر اسم العمود — هي ما حدد الإجابة الصحيحة لصاحب المتجر. - تحديد معيار حسم لكل تعادل. في تاريخ 2024-03-04 يوجد طلبان، ويحسم عمود
revenueأيهما يسبق الآخر، مما يضمن ثبات التقرير عند كل تشغيل. - إعادة ترقيم الفهرس لقائمة التصنيف فقط. الجدول
top3قائمة نهائية، لذا أصبحت تسمياته 0 و 1 و 2. بينما احتفظnewestوby_branchبتسمياتهما الأصلية لتسهيل مراجعة الصفوف في الملف الأصلي. - لم يتم تعديل ترتيب الجدول
ordersالأصلي أبداً. كل تقرير يمثل جدولاً جديداً مبنياً على نفس المدخلات الأصلية، فلا يؤثر تقرير على آخر. - السطر الأخير تأكيد سريع وغير مكلف. لا يجوز للترتيب تغيير عدد الصفوف. طباعة
Trueلا تكلف شيئاً وتكشف أي استدعاء عارض لدالةhead()في غير موضعه.
الأخطاء الشائعة وحلولها
KeyError: 'Quantity'
orders.sort_values("Quantity")KeyError: 'Quantity'يجب أن يطابق الاسم في by= اسم العمود تماماً — بما في ذلك حالة الأحرف والمسافات. اسم العمود هو quantity بحروف صغيرة. اطبع list(orders.columns) لمعاينة الأسماء الحقيقية بما فيها علامات التنصيص والمسافات الزائدة، كما نصحنا في الفصل الثالث.
AttributeError: 'NoneType' object has no attribute 'head'
top = orders.sort_values("quantity", inplace=True)
top.head()AttributeError: 'NoneType' object has no attribute 'head'عند تمرير inplace=True، تقوم دالة sort_values بتعديل جدول orders مباشرة وتُرجع None. بالتالي أصبحت قيمة top هي None، والتي لا تملك دالة head(). اختر أحد الأسلوبين: إما كتابة orders.sort_values("quantity", inplace=True) في سطر مستقل، أو — وهو الأفضل — top = orders.sort_values("quantity") بدون inplace.
ValueError: Length of ascending (1) != length of by (2)
orders.sort_values(["branch", "quantity"], ascending=[False])ValueError: Length of ascending (1) != length of by (2)تحتاج قائمة ascending إلى عنصر مقابل لكل مفتاح في by. مفتاحان يتطلبان قيمتين منطقيتين: ascending=[True, False]. (يُسمح بتمرير قيمة منطقية واحدة مباشرة مثل False خارج قائمة، وستنطبق حينها على كل المفاتيح).
ValueError: For argument "ascending" expected type bool, received type str.
orders.sort_values("branch", ascending="False")ValueError: For argument "ascending" expected type bool, received type str.القيمة "False" بين علامات تنصيص هي نص وليست قيمة منطقية بولية. احذف علامات التنصيص لتصبح False.
TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtype
orders.nlargest(3, "branch")TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtypeترتب دالتا nlargest و nsmallest الأرقام فقط. وللحصول على "آخر ثلاثة فروع أبجدياً"، استخدم orders.sort_values("branch", ascending=False).head(3). وإذا ظهر لك هذا الخطأ في عمود كنت تتوقع أنه رقمي، فالمشكلة الأساسية أنه قُرئ كنص — راجع dtypes.
TypeError: '<' not supported between instances of 'str' and 'int'
mixed = pd.Series([12, "unknown", 5])
mixed.sort_values()TypeError: '<' not supported between instances of 'str' and 'int'يحتوي هذا العمود على أرقام حقيقية ونص معاً، لذا فإن نوعه هو object، ولا يستطيع بايثون تحديد ما إذا كانت "unknown" أكبر أم أصغر من 12. نظف العمود أولاً: يحول pd.to_numeric(mixed, errors="coerce") النص "unknown" إلى NaN، وعندها ينجح الترتيب ويحدد na_position مكان ظهور الفراغ.
Step 4 of 6 — Predict
Check your understanding
تشتركت حقيبتان في أعلى سعر وهو 850.0. ما الذي سيتم طباعته؟
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))
top = orders.sort_values("price", ascending=False)
print(list(top.index[:3]))- A[0, 1, 2]
- B[2, 5, 3]
- C[5, 2, 7]
- D[2, 5, 7]
الهدف هو طباعة الطلب الذي يحتوي على أكبر عدد من المنتجات. ما الذي سيتم طباعته؟
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))
orders.sort_values("quantity", ascending=False)
print(orders.iloc[0]["order_id"])- A1005
- B1001 — دالة `sort_values` أعادت نسخة مرتبة ولم يقم أحد بحفظها في متغير
- C1007
- Dخطأ `TypeError`: يجب تعيين النتيجة لمتغير
تريد ترتيب الطلبات حسب الفرع أبجدياً (A–Z)، وداخل كل فرع تأتي الكمية الأكبر أولاً. أي سطر برمجيات يحقق ذلك؟
- Aorders.sort_values(["branch", "quantity"], ascending=False)
- Borders.sort_values(["quantity", "branch"], ascending=[False, True])
- Corders.sort_values("branch").sort_values("quantity", ascending=False)
- Dorders.sort_values(["branch", "quantity"], ascending=[True, False])
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.
دورك الآن
أرسل إليك معلم درجات اختبار معين. احفظ هذه البيانات في ملف باسم results.csv. هناك طالبان لم تُسجل درجاتهما بعد، وبعض الأسماء كُتبت بحروف صغيرة:
name,section,marks
Rafi,B,78
anika,A,91
Tanvir,A,
Mitu,B,91
zara,A,65
Karim,B,
Nadia,A,88
Sabbir,B,54اكتب برنامج ranking.py. وقبل كتابة أي كود برمجي، اكتب خطتك في تعليقات بأعلى الملف موضحةً: مفتاح الترتيب لكل قائمة، والاتجاه، وكيفية فك التعادل، وموضع الفراغات. ثم اجعل البرنامج يحقق هذه الشروط الأربعة:
- قبل ترتيب أي شيء، يطبع البرنامج الأبعاد
shape، وأنواع البياناتdtypes، وعدد الدرجات المفقودة. - يطبع قائمة الأوائل (merit list): الدرجة الأعلى أولاً، والطلاب الذين ليس لديهم درجات في الأسفل، ويُفك التعادل في الدرجات بترتيب الأسماء أبجدياً دون تحسس لحالة الأحرف، مع إعادة ترقيم الصفوف 0، 1، 2، …
- يطبع أعلى ثلاثة طلاب باستخدام
nlargest، مع الإبقاء على كل من يتعادل في المركز الثالث. - يطبع الطلاب الذين ليس لديهم درجات بعد، مرتبين بأسمائهم دون تحسس لحالة الأحرف، حتى يتمكن المعلم من متابعتهم — ثم يطبع الجدول الأصلي لإثبات أنه لم يتغير أبداً.
يجب أن يطبع برنامجك هذه المخرجات تحديداً:
(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0ثم قم بإجراء هاتين التجربتين على قائمة الأوائل:
- احذف المعامل
key=. أي الطالبين الحاصلين على 91 يأتي أولاً الآن، ولماذا؟ - احذف
"name"تماماً من مفاتيح الترتيب ورتب حسبmarksوحدها. من يسبق في درجة 91 — وهل هذه النتيجة مضمونة دائماً؟
الحل
الخطة. يحتوي الملف على 8 طلاب و 3 أعمدة. يحتوي عمود marks على فراغات، لذا ستتم قراءته كـ float64، ويجب أن تحدد كل قائمة مرتبة حسب الدرجات موضع الفراغات. ويخلط عمود name بين الحروف الكبيرة والصغيرة، لذا تحتاج كل مقارنة للأسماء إلى key=.
- قائمة الأوائل — المفاتيح:
marksثمname؛ الدرجات من الأعلى للأدنى، والأسماء من A إلى Z؛ يُفك التعادل بالاسم؛ الفراغات في النهاية (افتراضياً)؛ إعادة الترقيم بـreset_index(drop=True). - أعلى ثلاثة طلاب —
nlargest(3, "marks")معkeep="all"لضمان عدم استبعاد المركز الثالث المشترك. وتستبعدnlargestالفراغات تلقائياً. - الذين ليس لديهم درجات — تصفية الصفوف التي تفتقد لقيمة
marksكما تعلمنا سابقاً، ثم الترتيب بالاسم. التصفية أولاً ثم الترتيب. - الجدول الأصلي — لم تتم إعادة إسناده أبداً، لذا فإن طباعة
resultsفي النهاية تظهر ترتيب الملف الأصلي.
ملاحظة تفصيلية: تُطبق دالة key= على كل عمود في مفاتيح الترتيب. في قائمة الأوائل يعني هذا تطبيقها على marks أيضاً، وستفشل دالة .str.lower() مع الأرقام. لذلك تحول الدالة الأعمدة النصية فقط إلى حروف صغيرة وتترك أي عمود آخر كما هو دون تغيير.
الملف ranking.py:
import pandas as pd
# Merit list : marks high->low, ties by name A->Z ignoring case, blanks last, renumber
# Top three : nlargest on marks, keep="all" so a shared third place is not cut
# No mark yet : filter rows with no mark, then name A->Z ignoring case
# Original : never reassigned
results = pd.read_csv("results.csv")
print(results.shape)
print(results.dtypes)
print("Missing marks:", results["marks"].isna().sum())
print()
def lower_text(col):
return col.str.lower() if col.dtype == "str" else col
merit = results.sort_values(
["marks", "name"], ascending=[False, True], key=lower_text
).reset_index(drop=True)
print("Merit list")
print(merit)
print()
print("Top three")
print(results.nlargest(3, "marks", keep="all"))
print()
print("No mark yet")
missing = results[results["marks"].isna()]
print(missing.sort_values("name", key=lower_text))
print()
print("Original, unchanged")
print(results)(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0مقارنة المخرجات بالخطة:
- نوع
marksهوfloat64وليسint64، مع ظهورMissing marks: 2— تم اكتشاف الفراغات قبل أي ترتيب، ليعلم قارئ القائمة أن طالبين غائبان وليسا في قاع الترتيب. - حُسم التعادل في درجة 91 بالاسم. جاءت
anikaقبلMituلأن المقارنة تمت بين"anika"و"mitu". - أعلى ثلاثة طلاب تضم ثلاثة صفوف هنا لأن المركز الثالث (88) لم يشترك فيه أحد. ولو كانت درجة نادية 91 أيضاً، لأعادت دالة
keep="all"أربعة صفوف بدلاً من استبعاد طالب بصمت. - قائمة الطلاب بدون درجات صفّت البيانات أولاً ثم رتّبت — متبعة نفس مبدأ "احصر البيانات أولاً ثم رتبها".
- الجدول الأخير يطابق ترتيب الملف الأصلي. فقد أُنتجت ثلاث قوائم مختلفة وظل جدول
resultsدون أي تعديل.
التجربتان:
no_key = results.sort_values(["marks", "name"], ascending=[False, True])
marks_only = results.sort_values("marks", ascending=False)
print(no_key["name"].head(2).tolist())
print(marks_only["name"].head(2).tolist())['Mitu', 'anika']
['anika', 'Mitu']بدون key=، تُقارن الأسماء حرفاً بحرف، وتسبق الحروف الكبيرة نظيرتها الصغيرة، لذا تتفوق "Mitu" على "anika" — وهو ترتيب ناتج عن طريقة كتابة الاسم لا عن الأبجدية الحقيقية. ومع الترتيب بـ marks وحدها، جاءت anika أولاً بمحض الصدفة لأنها كانت أسبق في الملف الأصلي. تطابق هذه النتيجة قائمة الأوائل الصحيحة ظاهرياً ولكنها مجرد مصادفة: فالترتيب بمفتاح واحد لا يضمن الحفاظ على ترتيب الملف للصفوف المتعادلة، كما رأينا سابقاً مع الزجاجتين بسعر 120. القاعدة البرمجية الصريحة تضمن النتيجة دائماً، أما النتيجة التي تبدو صحيحة بمحض الصدفة فلا ضمانة لها.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz