اختيار الأعمدة والصفوف — بالاسم أو بالموضع
تحديد جزء من الجدول: اختيار الأعمدة بالأقواس المربعة، والصفوف بالتسمية عبر loc وبالموضع عبر iloc، والخلايا المفردة عبر at — ومعرفة أي الطريقتين تستخدم دائماً.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
لقد قمت بقراءة ملف طلبات المتجر وفحصه بدقة، تماماً كما تعلمت في الفصل الثالث. والآن بدأ الزملاء يطرحون عليك أسئلة محددة حول هذه البيانات.
يقول فريق المخزون: "أرسل لنا فقط اسم المنتج والكمية لكل طلب — لا نحتاج لأي شيء آخر." ويقول المحاسب: "ما هو الطلب رقم 1006 تحديداً، وما هي تكلفته الإجمالية؟" بينما يطلب منك مديرك: "أرني آخر ثلاثة طلبات وردت إلينا."
لا يتطلب أي من هذه الأسئلة إجراء عمليات حسابية على الجدول بأكمله؛ بل يتعلق كل منها بـ الإشارة والتحديد (pointing): الإشارة إلى أعمدة معينة، أو إلى صف واحد بعينه، أو إلى الصفوف القليلة الأخيرة، أو إلى خلية مفردة. إن طباعة الجدول بأكمله وتتبعه بإصبعك قد يفي بالغرض إذا كان الجدول يحتوي على ثمانية صفوف فقط؛ لكنه لا ينجح إطلاقاً مع ثمانية آلاف صف، وبالتأكيد لا يعمل داخل برنامج برمجي لا يملك إصبعاً لتتبع الشاشة.
يوفر بانداس عدة طرق للإشارة إلى البيانات، والسبب في وجود عدة طرق هو الجوهر الحقيقي لهذا الفصل. يمكن الإشارة إلى أي صف بطريقتين مختلفتين تماماً: إما عبر اسمه (تسميته أو معرّفه في الفهرس Index Label) أو عبر موضعه وترتيبه (الأول، الثاني، الأخير). عند قراءة ملف لأول مرة، يتصادف أن تكون التسميات والمواضع متطابقة في الأرقام، ولهذا لا يلاحظ المبتدئون أبداً وجود مفهومين منفصلين. لكن بمجرد اقتطاع آخر ثلاثة صفوف، أو إعادة ترتيب البيانات، أو تصفيتها، يتوقف هذا التطابق تماماً — والكود الذي يخلط بينهما سيبدأ في إعادة صفوف خاطئة تماماً دون أن يُصدر أي خطأ برمجي ينبهك.
لذلك، يتمحور هذا الفصل حول تحديد البيانات، لكنه في المقام الأول يدربك على أن تعرف دائماً وبوضوح أيّ الطريقتين تستخدم.
بنهاية هذا الفصل ستكون قادراً على
- تحديد عمود واحد والحصول على
Series، أو عدة أعمدة والحصول علىDataFrame— ومعرفة أيهما بين يديك بدقة - تحديد الصفوف بحسب التسمية باستخدام
.locوبحسب الموضع باستخدام.iloc، وشرح سبب اختلاف نهاية نطاقات التقطيع (Slices) بينهما - تحديد الصفوف والأعمدة معاً باستخدام
.loc[rows, columns]وتوقع أبعاد وشكل النتيجة مسبقاً — وصولاً إلى استخراج قيمة مفردة باستخدام.locأو.at - منح الصفوف تسميات ذات دلالة ومعنى باستخدام
set_index()، وفهم كيف يغير ذلك معنى ودلالة.loc - شرح سبب عدم تأثير تعديل جزء محدد على الجدول الأصلي في إصدار بانداس 3، وتعديل الجدول الأصلي بالطريقة الصحيحة؛ وفهم وإصلاح أخطاء
KeyErrorوIndexErrorالناتجة عن عمليات التحديد
المتطلبات المسبقة: قراءة ملف CSV وفحص البيانات بعد قراءتها.
أنشئ الملف أولاً
داخل مجلد مشروعك، أنشئ ملفاً باسم 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. صِغ السؤال في جملة واحدة، وضع خطاً تحت الأسماء (nouns). تخبرك الأسماء بدقة عما تشير إليه:
- "منتج (product) و كمية (quantity) كل طلب" — عمودان، لجميع الصفوف
- "الطلب 1006" — صف واحد، محدد برقم طلبه
- "آخر ثلاثة طلبات" — صفوف بحسب الموضع، معدودة من نهاية الجدول
- "سعر الطلب 1006" — خلية واحدة: صف وعمود معاً
2. عاين الجدول قبل أن تشير إلى أي شيء بداخله. لا يمكنك الإشارة إلا إلى أسماء موجودة بالفعل. هناك ثلاثة أمور حاسمة لعملية التحديد: الحجم والأبعاد، وأسماء الأعمدة الدقيقة، والفهرس (Index) — أي التسميات الممتدة على الجانب الأيسر.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(list(orders.columns))
print(orders.index)(8, 7)
['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
RangeIndex(start=0, stop=8, step=1)تظهر أسماء الأعمدة كقائمة محاطة بعلامات تنصيص — مما يتيح لك التأكد من أن 'price' مكتوب بحروف صغيرة ولا يحتوي على مسافات غير مرئية. أما الفهرس فهو RangeIndex من 0 إلى 7: التسميات مجرد أرقام متسلسلة. تذكر ذلك جيداً، لأن هذه هي المصادفة المحورية التي يدور حولها الفصل بأكمله. في هذه اللحظة، الصف الذي يحمل التسمية 2 هو نفسه الصف الواقع في الموضع 2. لكن هذا لن يظل صحيحاً دائماً.
3. تخيل شكل النتيجة وأبعادها قبل تشغيل الكود. ما الذي يجب أن يخرج لك — رقم مفرد، أم عمود واحد، أم جدول مصغر؟ كم عدد الصفوف والأعمدة؟ بالنسبة لطلب فريق المخزون، النتيجة جدول مكون من 8 صفوف وعمودين. وبالنسبة لـ "الطلب 1006"، النتيجة صف واحد. وبالنسبة لـ "سعر الطلب 1006"، النتيجة رقم مفرد هو 850.0. عندما تعرف الأبعاد المتوقعة مسبقاً، يصبح أي تحديد خاطئ واضحاً وفاقعاً بمجرد رؤية النتيجة.
4. اختر الأداة المناسبة بناءً على ما تعرفه. هذه القائمة تلخص الفصل بأكمله في صورة واحدة:
- تعرف أسماء الأعمدة → الأقواس المربعة:
orders["quantity"]،orders[["product", "quantity"]] - تعرف تسمية الصف (اسمه في الفهرس) →
.loc:orders.loc[2] - تعرف موضع الصف — الأول، الأخير، الخامس →
.iloc:orders.iloc[-3:] - تحتاج الصفوف والأعمدة معاً →
.loc[rows, cols]أو.iloc[rows, cols]:orders.loc[2:4, ["product", "price"]] - تحتاج خلية واحدة بالضبط →
.at[row, col]أو.loc[row, col]:orders.at[5, "price"]
5. حدد كيف ستتحقق من النتيجة. بعد أي عملية تحديد، اطبع .shape أو type() للنتيجة وقارنها بما تخيلته مسبقاً. سطر واحد فقط كفيل باكتشاف معظم الأخطاء الواردة أدناه قبل أن تتحول إلى حسابات رقمية خاطئة.
تستعرض بقية أقسام الفصل هذه الأدوات واحدة تلو الأخرى، مع أمثلة عملية ومحلولة لكل منها.
عمود واحد: اسم داخل أقواس مربعة
ضع اسم العمود داخل أقواس مربعة وستحصل على ذلك العمود:
quantity = orders["quantity"]
print(type(quantity))
print(quantity)<class 'pandas.Series'>
0 12
1 5
2 2
3 7
4 30
5 1
6 20
7 4
Name: quantity, dtype: int64ما تحصل عليه هو كائن Series — تماماً مثل الكائن الذي درسناه في الفصل الثاني: قيم، وفهرس يمتد على اليسار، واسم Name (العمود الذي استُخرجت منه)، ونوع بيانات واحد dtype. لاحظ أن الفهرس جاء مرافقاً للعمود؛ فالأرقام من 0 إلى 7 ليست مجرد زينة، بل تدل على الصف الذي تنتمي إليه كل قيمة.
ولأنه كائن Series، فإن كل دوال Series تعمل عليه مباشرة:
print(orders["quantity"].sum())81هذا هو السبب المعتاد لتحديد عمود واحد: طرح سؤال تحليلي أو حسابي عليه.
عدة أعمدة: قائمة داخل الأقواس المربعة
لتلبية طلب فريق المخزون، تحتاج إلى عمودين. ضع قائمة (list) من الأسماء داخل الأقواس المربعة:
stock = orders[["product", "quantity"]]
print(type(stock))
print(stock)<class 'pandas.DataFrame'>
product quantity
0 pen 12
1 notebook 5
2 bag 2
3 bottle 7
4 eraser 30
5 bag 1
6 pen 20
7 bottle 4الأقواس المزدوجة ليست صياغة خاصة بذاتها، ومن المهم إدراك ذلك بوضوح. الزوج الخارجي هو أداة التحديد؛ أما الزوج الداخلي فهو مجرد قائمة بايثون عادية ["product", "quantity"]. كان بإمكانك إنشاء القائمة أولاً ثم تمريرها — wanted = ["product", "quantity"] ثم orders[wanted] — وستعطي نفس المعنى تماماً. تعود الأعمدة بنفس ترتيب القائمة، ولذلك تُستخدم هذه الطريقة أيضاً لإعادة ترتيب الأعمدة.
النتيجة هي كائن DataFrame، لأن الجدول المكون من عمودين هو جدول متكامل. وهنا يوجد فارق يقع فيه الكثيرون لشهور: القائمة التي تحتوي على اسم واحد فقط تعيد أيضاً كائن DataFrame.
print(orders["product"].shape)
print(orders[["product"]].shape)(8,)
(8, 1)(8,) يمثل Series — بُعد واحد، ثماني قيم. بينما (8, 1) يمثل DataFrame بثمانية صفوف وعمود واحد. يختلفان في طريقة الطباعة، وفي الدوال المتاحة لكل منهما، وعند حفظهما في ملف، يحتفظ DataFrame بترويسة العمود. القاعدة بسيطة: الاسم المفرد يعطي Series؛ والقائمة تعطي DataFrame — مهما كان عدد الأسماء داخل القائمة.
لماذا لا نستخدم orders.quantity؟
ستشاهد في العديد من الدروس طريقة الوصول للأعمدة باستخدام النقطة (Dot Notation):
print(orders.branch.head(2))0 north
1 south
Name: branch, dtype: strهذه الطريقة تعمل هنا وهي أقصر في الكتابة. لكنها تعمل فقط عندما يكون اسم العمود صدفةً اسماً صالحاً كمعرّف في بايثون وغير مستخدم لشيء آخر — مع العلم أن DataFrame يحتوي على مئات الدوال والخصائص الجاهزة. إليك جدولاً صغيراً سُميت أعمدته count و size، وهما اسمان شائعان ومنطقيان جداً لأي متجر:
d = pd.DataFrame({"count": [1, 2], "size": [3, 4]})
print(d.size)
print(d["size"])4
0 3
1 4
Name: size, dtype: int64لم تُرجع d.size العمود على الإطلاق؛ بل أرجعت خاصية size المدمجة في DataFrame نفسه — أي عدد الخلايا الإجمالي، 2 × 2 = 4 — بصمت تام، ودون أي خطأ ينبهك إلى النتيجة الخاطئة. علاوة على ذلك، فإن أي اسم يحتوي على مسافة، مثل unit price، لا يمكن كتابته باستخدام النقطة إطلاقاً. الأقواس المربعة خالية تماماً من هذه المشاكل، ولذلك فإن القاعدة الأساسية لأي كود تعتمد عليه هي: استخدم دائماً df["col"]. كتابة النقطة مقبولة فقط للاستكشاف السريع في الطرفية، ليس إلا.
الصفوف بحسب التسمية (Label): .loc
لا يتم تحديد الصفوف باستخدام الأقواس المربعة العادية. (فالأقواس العادية مع اسم تعني "عموداً"، ولن يخمن بانداس قصدك). للصفوف أداتان مخصصتان، وأولاهما هي .loc — اختصار لـ label-based location (الموضع المعتمد على التسمية). أنت تمرر لها التسمية من الفهرس:
print(orders.loc[2])order_id 1003
date 2024-03-02
branch north
product bag
category accessories
quantity 2
price 850.0
Name: 2, dtype: objectيعود الصف الواحد ككائن Series معروضاً بالعرض: أصبحت أسماء الأعمدة هي الفهرس الخاص به، واسمه Name هو تسمية الصف، 2.
انظر إلى نوع البيانات dtype: object. العمود الواحد له نوع بيانات موحد، لكن الصف يمتد عبر أعمدة مختلفة — رقم، ونص، وقيمة عشرية — لذا فإن النوع الوحيد القادر على احتوائها معاً هو النوع العام object. وهذا أحد أسباب تفكير بانداس بالأعمدة: الصف يخلط الأنواع، بينما العمود لا يفعل.
والآن إلى الجزء الأهم: اطلب من .loc نطاقاً من التسميات عبر التقطيع (slice):
print(orders.loc[2:4])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
4 1005 2024-03-03 north eraser stationery 30 8.0ثلاثة صفوف: 2 و 3 و 4. شريحة التقطيع في .loc تتضمن نهايتها. يفاجئ هذا كل من يعرف لغة بايثون، حيث تتوقف range(2, 4) و list[2:4] قبل الوصول إلى 4. هذا ليس تناقضاً، بل وراءه سبب وجيه.
التسميات ليست دائماً أرقاماً. لو كانت التسميات أسماء منتجات، لكنت كتبت شيئاً مثل loc["bag":"eraser"]. ولكي تستثني النهاية، كان سيتعين عليك تحديد اسم التسمية التالية لـ "eraser" — ومع الأسماء، لا يوجد "اسم تالٍ" يمكنك معرفته دون البحث في الفهرس. لذلك، مع التسميات، أنت تحدد أول عنصر تريده وآخر عنصر تريده، وتحصل عليهما معاً. أما المواضع فالأمر مختلف معها: الموضع التالي لـ 4 هو دائماً 5، لذلك قاعدة بايثون بالتوقف قبل النهاية لا تكلف شيئاً هناك.
الصفوف بحسب الموضع (Position): .iloc
الأداة الثانية هي .iloc — اختصار لـ integer location (الموضع المعتمد على الأرقام الصحيحة). تتجاهل هذه الأداة التسميات تماماً وتعد المواضع بدءاً من 0، تماماً مثل قوائم بايثون:
print(orders.iloc[2:4])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صفان فقط، الموضعان 2 و 3؛ بينما الموضع 4 مستبعد تماماً كما هو الحال في بايثون عامة. قارن المخرجين معاً: أعطت loc[2:4] ثلاثة صفوف بينما أعطت iloc[2:4] صفين، على نفس الجدول وبنفس الأرقام داخل الأقواس.
ولأن .iloc تعتمد على الموضع، فإن الأرقام السالبة تبدأ العد من النهاية — وهو ما يجيب مباشرة عن سؤال مديرك:
print(orders.iloc[-3:])order_id date branch product category quantity price
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0سؤال "آخر ثلاثة" هو سؤال عن الموضع والترتيب، لذلك فإن .iloc هي الأداة المناسبة له تماماً. بينما .loc لا تملك أي فكرة عما يعنيه "الأخير"، فهي تفهم الأسماء فقط.
التسميات والمواضع ليسا نفس الشيء
حتى الآن، كان الصف ذو التسمية 2 والصف في الموضع 2 هما نفس الصف، لذلك لم يكن بإمكانك التفريق بين الأداتين من خلال النتائج. خذ آخر ثلاثة طلبات كجدول مستقل، وسترى كيف يفترقان:
last3 = orders.tail(3)
print(last3)order_id date branch product category quantity price
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0احتفظت الصفوف بتسمياتها الأصلية: 5 و 6 و 7. يتعمد بانداس عدم إعادة ترقيمها — فالتسمية هي الطريقة التي يظل بها الصف محتفظاً بهويته حتى بعد اقتطاع الجدول. والآن اطلب "الصف الأول" بكلتا الطريقتين:
print(last3.iloc[0])order_id 1006
date 2024-03-03
branch south
product bag
category accessories
quantity 1
price 850.0
Name: 5, dtype: objectlast3.loc[0]KeyError: 0تعني .iloc[0] "الصف الموجود في المركز الأول"، وهو الطلب ذو التسمية 5. بينما تعني .loc[0] "الصف الذي تسميته 0" — ولا يوجد صف كهذا في هذا الجدول، ولذلك يُصدر بانداس خطأ KeyError. هذا هو الفخ الذي يقع فيه المطورون لاحقاً في الدورة: بعد تصفية الصفوف أو فرزها — وكلاهما قادم لاحقاً — تتغير المواضع بينما تظل التسميات ثابتة. إليك القاعدة التي يجب أن تصحبك دائماً:
- إذا كان السؤال عن الموضع والترتيب — الأول، الأخير، أعلى ثلاثة بعد الترتيب؟ استخدم
.iloc. - إذا كان السؤال عن الهوية والتسمية — هذا الطلب بعينه، هذا المنتج؟ استخدم
.loc.
والحالة السيئة ليست ظهور KeyError؛ بل الحالة الأسوأ هي عندما تكون التسمية 0 موجودة بالفعل في مكان آخر من الجدول: عندئذٍ ستعيد .loc[0] صفاً مختلفاً بهدوء ودون أي تنبيه، بدلاً من الصف الذي قصدته.
تطلب أداة.locاسماً، بينما تطلب أداة.ilocموضعاً. في الجدول المقروء حديثاً تتطابق التسميات والمواضع مصادفةً، ولذلك يمر الكود الذي يخلط بينهما دون مشاكل في البداية — حتى إذا ما قُطع الجدول أو رُتب أو صُفي لأول مرة، بدأ يعيد صفوفاً خاطئة تماماً دون إظهار أي خطأ.
منح الصفوف تسميات ذات معنى: set_index
سأل المحاسب عن الطلب 1006. مع الفهرس الافتراضي، كان سيتعين عليك معرفة أن الطلب 1006 يقع تحت التسمية 5 — وهي معلومة تخص ترتيب الملف، وليست معلومة تخص الطلب نفسه. الاسم الحقيقي للصف هو رقم طلبه، لذا اجعل رقم الطلب هو الفهرس:
by_id = orders.set_index("order_id")
print(by_id.head(3))
print(by_id.shape)date branch product category quantity price
order_id
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
(8, 6)تغيرت ثلاثة أمور: انتقلت أرقام الطلبات إلى أقصى اليسار لتصبح هي الفهرس. وأصبح للفهرس اسم مطبوع فوقه في سطر منفصل وهو order_id. وتغيرت الأبعاد إلى (8, 6) بدلاً من (8, 7) — فـ order_id لم يعد عموداً عادياً، بل أصبح الفهرس. لاحظ أيضاً أن set_index أعادت جدولاً جديداً أسميناه by_id؛ بينما ظل الجدول الأصلي orders دون أي تعديل.
والآن تتحدث .loc بلغة السؤال نفسه:
print(by_id.loc[1006])date 2024-03-03
branch south
product bag
category accessories
quantity 1
price 850.0
Name: 1006, dtype: objectوأصبح تقطيع التسميات الآن عبارة عن نطاق من أرقام الطلبات، شاملاً كلا الطرفين:
print(by_id.loc[1003:1005, ["product", "quantity"]])product quantity
order_id
1003 bag 2
1004 bottle 7
1005 eraser 30لم يتغير عمل .iloc على الإطلاق — فالمواضع تظل مواضع ثابتة أياً كانت التسميات:
print(by_id.iloc[0:2])date branch product category quantity price
order_id
1001 2024-03-01 north pen stationery 12 15.0
1002 2024-03-01 south notebook stationery 5 60.0هنا تمايزت الأداتان بوضوح: فـ by_id.loc[0] ستعطي الآن خطأ KeyError لأن 0 ليس رقم طلب، بينما by_id.iloc[0] تشير إلى الطلب 1001. وإذا احتجت يوماً إلى استعادة رقم الطلب كعمود عادي، فإن by_id.reset_index() تعيده إلى مكانه السابق.
اختر عموداً تكون قيمه فريدة غير مكررة (unique). الغرض من الفهرس هو تسمية الصفوف وتمييزها، والاسم المشترك بين صفين لا يمكنه الإشارة إلى صف واحد بعينه. راقب ما يحدث مع عمود product الذي تتكرر فيه المنتجات:
by_product = orders.set_index("product")
print(by_product.index.is_unique)
print(by_product.loc["bag"])False
order_id date branch category quantity price
product
bag 1003 2024-03-02 north accessories 2 850.0
bag 1006 2024-03-03 south accessories 1 850.0لا يظهر أي خطأ — لكن loc["bag"] أعادت كائن DataFrame من صفين بدلاً من صف واحد كـ Series. فالكود الذي كان يتوقع صفاً واحداً (مثلاً لقراءة سعره price كرقم مفرد) أصبح يستقبل صفين الآن. ولهذا السبب يستحق الأمر سطر فحص واحد باستخدام index.is_unique متى ما قمت بتعيين فهرس تنوي البحث في صفوفه لاحقاً.
الصفوف والأعمدة معاً
تقبل كل من .loc و .iloc قيمتين مفصولتين بفواصل: الصفوف أولاً، ثم الأعمدة ثانياً. ويمكن أن يكون كل جزء منهما تسمية مفردة، أو قائمة، أو شريحة تقطيع (slice):
print(orders.loc[2:4, ["product", "price"]])product price
2 bag 850.0
3 bottle 120.0
4 eraser 8.0print(orders.loc[[0, 3], ["branch", "product"]])branch product
0 north pen
3 east bottleيعمل التقطيع مع الأعمدة أيضاً وفق قاعدة .loc الشاملة لطرفي النطاق — "كل عمود من branch إلى category". وتدل النقطتان الرأسيتان بمفردهما : على "جميع الصفوف":
print(orders.loc[:, "branch":"category"])branch product category
0 north pen stationery
1 south notebook stationery
2 north bag accessories
3 east bottle accessories
4 north eraser stationery
5 south bag accessories
6 east pen stationery
7 north bottle accessoriesتقوم .iloc بالأمر نفسه ولكن بالمواضع. يتم ترقيم الأعمدة أيضاً بدءاً من 0، لذا فإن branch هو العمود رقم 2، و product هو العمود رقم 3:
print(orders.iloc[[0, 3], [2, 3]])branch product
0 north pen
3 east bottleنفس النتيجة السابقة التي حصلنا عليها مع .loc — ولكن لاحظ كم يصعب قراءتها وفهمها. الأرقام [2, 3] لا تفصح عن شيء؛ بينما ["branch", "product"] واضحة وصريحة، وستظل تعمل حتى لو أضاف شخص ما عموداً جديداً في بداية الملف. لذلك، يفضل دائماً استخدام الأسماء متى ما كانت معروفة لديك.
ما هو الشكل (Shape) الذي ستحصل عليه؟
يعتمد حصولك على قيمة مفردة، أو كائن Series، أو كائن DataFrame، فقط على ما تضعه على جانبي الفاصلة. التسمية المفردة تلغي ذلك البُعد؛ بينما القائمة أو شريحة التقطيع تحافظ عليه:
print(type(by_id.loc[1006, "price"]).__name__)
print(type(by_id.loc[1006, ["product", "price"]]).__name__)
print(type(by_id.loc[[1003, 1006], "price"]).__name__)
print(type(by_id.loc[[1003, 1006], ["product", "price"]]).__name__)float64
Series
Series
DataFrame- تسمية مفردة، تسمية مفردة → قيمة مفردة
- تسمية مفردة، قائمة أو شريحة → كائن
Series(صف واحد) - قائمة أو شريحة، تسمية مفردة → كائن
Series(عمود واحد) - قائمة أو شريحة، قائمة أو شريحة → كائن
DataFrame
هذه هي الخطوة 3 من التخطيط المسبق — تخيل شكل النتيجة — وقد تحولت إلى قاعدة ثابتة. فإذا أردت الحصول على جدول، حتى لو كان يحتوي على صف واحد فقط، ضع قائمة على كلا الجانبين: by_id.loc[[1006], ["price"]].
قيمة مفردة: .loc و .at
سؤال المحاسب — كم كانت تكلفة الطلب 1006؟ — هو خلية واحدة: تسمية صف واسم عمود.
print(by_id.loc[1006, "price"])
print(by_id.at[1006, "price"])850.0
850.0تعطي كلتاهما نفس الرقم. تقبل .at صفاً واحداً وعموداً واحداً فقط ولا تقبل أي شيء آخر، مما يجعلها أسرع قليلاً ويوضح قصدك البرمجي تماماً: هذه خلية واحدة محددة. (والنظير الموضعي لها هو .iat، مثل by_id.iat[5, 5]). استخدم ما تراه أوضح في القراءة؛ فاستخدام .loc ممتاز أيضاً.
القيمة المفردة هي رقم عادي، لذا يمكنك إجراء العمليات الحسابية عليها مباشرة. القيمة الإجمالية للطلب 1004 — الكمية مضروبة في السعر:
print(by_id.at[1004, "quantity"] * by_id.at[1004, "price"])840.0التحديد كائن مستقل بذاته
هناك أمر أخير يجب فهمه قبل استخدام التحديد في برامج حقيقية: ما الذي يحدث عندما تُعدل جزءاً محدداً.
import pandas as pd
orders = pd.read_csv("orders.csv")
prices = orders["price"]
prices[0] = 999.0
print(prices.head(2))
print(orders.loc[0, "price"])0 999.0
1 60.0
Name: price, dtype: float64
15.0تغيرت prices؛ بينما لم تتغير orders على الإطلاق. في بانداس 3، يتصرف كل تحديد كنسخة مستقلة بذاتها: يمكنك تعديلها بحرية دون المساس بالجدول الأصلي إطلاقاً. (داخلياً يتجنب بانداس نسخ البيانات فعلياً حتى لحظة الكتابة والتعديل — وهي الآلية المعروفة بـ النسخ عند الكتابة Copy-on-Write — لكن ما يهمك هنا هو السلوك الظاهر، وليس الآلية الداخلية).
ينطبق الأمر نفسه تماماً على تحديد عدة أعمدة:
stock = orders[["product", "price"]]
stock["price"] = 0
print(orders["price"].head(2))0 15.0
1 60.0
Name: price, dtype: float64هذا هو السلوك الآمن والمثالي؛ فالدالة التي تستقبل تحديداً معيناً وتعدله لا يمكنها إتلاف جدولك الأساسي من وراء ظهرك. ولكن يترتب على هذا السلوك أمر جوهري يجب أن تعرفه: إذا كنت تقصد تعديل الجدول الأصلي، فيجب عليك إجراء التعديل على الجدول الأصلي نفسه، في خطوة واحدة، باستخدام .loc:
orders.loc[0, "price"] = 16.0
print(orders.loc[0, "price"])16.0أما الطريقة المغرية المكونة من خطوتين — تحديد عمود أولاً، ثم تعيين قيمة لخلية بداخله — فهي لا تعمل، وينبهك بانداس 3 إلى ذلك صراحة:
orders["price"][0] = 1.0ChainedAssignmentError: 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-assignmentprint(orders.loc[0, "price"])16.0لا تزال القيمة 16.0 — فقد ذهب التعديل إلى نسخة مؤقتة وتم التخلص منها بعد ذلك. أنتجت orders["price"] تحديداً مستقلاً، وقامت [0] = 1.0 بتعديل ذلك التحديد، وليس orders. يُعرف هذا بـ التعيين المتسلسل (Chained Assignment): كتابة زوجين متتاليين من الأقواس المربعة على يسار علامة =. والحل دائماً هو ما ذكرته رسالة التحذير بدقة: استخدام أمر .loc واحد يحدد كلاً من الصف والعمود معاً: orders.loc[0, "price"] = 1.0.
إذا قرأت شروحات قديمة، فستجد هذا الموضوع مشروحاً مع تحذير SettingWithCopyWarning ونصائح تذكر أن التعديل قد يصيب الأصل أحياناً وقد لا يصيبه أحياناً أخرى. كان ذلك في إصدارات بانداس 1 و 2. أما في بانداس 3 فالقاعدة واضحة وثابتة دائماً: التحديد لا يغير الأصل أبداً؛ واستخدام .loc على الجدول الأصلي هو ما يغيره.
مثال تطبيقي متكامل
إليك إجابة الطلبات الثلاثة التي وردت في بداية الفصل، مجموعة في برنامج برمجي متكامل واحد. select_orders.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# 1. Check what arrived before pointing into it
print("Shape :", orders.shape)
print("Columns:", list(orders.columns))
# 2. Name the rows by what they are
by_id = orders.set_index("order_id")
print("Order ids unique:", by_id.index.is_unique)
print()
# Stock team: two columns, every row
stock = orders[["product", "quantity"]]
print("Stock view:", stock.shape)
print(stock.head(3))
print()
# Accountant: one order by its number, then one cell
print(by_id.loc[1006, ["product", "quantity", "price"]])
print("Order 1006 total:", by_id.at[1006, "quantity"] * by_id.at[1006, "price"])
print()
# Manager: the last three orders, a question about position
print(by_id.iloc[-3:, [1, 2, 4]])Shape : (8, 7)
Columns: ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
Order ids unique: True
Stock view: (8, 2)
product quantity
0 pen 12
1 notebook 5
2 bag 2
product bag
quantity 1
price 850.0
Name: 1006, dtype: object
Order 1006 total: 850.0
branch product quantity
order_id
1006 south bag 1
1007 east pen 20
1008 north bottle 4لماذا كُتب الكود بهذا النمط:
- يفحص البيانات قبل الإشارة إليها. طُبعت أبعاد
shapeوقائمة الأعمدةlist(columns)أولاً، بحيث تظهر أي مشكلة إملائية أو مسافة غير مرئية في أسماء الأعمدة قبل أن يفشل أي استعلام. - يتأكد من أن الفهرس فريد (unique) قبل استخدامه في البحث عن الطلبات، مما يضمن أن
by_id.loc[1006]ستعيد صفاً واحداً بالضبط وby_id.at[...]رقماً مفرداً. - يستخدم كل استعلام الأداة المناسبة لسؤاله. تحديد الأعمدة بأسمائها باستخدام الأقواس؛ والبحث عن الطلب بـ هويته باستخدام
.locو.at؛ وطلب "آخر ثلاثة" بـ موضعها باستخدام.iloc. - يطبع أبعاد عرض المخزون — فالشكل
(8, 2)يطابق تماماً التخطيط الأولي: كل طلب، بعمودين. - التحديد الأخير هو ما يمكن تحسينه. فالأرقام
[1, 2, 4]تشير مصادفةً إلىbranchوproductوquantityفيby_id— لاحظ إزاحة المواضع بمقدار خانة واحدة لأنorder_idأصبح هو الفهرس. المواضع هشة وعرضة للكسر. وفي كودك الخاص، فإن كتابةby_id.iloc[-3:][["branch", "product", "quantity"]]أوby_id.loc[by_id.index[-3:], ["branch", "product", "quantity"]]توضح نفس المعنى بدقة باستخدام الأسماء.
الأخطاء الشائعة وحلولها
خطأ KeyError: 'Price' لا يوجد عمود بهذا الاسم تحديداً. أسماء الأعمدة حساسة لحالة الأحرف (case-sensitive): فالاسمان price و Price مختلفان تماماً. اطبع list(df.columns) وانسخ الاسم الصحيح من هناك. وعند تمرير قائمة أسماء مثل orders[["product", "Price"]] تصبح الرسالة KeyError: "['Price'] not in index" وتوضح تحديداً الاسم المفقود؛ أما عند استخدام النقطة orders.Price فالخطأ يكون AttributeError: 'DataFrame' object has no attribute 'Price'. نفس السبب، ونفس الحل.
خطأ KeyError: 'price' — رغم أن price موجود بوضوح في الملف تحتوي الترويسة غالباً على مسافات تلي الفواصل مثل order_id, product, price، وأصبحت تلك المسافات جزءاً من أسماء الأعمدة:
from io import StringIO
import pandas as pd
raw = "order_id, product, price\n1001, pen, 15.0\n"
padded = pd.read_csv(StringIO(raw))
print(list(padded.columns))
padded["price"]['order_id', ' product', ' price']
KeyError: 'price'تُظهر القائمة المسافة البادئة بوضوح في ' price'، وهذا هو السبب الدقيق الذي جعل الفصل الثالث يوجهك لطباعة الأعمدة كقائمة. أصلح ذلك أثناء القراءة عبر pd.read_csv("file.csv", skipinitialspace=True)، أو بعد القراءة عبر df.columns = df.columns.str.strip().
خطأ KeyError: ('product', 'price') لقد كتبت orders["product", "price"] — بزوج واحد من الأقواس. وبدون القائمة الداخلية، يمرر بايثون الاسمين كـ tuple مفرد، ويبحث بانداس عن عمود واحد بهذا الاسم المركب ('product', 'price'). اكتب بدلاً من ذلك: orders[["product", "price"]].
خطأ KeyError: 0 الصادر عن .loc لقد استخدمت موضعاً مع .loc. إما أن الجدول تم اقتطاعه (tail، أو تصفية، أو ترتيب) فاختفت التسمية 0، أو أنك قمت بتعيين فهرس وأصبحت التسميات هي أرقام الطلبات. إذا كنت تقصد "الصف الأول"، فاكتب .iloc[0]. وفي جدول يكون الفهرس فيه نصياً، فإن ارتكاب نفس الخطأ مع شريحة تقطيع — مثل orders.set_index("product").loc[0:2] — يؤدي إلى خطأ TypeError: cannot do slice indexing on Index with these indexers [0] of type int؛ والحل هو نفسه أيضاً: .iloc[0:2].
خطأ IndexError: single positional indexer is out-of-bounds تم تزويد .iloc بموضع غير موجود — مثل orders.iloc[8] على جدول من ثمانية صفوف تمتد مواضعه من 0 إلى 7. تحقق من df.shape؛ فالصف الأخير هو دائماً df.iloc[-1]. وتظهر نفس الرسالة عند إدخال موضع عمود يتجاوز الحدود مثل orders.iloc[:, 7].
خطأ ValueError: Location based indexing can only have [integer, integer slice (START point is INCLUDED, END point is EXCLUDED), listlike of integers, boolean array] types لقد مررت لـ .iloc اسم عمود: orders.iloc[0, "branch"]. أداة .iloc لا تقبل سوى أرقام المواضع. استخدم .loc مع التسمية — orders.loc[0, "branch"] — أو إذا كنت بحاجة فعلية للموضع فابحث عنه برمجياً: orders.columns.get_loc("branch") التي تعيد 2.
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))
print(len(orders.loc[2:5]), len(orders.iloc[2:5]))- A4 4
- B3 3
- C4 3
- D3 4
المقصود من هذا الكود طباعة منتج الطلب الأول من بين الطلبات الثلاثة الأخيرة. ما الذي يحدث فعلياً، ولماذا؟
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))
last3 = orders.tail(3)
# Wanted: the product of the first of these three orders
print(last3.loc[0, "product"])- Aيطبع `pen`، وهو المنتج الموجود في الصف الأول من الجدول الأصلي
- Bيطبع `bag`، وهو منتج الصف الأول من بين الصفوف الثلاثة
- C`KeyError: 0` — احتفظت `tail(3)` بالتسميات 5 و 6 و 7، بينما تبحث `.loc` عن التسمية 0
- D`IndexError` — الجدول يحتوي على ثلاثة صفوف فقط
تحتاج إلى DataFrame يحتوي فقط على عمود price — وليس Series. أي سطر يعطيك ذلك؟
- Aorders["price"]
- Borders.price
- Corders.loc[:, "price"]
- Dorders[["price"]]
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، اكتب برنامجاً باسم select.py. قبل كتابة أي كود، اكتب في تعليق ما يجب أن يعيده كل استعلام — قيمة مفردة، أم Series، أم DataFrame — وكم عدد الصفوف. يجب أن يستوفي البرنامج أربعة شروط:
- تُحدد الأعمدة دائماً بالاسم: اطبع
shapeلأعمدةdateوbranchوquantityمعاً. - يتم البحث عن الطلبات بهويتها في جدول مفهرس بـ
order_id، بعد التأكد من أن الفهرس فريد: اطبع الصف الكامل للطلب 1007، ثم اطبع عموديproductوbranchللطلبات من 1002 إلى 1005 — الأربعة جميعاً. - الإجابة عن "الأول" و "الأخير" تكون بالموضع: اطبع عمودي
productوpriceلأول طلبين ولآخر طلبين؛ ثم باستخدام.atاطبع كمية الطلب 1005 وإجمالي قيمة الطلب 1003 (الكمية × السعر). - غيّر سعر الطلب 1001 إلى
16.0في الجدول المفهرس باستخدام أمر.locمفرد، واطبعه لتثبت حدوث التغيير.
يجب أن تكون مخرجاتك مطابقة تماماً لما يلي:
(8, 3)
unique: True
date 2024-03-04
branch east
product pen
category stationery
quantity 20
price 15.0
Name: 1007, dtype: object
product branch
order_id
1002 notebook south
1003 bag north
1004 bottle east
1005 eraser north
product price
order_id
1001 pen 15.0
1002 notebook 60.0
product price
order_id
1007 pen 15.0
1008 bottle 120.0
qty of 1005: 30
total of 1003: 1700.0
price of 1001: 16.0بعد ذلك، توقع النتيجة واكتب توقعك في ورقة، وفقط بعد ذلك قم بتشغيل كل سطر من هذه الأسطر الثلاثة:
by_id.loc[0]orders.iloc[8]by_id["price"][1001] = 99.0— وبعدها اطبع سعر الطلب 1001 مرة أخرى
الحل
خطط أولاً. كتلة التعليقات هي التخطيط الأولي من الخطوة 3 في "قبل أن تكتب الكود" — ما يجب أن تعيده كل عملية تحديد:
- ثلاثة أعمدة →
orders[[...]]→ كائنDataFrameبأبعاد(8, 3) - طلب واحد، بهويته →
by_id.loc[1007]→ كائنSeries(صف واحد) - أربعة طلبات بهويتها، وعمودان →
by_id.loc[1002:1005, [...]]→ كائنDataFrameبأبعاد 4 × 2، لأن.locتتضمن 1005 - أول اثنين وآخر اثنين بالموضع →
.iloc[:2]و.iloc[-2:]→ كائنان من نوعDataFrameبأبعاد 2 × 2 - خلايا مفردة →
.at→ أرقام عادية - تعديل الأصل → أمر
.locواحد علىby_id→ يتغير الجدول الأصلي نفسه
select.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Columns by name -> a DataFrame of 8 rows, 3 columns
print(orders[["date", "branch", "quantity"]].shape)
# Rows named by their order number; check the names are unique first
by_id = orders.set_index("order_id")
print("unique:", by_id.index.is_unique)
# One order by identity -> a Series; four orders -> 4 x 2 (loc includes 1005)
print(by_id.loc[1007])
print(by_id.loc[1002:1005, ["product", "branch"]])
# First two and last two by place -> .iloc for rows, names for columns
print(by_id.iloc[:2][["product", "price"]])
print(by_id.iloc[-2:][["product", "price"]])
# Single cells -> plain numbers
print("qty of 1005:", by_id.at[1005, "quantity"])
print("total of 1003:", by_id.at[1003, "quantity"] * by_id.at[1003, "price"])
# Change the original, in one step
by_id.loc[1001, "price"] = 16.0
print("price of 1001:", by_id.at[1001, "price"])(8, 3)
unique: True
date 2024-03-04
branch east
product pen
category stationery
quantity 20
price 15.0
Name: 1007, dtype: object
product branch
order_id
1002 notebook south
1003 bag north
1004 bottle east
1005 eraser north
product price
order_id
1001 pen 15.0
1002 notebook 60.0
product price
order_id
1007 pen 15.0
1008 bottle 120.0
qty of 1005: 30
total of 1003: 1700.0
price of 1001: 16.0تتطابق كل نتيجة تماماً مع الخطة: (8, 3) لثلاثة أعمدة، وأربعة طلبات لـ 1002:1005 لأن .loc تتضمن نهاية النطاق، وصفان لكل من التحديدين الموضعيين. في الشرط 3، استُخرجت الصفوف بالموضع والأعمدة بالاسم — .iloc للصفوف، ثم قائمة أسماء للأعمدة — ليعبر الكود بوضوح تام عما يقصده.
والآن مع التوقعات الثلاثة:
by_id.loc[0]KeyError: 0أصبحت التسميات الآن أرقام طلبات؛ ولا يوجد طلب برقم 0. والصف الأول بحسب الموضع كان سيكتب by_id.iloc[0].
orders.iloc[8]IndexError: single positional indexer is out-of-boundsثمانية صفوف تعني المواضع من 0 إلى 7. والصف الأخير هو orders.iloc[7]، أو الأفضل orders.iloc[-1].
by_id["price"][1001] = 99.0ChainedAssignmentError: 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-assignmentprint(by_id.at[1001, "price"])16.0لا تزال القيمة 16.0: صنعت by_id["price"] تحديداً مستقلاً وذهبت 99.0 إليه، وليس إلى by_id. نجح الشرط 4 وفشل هذا، والفرق هو أمر .loc واحد مقابل زوجين متتاليين من الأقواس في سطر واحد.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz