EduFlow
Python لتحليل البيانات
⁦0%⁩

الوحدة 2 · أساسيات pandas

إضافة الأعمدة وتعديلها

18 دقراءة 4 دتمرين Python

الدرس في لمحة

أنشئ أعمدة محسوبة بعمليات حسابية متّجهة، وابنِ أعمدة شرطية بـ ‎.loc و np.where، وحوّل الأكواد إلى تسميات، وغيّر القيم بأمان وفق قواعد Copy-on-Write في pandas.

  1. 1الأعمدة المحسوبة
  2. 2الأعمدة الشرطية
  3. 3ترجمة الأكواد بـ map
  4. 4إصلاح نوع العمود بـ astype
  5. 5تغيير القيم: القاعدة الوحيدة

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

في جدول order_items الخاص بـ Cartwheel كمية وسعر وحدة وخصم في كل بند من بنوده الـ 3,991، لكن لا يوجد إيراد. الإيراد شيء تحسبه أنت، وطريقة حسابه يجب أن تُكتب حيث يراها الجميع. هذا ما يفعله العمود المحسوب: يحوّل قاعدة عمل إلى سطر كود واحد يُطبَّق على كل صف.

الأعمدة المحسوبة

الإسناد إلى اسم عمود جديد ينشئه:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
items["gross"] = items["quantity"] * items["unit_price"]
items["revenue"] = items["gross"] * (1 - items["discount"])
items["discount_amount"] = items["gross"] - items["revenue"]
print(items.head().to_string())
print(round(items["revenue"].sum(), 2), round(items["discount_amount"].sum(), 2))

كل سطر يعمل على أعمدة كاملة، صفًا بصف، دون حلقة. النتيجة: إيراد قدره 471,940.08 دولارًا عبر كل البنود، بعد خصومات قدرها 11,658.52 دولارًا. (هذا المجموع ما زال يشمل الطلبات الملغاة؛ وستزيلها في القسم 4 حين تستطيع ربط order_items بـ orders.)

تفعل assign() الشيء نفسه، لكنها تعيد DataFrame جديدًا بدلًا من تغيير الموجود، وهذا يتيح لك تسلسل الخطوات:

Python
import pandas as pd

items = (
    pd.read_csv("order_items.csv")
    .assign(revenue=lambda d: d["quantity"] * d["unit_price"] * (1 - d["discount"]))
    .assign(is_discounted=lambda d: d["discount"] > 0)
)
print(items[["revenue", "is_discounted"]].head(3))
print(items["is_discounted"].sum(), "discounted lines")

الكود lambda d: ... دالة صغيرة بلا اسم تستقبل الـ DataFrame عند تلك النقطة من السلسلة. تحتاجها حين تستخدم خطوةٌ عمودًا أُنشئ قبلها في السلسلة نفسها. الأسلوبان جيدان؛ الإسناد العادي أسهل قراءة حين تكون في البداية، والسلاسل تتألّق في دالة التنظيف التي ستكتبها في الدرس 3.5.

الأعمدة الشرطية

التسمية بخيارين مهمة تناسب الدالة where من NumPy، المكتبة التي بُنيت عليها pandas:

Python
import numpy as np
import pandas as pd

items = pd.read_csv("order_items.csv")
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
items["line_size"] = np.where(items["revenue"] >= 100, "large", "small")
print(items["line_size"].value_counts())

اقرأها هكذا: “حيث يكون الإيراد 100 على الأقل، large؛ وفي غير ذلك، small”. ولثلاث فئات أو أكثر، ابدأ بقيمة افتراضية ثم استبدل مجموعات فرعية بـ .loc:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
items["discount_band"] = "none"
items.loc[items["discount"] > 0, "discount_band"] = "small"
items.loc[items["discount"] > 0.10, "discount_band"] = "big"
print(items["discount_band"].value_counts())

الترتيب مهم تمامًا كما كان مع if/elif في الدرس 1.4، لكن بالعكس: الإسنادات اللاحقة تكتب فوق السابقة، فانتقل من الشرط الأوسع إلى الأضيق. الخصومات 5% و10% تنتهي في الفئة small، و15% و25% في الفئة big.

ترجمة الأكواد بـ map

الأعمدة المليئة بالأكواد (mobile_app، والمعرّفات، وأعلام الحالة) تحتاج غالبًا تسميات مقروءة. تترجم map() مع قاموس كل قيمة:

Python
import pandas as pd

orders = pd.read_csv("orders.csv")
labels = {"web": "Website", "mobile_app": "Mobile app", "marketplace": "Marketplace"}
orders["channel_label"] = orders["channel"].map(labels)
print(orders["channel_label"].value_counts())
print(orders["channel_label"].isna().sum(), "unmapped")

أي قيمة غير موجودة في القاموس تصبح NaN، بصمت. والسطر الأخير هو الفحص الذي يلتقط ذلك: صفر قيم غير مُحوّلة يعني أن لكل قناة تسمية.

إصلاح نوع العمود بـ astype

أحيانًا تكون القيم صحيحة لكن النوع ليس كذلك. يخزّن products.csv العمود is_active على هيئة 0 و1، فتقرؤه pandas أعدادًا صحيحة. الجمع يعمل، لكن قارئ كودك لا يستطيع التمييز بين علَم (flag) وعدد، وتُقرأ التصفيات بشكل متكلّف. تحوّل astype() عمودًا كاملًا:

