الفصل 02

Series و DataFrame — المفهومان الأساسيان في بانداس

ما هو العمود وما هو الجدول، ولماذا يجب أن تتشارك كل قيم العمود نوعاً واحداً. مع توضيح الفرق الجوهري بين df["col"] و df[["col"]].

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

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

في الفصل السابق كتبنا سطراً واحداً، وقد أدى مهمته بنجاح:

python
print(sales["quantity"].sum())
text
19

قراءة هذا السطر سهلة ومباشرة، ولكن هناك سؤالاً مهماً يختبئ في داخله: ما الذي أعاده التعبير البرمجي sales["quantity"] تحديداً؟ هل أعاد قائمة (list)؟ القوائم العادية في بايثون لا تحتوي على دالة .sum() — فكتابة [12, 5, 2].sum() في بايثون تتسبب في حدوث خطأ فوري.

من الضروري جداً معرفة الإجابة، لأنك في مكتبة بانداس تتعامل دائماً مع واحد من شيئين رئيسيين، ومعظم حالات اللبس تبدأ من هذا السؤال بعينه: ما الذي أحمله بين يديّ الآن تحديداً؟ هل هو عمود واحد أم جدول كامل؟

يتناول هذا الفصل هذين الشيئين: تسمياتهما، وأبعادهما، والفرق الجوهري بينهما.

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

  • التمييز بجملة واحدة بين Series و DataFrame
  • إنشاء كائن Series وفحص فهرسه وقيمه ونوعه بشكل مستقل
  • شرح سبب إلزام جميع القيم داخل العمود الواحد بنفس النوع
  • استخدام df["col"] و df[["col"]] مع إدراك الفرق بينهما
  • قراءة خطأ KeyError وتحديد سبب المشكلة بدقة في أسماء الأعمدة

المتطلبات المسبقة: تثبيت بانداس وجدولك الأول.


Series — عمود واحد

لنبدأ بالوحدة الأصغر. كائن Series هو سلسلة متتالية من القيم، ترتبط كل قيمة منها باسم محدد:

python
import pandas as pd

quantity = pd.Series([12, 5, 2])

print(quantity)
text
0    12
1     5
2     2
dtype: int64

لقد مررنا ثلاث قيم، وحصلنا في المخرجات على ثلاثة أزواج. الأرقام 0 و 1 و 2 على اليسار تمثل الفهرس (index) — وهو بمثابة تسمية أو معرّف لكل قيمة. أنت لم تمرر هذه الأرقام؛ بل قام بانداس بالعد تلقائياً بدءاً من الصفر.

وفي الأسفل يظهر dtype: int64 — وهو نوع البيانات (data type) للسلسلة بأكملها. نوع واحد مشترك، وليس نوعاً مستقلاً لكل قيمة. هذا التوحيد هو ما يمنح بانداس سرعته الفائقة، وهو في الوقت نفسه قيده الأساسي.

يمكن فحص هذه الأجزاء الثلاثة بشكل منفصل:

python
import pandas as pd

quantity = pd.Series([12, 5, 2])

print(quantity.index)
print(quantity.values)
print(quantity.dtype)
text
RangeIndex(start=0, stop=3, step=1)
[12  5  2]
int64

يعني RangeIndex أن الفهرس هو مجرد عد تسلسلي — يبدأ من الصفر، ويزداد بمقدار واحد. وتمثل values القيم المجردة بدون أسمائها، بينما تمثل dtype نوع البيانات للعمود بأكمله.

ولا يشترط أن يكون الفهرس أرقاماً فقط:

python
import pandas as pd

quantity = pd.Series([12, 5, 2], index=["pen", "notebook", "bag"])

print(quantity)
print(quantity["notebook"])
text
pen         12
notebook     5
bag          2
dtype: int64
5

أصبحت الصفوف الآن مسماة بأسماء المنتجات، ويمكنك جلب أي قيمة بالاعتماد على ذلك الاسم. يشبه هذا سلوك القواميس (dictionaries) في بايثون، وهذا الشبه حقيقي بالفعل — لكن مع فارق جوهري هائل:

python
import pandas as pd

quantity = pd.Series([12, 5, 2], index=["pen", "notebook", "bag"])

print(quantity * 2)
text
pen         24
notebook    10
bag          4
dtype: int64

تمت مضاعفة العمود بأكمله في خطوة واحدة، ودون استخدام أي حلقات تكرارية (loops). لفعل ذلك مع القاموس العادي، يتعين عليك المرور على كل عنصر بنفسك. عند تطبيق أي عملية على كائن Series، فإن بانداس يطبقها على كل قيمة بداخله فوراً — وكل الفصول القادمة تعتمد على هذا السلوك المحوري.

