الفصل 04

اختيار الأعمدة والصفوف — بالاسم أو بالموضع

تحديد جزء من الجدول: اختيار الأعمدة بالأقواس المربعة، والصفوف بالتسمية عبر loc وبالموضع عبر iloc، والخلايا المفردة عبر at — ومعرفة أي الطريقتين تستخدم دائماً.

45 دقيقةPython 3.12
  1. 1المشكلة
  2. 2الفهم
  3. 3أمثلة محلولة
  4. 4التوقع
  5. 5التطبيق
  6. 6التحدي

المشكلة التي نقوم بحلها

لقد قمت بقراءة ملف طلبات المتجر وفحصه بدقة، تماماً كما تعلمت في الفصل الثالث. والآن بدأ الزملاء يطرحون عليك أسئلة محددة حول هذه البيانات.

يقول فريق المخزون: "أرسل لنا فقط اسم المنتج والكمية لكل طلب — لا نحتاج لأي شيء آخر." ويقول المحاسب: "ما هو الطلب رقم 1006 تحديداً، وما هي تكلفته الإجمالية؟" بينما يطلب منك مديرك: "أرني آخر ثلاثة طلبات وردت إلينا."

لا يتطلب أي من هذه الأسئلة إجراء عمليات حسابية على الجدول بأكمله؛ بل يتعلق كل منها بـ الإشارة والتحديد (pointing): الإشارة إلى أعمدة معينة، أو إلى صف واحد بعينه، أو إلى الصفوف القليلة الأخيرة، أو إلى خلية مفردة. إن طباعة الجدول بأكمله وتتبعه بإصبعك قد يفي بالغرض إذا كان الجدول يحتوي على ثمانية صفوف فقط؛ لكنه لا ينجح إطلاقاً مع ثمانية آلاف صف، وبالتأكيد لا يعمل داخل برنامج برمجي لا يملك إصبعاً لتتبع الشاشة.

يوفر بانداس عدة طرق للإشارة إلى البيانات، والسبب في وجود عدة طرق هو الجوهر الحقيقي لهذا الفصل. يمكن الإشارة إلى أي صف بطريقتين مختلفتين تماماً: إما عبر اسمه (تسميته أو معرّفه في الفهرس Index Label) أو عبر موضعه وترتيبه (الأول، الثاني، الأخير). عند قراءة ملف لأول مرة، يتصادف أن تكون التسميات والمواضع متطابقة في الأرقام، ولهذا لا يلاحظ المبتدئون أبداً وجود مفهومين منفصلين. لكن بمجرد اقتطاع آخر ثلاثة صفوف، أو إعادة ترتيب البيانات، أو تصفيتها، يتوقف هذا التطابق تماماً — والكود الذي يخلط بينهما سيبدأ في إعادة صفوف خاطئة تماماً دون أن يُصدر أي خطأ برمجي ينبهك.

لذلك، يتمحور هذا الفصل حول تحديد البيانات، لكنه في المقام الأول يدربك على أن تعرف دائماً وبوضوح أيّ الطريقتين تستخدم.

بنهاية هذا الفصل ستكون قادراً على

  • تحديد عمود واحد والحصول على Series، أو عدة أعمدة والحصول على DataFrame — ومعرفة أيهما بين يديك بدقة
  • تحديد الصفوف بحسب التسمية باستخدام .loc وبحسب الموضع باستخدام .iloc، وشرح سبب اختلاف نهاية نطاقات التقطيع (Slices) بينهما
  • تحديد الصفوف والأعمدة معاً باستخدام .loc[rows, columns] وتوقع أبعاد وشكل النتيجة مسبقاً — وصولاً إلى استخراج قيمة مفردة باستخدام .loc أو .at
  • منح الصفوف تسميات ذات دلالة ومعنى باستخدام set_index()، وفهم كيف يغير ذلك معنى ودلالة .loc
  • شرح سبب عدم تأثير تعديل جزء محدد على الجدول الأصلي في إصدار بانداس 3، وتعديل الجدول الأصلي بالطريقة الصحيحة؛ وفهم وإصلاح أخطاء KeyError و IndexError الناتجة عن عمليات التحديد

المتطلبات المسبقة: قراءة ملف CSV وفحص البيانات بعد قراءتها.


أنشئ الملف أولاً