Python
import pandas as pd

products = pd.read_csv("products.csv")
products["is_active"] = products["is_active"].astype(bool)
print(products["is_active"].dtype, products["is_active"].sum(), "active products")
print(products.loc[~products["is_active"], "name"].tolist())

ثلاثة منتجات متوقّفة، و~products["is_active"] تُقرأ “غير نشط”. استخدم astype حين يمكن تحويل كل القيم بنظافة؛ ويبيّن القسم 3 ما تفعله حين لا يمكن تحويل بعضها، مثل المجاميع التي فيها علامات دولار.

تغيير القيم: القاعدة الوحيدة

هذه هي القاعدة التي توفّر عليك ساعات من الارتباك: لتغيير القيم في DataFrame، أسنِد إليه في خطوة واحدة، بعمود جديد أو بـ .loc[rows, column]. لا تختر ثم تُسند في خطوتين أبدًا:

Python
# Wrong: chained assignment. Under Copy-on-Write this never updates items.
items["discount"][items["discount"] > 0.2] = 0.2

# Right: one .loc call names the rows and the column together.
items.loc[items["discount"] > 0.2, "discount"] = 0.2

الشكل الأول يختار العمود discount، وهذا ينتج كائنًا جديدًا، ثم يُسند إلى ذلك الكائن. هل تغيّر items نفسه؟ كان ذلك يعتمد على تفاصيل pandas الداخلية، وتطبع pandas 2 تحذير ChainedAssignmentError أو SettingWithCopyWarning حين تلاحظ هذا النمط. أما مع Copy-on-Write (السلوك الافتراضي منذ pandas 3.0، والمتاح في pandas 2 عبر pd.options.mode.copy_on_write = True)، فتصبح القاعدة بسيطة وصارمة: كل عملية اختيار تتصرّف كنسخة، فلا يحدّث الإسناد المتسلسل الأصل أبدًا. اكتبها بطريقة .loc وسيتصرّف كودك بالطريقة نفسها على كل الإصدارات.

بهذا تكتمل أساسيات pandas لديك: التحميل، والفحص، والاختيار، والتصفية، والترتيب، والحساب. يوجّه القسم 3 هذه الأدوات نحو الملف الفوضوي، حيث تنتظرك الخانات الفارغة والأخطاء الكتابية والمبالغ النصية والتواريخ الملتبسة.

أهم النقاط

  • df["new"] = expression تضيف عمودًا محسوبًا لكل الصفوف دفعة واحدة؛ وassign() تفعل الشيء نفسه وتعيد DataFrame جديدًا.
  • غيّر القيم الموجودة باستدعاء واحد .loc[row_mask, "col"] = value، ولا تستخدم أبدًا الأقواس المتسلسلة مثل df["col"][mask] = value.
  • np.where(condition, a, b) تبني عمودًا بخيارين؛ وإسنادات .loc المتتالية أو np.select تتعامل مع فئات أكثر.
  • map() مع قاموس تحوّل الأكواد إلى تسميات، والقيم غير الموجودة في القاموس تصبح NaN.
  • مع Copy-on-Write تتصرّف كل عملية اختيار كأنها نسخة، فالطريقة الوحيدة لتغيير DataFrame هي الإسناد إليه مباشرة.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

تريد أن تجعل كل خصم أعلى من 0.2 مساويًا لـ 0.2. أيّ سطر يفعل ذلك بشكل صحيح في pandas 2 و3؟

السؤال 2 من 3

orders["channel_label"] = orders["channel"].map({"web": "Website", "mobile_app": "App"}). ماذا يحدث لطلبات الـ marketplace؟

السؤال 3 من 3

ماذا ينتج np.where(items["quantity"] >= 3, "bulk", "regular")؟

تدريب

تمرين Python

أضِف الإيراد والتسميات إلى بنود الطلبات

أضِف ثلاثة أعمدة إلى items: revenue (الكمية × سعر الوحدة × (1 − الخصم))، وdiscount_band ("none" حين يكون الخصم 0، و"small" حين يكون أكبر من 0 وأقل من أو يساوي 0.10، و"big" حين يكون أكبر من 0.10)، وis_bulk (قيمة منطقية تساوي True حين تكون الكمية 4 أو أكثر). ثم اجعل total_revenue مجموع revenue، مقرّبًا إلى منزلتين عشريتين. احتفظ بكل الصفوف الـ 3,991.

ملفات البيانات: order_items.csv

import pandas as pd

items = pd.read_csv("order_items.csv")

# 1. Column "revenue": quantity * unit_price * (1 - discount)

# 2. Column "discount_band": "none" when discount is 0,
#    "small" when it is above 0 and at most 0.10, "big" when above 0.10

# 3. Column "is_bulk": True when quantity is 4 or more

# 4. Total revenue of all lines, rounded to 2 decimals
total_revenue = None
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

(لا مخرجات)

الفحوص

  • العمود revenue موجود، والبند 4 (الفهرس 3) يساوي 270.90
  • الخصومات مطبّقة في revenue
  • discount_band يقسم البنود إلى 3,367 في none و231 في small و393 في big
  • is_bulk عمود منطقي فيه 126 بندًا بالجملة
  • total_revenue يساوي 471,940.08
  • ما زال في items كل الصفوف الـ 3,991