DataFrame — عدة أعمدة بفهرس مشترك

كائن DataFrame هو عبارة عن عدة كائنات Series مصفوفة جنباً إلى جنب وتتشارك فهرساً واحداً.

python
import pandas as pd

sales = pd.DataFrame({
    "product": ["pen", "notebook", "bag"],
    "quantity": [12, 5, 2],
    "price": [15.0, 60.0, 850.0],
})

print(sales)
print(sales.shape)
print(sales.columns)
text
product  quantity  price
0       pen        12   15.0
1  notebook         5   60.0
2       bag         2  850.0
(3, 3)
Index(['product', 'quantity', 'price'], dtype='object')

عبارة "تتشارك فهرساً واحداً" هي جوهر المسألة. فالقيم المقابلة للمنتج والكمية والسعر عند الفهرس 0 كلها تعود إلى نفس الشيء، وهو القلم الأول. الفهرس هو الرابط الذي يجمع الأعمدة الثلاثة في صفوف مترابطة.

جدول واحد وأنواع متعددة — ولكن عمود واحد بنوع واحد

python
import pandas as pd

sales = pd.DataFrame({
    "product": ["pen", "notebook", "bag"],
    "quantity": [12, 5, 2],
    "price": [15.0, 60.0, 850.0],
})

print(sales.dtypes)
text
product      object
quantity      int64
price       float64
dtype: object

ثلاثة أعمدة، بثلاثة أنواع مختلفة. int64 للأعداد الصحيحة، و float64 للأعداد العشرية، و object — والذي يعني عادةً النصوص.

لاحظ غياب النوع str. أنواع البيانات في بانداس مأخوذة من مكتبة NumPy، حيث تشغل كل قيمة حجماً ثابتاً من الذاكرة — وهو أمر ممكن مع الأرقام، لكنه مستحيل مع النصوص المتفاوتة في أطوالها. لذلك، للاحتفاظ بالنصوص، يخزن بانداس مؤشرات لعناصر بايثون، ويُطلق على هذا الترتيب اسم object.

والآن إلى القاعدة الذهبية: كل قيمة داخل العمود الواحد تتشارك نوعاً واحداً. فما الذي يحدث إذا خلطنا الأرقام والنصوص معاً في عمود واحد؟

python
import pandas as pd

mixed = pd.Series([1, "two", 3.0])

print(mixed)
print(mixed.dtype)
text
0      1
1    two
2    3.0
dtype: object

لا يُظهر بانداس أي رسالة خطأ — بل يحول العمود بأكمله إلى نوع object، متراجعاً إلى النوع الأكثر مرونة واستيعاباً. عاقبة ذلك ليست خطأ صريحاً ومباشراً، بل مشكلة صامتة: لم يعد العمود رقمياً، لذا ستفشل دالة .mean() في حسابه، وستبدأ دالة .sum() بدمج النصوص بدلاً من جمع الأرقام. وجود خلية نصية واحدة مثل N/A في صف واحد من ملف CSV يؤدي إلى هذا السلوك تحديداً، وهو موضوع الفصل السادس.

أيهما بين يديّ الآن — Series أم DataFrame؟

python
import pandas as pd

sales = pd.DataFrame({
    "product": ["pen", "notebook", "bag"],
    "quantity": [12, 5, 2],
    "price": [15.0, 60.0, 850.0],
})

column = sales["quantity"]

print(type(column))
print(type(sales))
print(column.mean())
text
<class 'pandas.core.series.Series'>
<class 'pandas.core.frame.DataFrame'>
6.333333333333333

وها هي الإجابة عن السؤال الذي افتتحنا به هذا الفصل. التعبير sales["quantity"] ليس قائمة، بل هو كائن Series — ويمتلك Series دوال مثل .sum() و .mean() و .max() وغيرها.

وهناك أيضاً تمييز دقيق يربك المبتدئين في البداية:

python
import pandas as pd

sales = pd.DataFrame({
    "product": ["pen", "notebook", "bag"],
    "quantity": [12, 5, 2],
    "price": [15.0, 60.0, 850.0],
})

print(sales["quantity"])
print("---")
print(sales[["quantity"]])
text
0    12
1     5
2     2
Name: quantity, dtype: int64
---
   quantity
0        12
1         5
2         2

قوس معقوف واحد إضافي كفيل بتغيير طبيعة الناتج بالكامل. الأول هو Series — عمود واحد، مع طباعة اسمه ونوعه في الأسفل. أما الثاني فهو DataFrame — جدول تصادف أنه يحتوي على عمود واحد فقط، ولذلك يظهر اسم العمود في الأعلى كعنوان جدول.