داخل مجلد مشروعك، أنشئ ملفاً باسم orders.csv يحتوي بدقة على هذه الأسطر التسعة. سيُستخدم هذا الملف في بقية فصول الدورة، لذا احفظه في مكان يسهل عليك الرجوع إليه:

text
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) — أي التسميات الممتدة على الجانب الأيسر.

python
import pandas as pd

orders = pd.read_csv("orders.csv")

print(orders.shape)
print(list(orders.columns))
print(orders.index)
text
(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() للنتيجة وقارنها بما تخيلته مسبقاً. سطر واحد فقط كفيل باكتشاف معظم الأخطاء الواردة أدناه قبل أن تتحول إلى حسابات رقمية خاطئة.

تستعرض بقية أقسام الفصل هذه الأدوات واحدة تلو الأخرى، مع أمثلة عملية ومحلولة لكل منها.


عمود واحد: اسم داخل أقواس مربعة

ضع اسم العمود داخل أقواس مربعة وستحصل على ذلك العمود:

python
quantity = orders["quantity"]

print(type(quantity))
print(quantity)
text
<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 تعمل عليه مباشرة:

python
print(orders["quantity"].sum())
text
81

هذا هو السبب المعتاد لتحديد عمود واحد: طرح سؤال تحليلي أو حسابي عليه.

عدة أعمدة: قائمة داخل الأقواس المربعة

لتلبية طلب فريق المخزون، تحتاج إلى عمودين. ضع قائمة (list) من الأسماء داخل الأقواس المربعة:

python
stock = orders[["product", "quantity"]]

print(type(stock))
print(stock)
text
<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.

python
print(orders["product"].shape)
print(orders[["product"]].shape)
text
(8,)
(8, 1)

(8,) يمثل Series — بُعد واحد، ثماني قيم. بينما (8, 1) يمثل DataFrame بثمانية صفوف وعمود واحد. يختلفان في طريقة الطباعة، وفي الدوال المتاحة لكل منهما، وعند حفظهما في ملف، يحتفظ DataFrame بترويسة العمود. القاعدة بسيطة: الاسم المفرد يعطي Series؛ والقائمة تعطي DataFrame — مهما كان عدد الأسماء داخل القائمة.

لماذا لا نستخدم orders.quantity؟

ستشاهد في العديد من الدروس طريقة الوصول للأعمدة باستخدام النقطة (Dot Notation):

python
print(orders.branch.head(2))
text
0    north
1    south
Name: branch, dtype: str

هذه الطريقة تعمل هنا وهي أقصر في الكتابة. لكنها تعمل فقط عندما يكون اسم العمود صدفةً اسماً صالحاً كمعرّف في بايثون وغير مستخدم لشيء آخر — مع العلم أن DataFrame يحتوي على مئات الدوال والخصائص الجاهزة. إليك جدولاً صغيراً سُميت أعمدته count و size، وهما اسمان شائعان ومنطقيان جداً لأي متجر:

python
d = pd.DataFrame({"count": [1, 2], "size": [3, 4]})

print(d.size)
print(d["size"])
text
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 (الموضع المعتمد على التسمية). أنت تمرر لها التسمية من الفهرس:

python
print(orders.loc[2])
text
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):

python
print(orders.loc[2:4])
text
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، تماماً مثل قوائم بايثون:

python
print(orders.iloc[2:4])
text
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 تعتمد على الموضع، فإن الأرقام السالبة تبدأ العد من النهاية — وهو ما يجيب مباشرة عن سؤال مديرك:

python
print(orders.iloc[-3:])
text
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 هما نفس الصف، لذلك لم يكن بإمكانك التفريق بين الأداتين من خلال النتائج. خذ آخر ثلاثة طلبات كجدول مستقل، وسترى كيف يفترقان:

python
last3 = orders.tail(3)

print(last3)
text
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. يتعمد بانداس عدم إعادة ترقيمها — فالتسمية هي الطريقة التي يظل بها الصف محتفظاً بهويته حتى بعد اقتطاع الجدول. والآن اطلب "الصف الأول" بكلتا الطريقتين:

python
print(last3.iloc[0])
text
order_id           1006
date         2024-03-03
branch            south
product             bag
category    accessories
quantity              1
price             850.0
Name: 5, dtype: object
python
last3.loc[0]
text
KeyError: 0

تعني .iloc[0] "الصف الموجود في المركز الأول"، وهو الطلب ذو التسمية 5. بينما تعني .loc[0] "الصف الذي تسميته 0" — ولا يوجد صف كهذا في هذا الجدول، ولذلك يُصدر بانداس خطأ KeyError. هذا هو الفخ الذي يقع فيه المطورون لاحقاً في الدورة: بعد تصفية الصفوف أو فرزها — وكلاهما قادم لاحقاً — تتغير المواضع بينما تظل التسميات ثابتة. إليك القاعدة التي يجب أن تصحبك دائماً:

  • إذا كان السؤال عن الموضع والترتيب — الأول، الأخير، أعلى ثلاثة بعد الترتيب؟ استخدم .iloc.
  • إذا كان السؤال عن الهوية والتسمية — هذا الطلب بعينه، هذا المنتج؟ استخدم .loc.

والحالة السيئة ليست ظهور KeyError؛ بل الحالة الأسوأ هي عندما تكون التسمية 0 موجودة بالفعل في مكان آخر من الجدول: عندئذٍ ستعيد .loc[0] صفاً مختلفاً بهدوء ودون أي تنبيه، بدلاً من الصف الذي قصدته.

تطلب أداة .loc اسماً، بينما تطلب أداة .iloc موضعاً. في الجدول المقروء حديثاً تتطابق التسميات والمواضع مصادفةً، ولذلك يمر الكود الذي يخلط بينهما دون مشاكل في البداية — حتى إذا ما قُطع الجدول أو رُتب أو صُفي لأول مرة، بدأ يعيد صفوفاً خاطئة تماماً دون إظهار أي خطأ.

منح الصفوف تسميات ذات معنى: set_index

سأل المحاسب عن الطلب 1006. مع الفهرس الافتراضي، كان سيتعين عليك معرفة أن الطلب 1006 يقع تحت التسمية 5 — وهي معلومة تخص ترتيب الملف، وليست معلومة تخص الطلب نفسه. الاسم الحقيقي للصف هو رقم طلبه، لذا اجعل رقم الطلب هو الفهرس:

python
by_id = orders.set_index("order_id")

print(by_id.head(3))
print(by_id.shape)
text
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 بلغة السؤال نفسه:

python
print(by_id.loc[1006])
text
date         2024-03-03
branch            south
product             bag
category    accessories
quantity              1
price             850.0
Name: 1006, dtype: object

وأصبح تقطيع التسميات الآن عبارة عن نطاق من أرقام الطلبات، شاملاً كلا الطرفين:

python
print(by_id.loc[1003:1005, ["product", "quantity"]])
text
product  quantity
order_id                  
1003         bag         2
1004      bottle         7
1005      eraser        30

لم يتغير عمل .iloc على الإطلاق — فالمواضع تظل مواضع ثابتة أياً كانت التسميات:

python
print(by_id.iloc[0:2])
text
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 الذي تتكرر فيه المنتجات:

python
by_product = orders.set_index("product")

print(by_product.index.is_unique)
print(by_product.loc["bag"])
text
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):

python
print(orders.loc[2:4, ["product", "price"]])
text
product  price
2     bag  850.0
3  bottle  120.0
4  eraser    8.0
python
print(orders.loc[[0, 3], ["branch", "product"]])
text
branch product
0  north     pen
3   east  bottle

يعمل التقطيع مع الأعمدة أيضاً وفق قاعدة .loc الشاملة لطرفي النطاق — "كل عمود من branch إلى category". وتدل النقطتان الرأسيتان بمفردهما : على "جميع الصفوف":

python
print(orders.loc[:, "branch":"category"])
text
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:

python
print(orders.iloc[[0, 3], [2, 3]])
text
branch product
0  north     pen
3   east  bottle

نفس النتيجة السابقة التي حصلنا عليها مع .loc — ولكن لاحظ كم يصعب قراءتها وفهمها. الأرقام [2, 3] لا تفصح عن شيء؛ بينما ["branch", "product"] واضحة وصريحة، وستظل تعمل حتى لو أضاف شخص ما عموداً جديداً في بداية الملف. لذلك، يفضل دائماً استخدام الأسماء متى ما كانت معروفة لديك.