القاعدة هي أن الأقواس المعقوفة المزدوجة تعني قائمة من الأعمدة، وطلب قائمة من الأعمدة يعيد دائماً جدولاً كاملاً. بناءً على ذلك، فإن sales[["product", "price"]] يعطيك جدولاً من عمودين.

لماذا يجب استحضار هذا الفارق دائماً؟ لأن رسائل الخطأ تختلف تماماً باختلافهما. فكائن Series يمتلك دالة مثل .mean() ولكنه لا يمتلك أسماء أعمدة، بينما يتيح لك DataFrame اختيار الأعمدة ولكنه لا يعطيك رقماً مفرداً مباشرة. عندما تخبرك رسالة خطأ بأن الكائن لا يحتوي على خاصية معينة، فإن أول سؤال تطرحه على نفسك هو: أي الكائنين أحمله بين يديّ الآن؟

مثال تطبيقي متكامل

inventory.py:

python
import pandas as pd

# Product names become the index, so a row can be reached by name.
stock = pd.DataFrame(
    {
        "quantity": [12, 5, 2, 30],
        "price": [15.0, 60.0, 850.0, 8.0],
    },
    index=["pen", "notebook", "bag", "eraser"],
)

print(stock)
print()
print("Shape      :", stock.shape)
print("Columns    :", list(stock.columns))
print("Index      :", list(stock.index))
print()

# A column is a Series, so an operation applies to every value at once.
value = stock["quantity"] * stock["price"]
print("Value per product:")
print(value)
print()
print("Total value:", value.sum())
text
quantity  price
pen             12   15.0
notebook         5   60.0
bag              2  850.0
eraser          30    8.0

Shape      : (4, 2)
Columns    : ['quantity', 'price']
Index      : ['pen', 'notebook', 'bag', 'eraser']

Value per product:
pen          180.0
notebook     300.0
bag         1700.0
eraser       240.0
dtype: float64

Total value: 2420.0

هناك أمران جديران بالملاحظة هنا:

أولاً، العملية stock["quantity"] * stock["price"] قامت بضرب كائني Series وأنتجت كائن Series ثالثاً. طابق بانداس القيم بينهما حسب الفهرس — فكمية القلم ضُربت في سعر القلم، وكمية الحقيبة في سعر الحقيبة؛ أي بالاسم وليس بمجرد ترتيب الصفوف. هذا السلوك يمثل حجر الأساس لدمج الجداول لاحقاً.

ثانياً، نوع العمود الناتج هو float64، على الرغم من أن عمود quantity كان من نوع int64. فضرب عدد صحيح في عدد عشري ينتج عدداً عشرياً — وهي نفس القاعدة المتبعة في بايثون الأساسي، دون أي تغيير من جانب بانداس.


الأخطاء الشائعة وحلولها

خطأ KeyError: 'Quantity' لا يوجد عمود بهذا الاسم. أسماء الأعمدة حساسة لحالة الأحرف، وغالباً ما تحتوي الأسماء المقروءة من ملفات CSV على مسافات زائدة في البداية أو النهاية. شغّل print(list(df.columns)) لرؤية الأسماء بدقة — فطريقة عرضها كقائمة تضع علامات تنصيص حول كل اسم، مما يوضح أي مسافة خفية.

خطأ AttributeError: 'DataFrame' object has no attribute 'mean_price' يمكن لكتابة df.column_name جلب العمود أيضاً، ولكن فقط في حال كانت الأسماء بسيطة — وليس عندما يحتوي الاسم على مسافات، أو يبدأ برقم، أو يتعارض مع دالة مدمجة في بانداس مثل count. أما الصيغة df["column_name"] فهي تعمل دائماً وفي جميع الحالات، فاجعلها خيارك الأساسي والمعتمد دائماً.

خطأ TypeError: unsupported operand type(s) أثناء إجراء حسابات على عمود رقمي العمود في الغالب ليس رقمياً. افحص df.dtypes؛ فظهور object هناك يعني اختلاط نصوص بالبيانات في مكان ما.

دالة .sum() تقوم بدمج النصوص بدلاً من جمع الأرقام السبب نفسه — نوع العمود هو object. وجود خلية واحدة بقيمة N/A أو فارغة في صف واحد يكفي لتغيير نوع العمود بأكمله.

خطأ ValueError: All arrays must be of the same length القوائم الممررة إلى pd.DataFrame({...}) ليست متساوية في الطول. يحتاج كل عمود إلى نفس عدد القيم، لأن الصفوف يجب أن تصطف بدقة على فهرس مشترك واحد.