ما هو الشكل (Shape) الذي ستحصل عليه؟

يعتمد حصولك على قيمة مفردة، أو كائن Series، أو كائن DataFrame، فقط على ما تضعه على جانبي الفاصلة. التسمية المفردة تلغي ذلك البُعد؛ بينما القائمة أو شريحة التقطيع تحافظ عليه:

python
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__)
text
float64
Series
Series
DataFrame
  • تسمية مفردة، تسمية مفردة → قيمة مفردة
  • تسمية مفردة، قائمة أو شريحة → كائن Series (صف واحد)
  • قائمة أو شريحة، تسمية مفردة → كائن Series (عمود واحد)
  • قائمة أو شريحة، قائمة أو شريحة → كائن DataFrame

هذه هي الخطوة 3 من التخطيط المسبق — تخيل شكل النتيجة — وقد تحولت إلى قاعدة ثابتة. فإذا أردت الحصول على جدول، حتى لو كان يحتوي على صف واحد فقط، ضع قائمة على كلا الجانبين: by_id.loc[[1006], ["price"]].

قيمة مفردة: .loc و .at

سؤال المحاسب — كم كانت تكلفة الطلب 1006؟ — هو خلية واحدة: تسمية صف واسم عمود.

python
print(by_id.loc[1006, "price"])
print(by_id.at[1006, "price"])
text
850.0
850.0

تعطي كلتاهما نفس الرقم. تقبل .at صفاً واحداً وعموداً واحداً فقط ولا تقبل أي شيء آخر، مما يجعلها أسرع قليلاً ويوضح قصدك البرمجي تماماً: هذه خلية واحدة محددة. (والنظير الموضعي لها هو .iat، مثل by_id.iat[5, 5]). استخدم ما تراه أوضح في القراءة؛ فاستخدام .loc ممتاز أيضاً.

القيمة المفردة هي رقم عادي، لذا يمكنك إجراء العمليات الحسابية عليها مباشرة. القيمة الإجمالية للطلب 1004 — الكمية مضروبة في السعر:

python
print(by_id.at[1004, "quantity"] * by_id.at[1004, "price"])
text
840.0

التحديد كائن مستقل بذاته

هناك أمر أخير يجب فهمه قبل استخدام التحديد في برامج حقيقية: ما الذي يحدث عندما تُعدل جزءاً محدداً.

python
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"])
text
0    999.0
1     60.0
Name: price, dtype: float64
15.0

تغيرت prices؛ بينما لم تتغير orders على الإطلاق. في بانداس 3، يتصرف كل تحديد كنسخة مستقلة بذاتها: يمكنك تعديلها بحرية دون المساس بالجدول الأصلي إطلاقاً. (داخلياً يتجنب بانداس نسخ البيانات فعلياً حتى لحظة الكتابة والتعديل — وهي الآلية المعروفة بـ النسخ عند الكتابة Copy-on-Write — لكن ما يهمك هنا هو السلوك الظاهر، وليس الآلية الداخلية).

ينطبق الأمر نفسه تماماً على تحديد عدة أعمدة:

python
stock = orders[["product", "price"]]
stock["price"] = 0

print(orders["price"].head(2))
text
0    15.0
1    60.0
Name: price, dtype: float64

هذا هو السلوك الآمن والمثالي؛ فالدالة التي تستقبل تحديداً معيناً وتعدله لا يمكنها إتلاف جدولك الأساسي من وراء ظهرك. ولكن يترتب على هذا السلوك أمر جوهري يجب أن تعرفه: إذا كنت تقصد تعديل الجدول الأصلي، فيجب عليك إجراء التعديل على الجدول الأصلي نفسه، في خطوة واحدة، باستخدام .loc:

python
orders.loc[0, "price"] = 16.0

print(orders.loc[0, "price"])
text
16.0

أما الطريقة المغرية المكونة من خطوتين — تحديد عمود أولاً، ثم تعيين قيمة لخلية بداخله — فهي لا تعمل، وينبهك بانداس 3 إلى ذلك صراحة:

python
orders["price"][0] = 1.0
text
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
python
print(orders.loc[0, "price"])
text
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:

python
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]])
text
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، وأصبحت تلك المسافات جزءاً من أسماء الأعمدة:

python
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"]
text
['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.