حل خطأ ChainedAssignmentError في Pandas 3.0 باستخدام loc | مشكلة وحل 38

حل خطأ ChainedAssignmentError في Pandas 3.0 باستخدام loc مع Copy-on-Write لتعديل DataFrame بشكل صحيح

بعد تحديث مكتبة Pandas قد تلاحظ أن كود تعديل القيم لا يغير DataFrame الأصلية كما تتوقع، وتظهر رسالة تبدأ بـChainedAssignmentError: A value is trying to be set on a copy. قد ترى أيضًا شروحات قديمة تتحدث عن SettingWithCopyWarning، فتبحث عن طريقة لإخفاء التحذير بدل إصلاح التعديل نفسه.

في هذا الدرس من بايثون العرب — مشكلة وحل 38 سنعيد إنتاج المشكلة، ونشرح أثر نظام Copy-on-Write (CoW) الذي أصبح السلوك الإلزامي في Pandas 3.0، ثم نصلح التعديل باستخدام .loc و.iloc ونوضح متى تكون .copy() مطلوبة، وكيف نتعامل مع inplace=True عند اختيار عمود من DataFrame.

{alertInfo}ملاحظة مهمة: في هذا السياق، ChainedAssignmentError اسم فئة Warning، وقد تحوّله إعدادات التحذيرات إلى استثناء. لا يكفي اختفاء الرسالة؛ يجب التحقق من أن التعديل حدث فعلًا على DataFrame المطلوبة.

تحميل مشروع إصلاح ChainedAssignmentError

ملف CSV أصلي ومصحح، أمثلة Before/After، سكربت يختبر Copy-on-Write، و21 اختبارًا تلقائيًا مع مخرجات حقيقية.

تحميل المشروع ZIP

{getToc} $title={محتوى المقال}


ما هو خطأ ChainedAssignmentError في Pandas؟

يحدث Chained Assignment عندما تحاول تعديل بيانات أصلية من خلال عمليتَي فهرسة متتاليتين، مثل اختيار عمود أولًا ثم اختيار صفوف منه. مع Copy-on-Write تتصرف نتيجة الاختيار ككائن مستقل عند التعديل، فلا تنتقل التغييرات إلى DataFrame الأصلية. لهذا قد يستمر الكود في التنفيذ لكنه يعطي بيانات غير صحيحة.

import pandas as pd

df = pd.DataFrame({
    "name": ["Ali", "Sara", "Omar"],
    "score": [65, 90, 70],
})

df["score"][df["score"] < 75] = 75

في Pandas 3.0 لا تعدل التعليمة السابقة DataFrame الأصلية كما يتوقع الكاتب. وبدل الاعتماد على المخرجات القديمة أو إخفاء التحذير، سنعيد كتابة التعديل بصورة مباشرة.

ماذا تغير في Pandas 3.0؟

وفق توثيق Pandas الرسمي، أصبح Copy-on-Write السلوك الدائم ابتداءً من الإصدار 3.0 الذي صدر في 21 يناير 2026. لا يمكن تعطيله للعودة إلى السلوك السابق. أزال المشروع تحذير SettingWithCopyWarning القديم، وأصبح نمط Chained Assignment يُظهر التحذير ChainedAssignmentError بدلًا منه.

والفكرة الدقيقة هنا ليست أن Pandas تنسخ كل البيانات إلى الذاكرة فورًا؛ بل إن الجزء المحدد يتصرف، من منظور المستخدم، كما لو كان نسخة مستقلة عند التعديل، مع إمكان استخدام مشاركة للذاكرة داخليًا حتى تكون النسخة الفعلية ضرورية.

الإصدار أو الوضعنتيجة التعديل المتسلسل
Pandas 2.x مع CoW معطّلقد يعمل بعض الأنماط وقد تفشل أخرى؛ السلوك ملتبس
Pandas 2.2 مع copy_on_write=Trueتحذير من نوع ChainedAssignmentError وعدم تحديث الأصل في المثال
Pandas 3.0CoW إلزامي، وChained Assignment لا يحدث التعديل المقصود على الأصل

كيف تعرف إصدار Pandas وتختبر Copy-on-Write؟

import pandas as pd

print(pd.__version__)

إذا كنت تستخدم Pandas 2.2 وتريد تجربة سلوك CoW قبل الترقية، يمكنك تفعيله اختياريًا:

import pandas as pd

major = int(pd.__version__.split(".", 1)[0])
if major < 3:
    pd.options.mode.copy_on_write = True
{alertWarning}لا تستخدم ضبط pd.options.mode.copy_on_write=False للتحايل على المشكلة في Pandas 3.0. هذا الوضع أصبح دائمًا ولا يمكن إيقافه. إعداد الخيار في المشروع يستخدم فقط لاختبار السلوك نفسه في Pandas 2.x.

إعادة إنتاج المشكلة: التعديل لا يصل إلى DataFrame الأصلية

سنستخدم بيانات أربعة طلاب، ونرفع كل درجة أقل من 75 إلى 75. هكذا يمكننا التحقق من صحة النتائج بدل الاكتفاء بالتحذير.

import pandas as pd

if int(pd.__version__.split(".", 1)[0]) < 3:
    pd.options.mode.copy_on_write = True

df = pd.DataFrame({
    "name": ["Ali", "Sara", "Omar", "Mona"],
    "score": [65, 90, 70, 82],
})

print("قبل:", df["score"].tolist())
df["score"][df["score"] < 75] = 75
print("بعد:", df["score"].tolist())

في اختبارنا باستخدام Pandas 2.2.3 مع Copy-on-Write مفعّل، ظهرت رسالة من فئة ChainedAssignmentError وكانت القيم قبل وبعد متطابقة:

قبل: [65, 90, 70, 82]
بعد: [65, 90, 70, 82]

يوضح ذلك أن الكود حاول التعديل لكن DataFrame الأصلية بقيت دون تغيير.

مقارنة Chained Assignment قبل وبعد استخدام loc في Pandas وتوضيح تغير القيم من 65 و70 إلى 75

الحل الصحيح: استخدام loc لتعديل DataFrame مباشرة

استخدم عملية فهرسة واحدة تجمع شرط الصفوف واسم العمود. هذا هو التصحيح الأساسي الذي توصي به Pandas:

df.loc[df["score"] < 75, "score"] = 75

print(df)
   name  score
0   Ali     75
1  Sara     90
2  Omar     75
3  Mona     82

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

{alertSuccess}اختبرت هذا الحل على ملف البيانات المرفق: تحولت الدرجتان 65 و70 إلى 75، وبقيت 90 و82 كما هما. هذا هو المعيار الصحيح لنجاح الإصلاح.

هل يمكن استخدام iloc بدل loc؟

نعم إذا كنت تريد تحديد المواضع الرقمية للصفوف والأعمدة. .loc تعمل بالمسميات والشروط، بينما .iloc تعتمد على المواقع الرقمية. مثال موازٍ:

rows = (df["score"] < 75).to_numpy().nonzero()[0]
column = df.columns.get_loc("score")

df.iloc[rows, column] = 75

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

متى نستخدم copy() بدلًا من loc؟

إذا كان المطلوب تعديل مجموعة بيانات مستقلة دون المساس ببيانات الأصل، فاستخدم .copy() بصورة صريحة. هذا يختلف عن هدف الحل السابق:

students = pd.DataFrame({
    "name": ["Ali", "Sara", "Omar", "Mona"],
    "score": [65, 90, 70, 82],
})

low_scores = students.loc[students["score"] < 75].copy()
low_scores.loc[:, "score"] = 75

print(low_scores["score"].tolist())
print(students["score"].tolist())
[75, 75]
[65, 90, 70, 82]

التغييرات ذهبت إلى low_scores فقط؛ لم تتغير DataFrame الأصلية. إذًا .copy() ليست بديلًا عامًا عن .loc، بل تختارها عندما يكون فصل البيانات مقصودًا.

حل مشكلة inplace=True عند تعديل عمود محدد

قد يظهر تحذير مشابه عندما تستدعي دالة inplace=True على Series ناتجة من تحديد عمود:

df = pd.DataFrame({
    "score": [65.0, float("nan"), 70.0]
})

df["score"].fillna(0, inplace=True)

في وضع Copy-on-Write، لا تجعل inplace=True على الكائن الوسيط يحدّث الأصل كما تتوقع. في تجربتنا بقيت القيمة المفقودة موجودة وظهر تحذير من فئة ChainedAssignmentError. الحل الأكثر وضوحًا هو إعادة إسناد الناتج إلى العمود:

df["score"] = df["score"].fillna(0)
print(df["score"].tolist())
[65.0, 0.0, 70.0]

ويمكن أيضًا إجراء التعديل على DataFrame نفسها:

df.fillna({"score": 0}, inplace=True)

لاحظ الفرق بين استدعاء الدالة على df["score"] الذي يعيد Series، واستدعائها على df مباشرة.

كيف تصلح replace وغيرها من دوال التعديل؟

ينطبق المبدأ ذاته على بعض أشكال replace عند استخدام inplace=True على عمود محدد. بدل محاولة تعديل Series وسيطة، أعد إسناد الناتج إلى عمود DataFrame:

df = pd.DataFrame({"score": [65, 90, 70]})

df["score"] = df["score"].replace(70, 75)
print(df["score"].tolist())
[65, 90, 75]

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

لماذا لا يحل إخفاء SettingWithCopyWarning المشكلة؟

من الخطأ إخفاء الرسالة باستخدام إعدادات التحذيرات ثم اعتبار المشكلة منتهية. في Pandas 3.0 لم يعد SettingWithCopyWarning القديم هو آلية التنبيه المستخدمة لهذا السيناريو. وحتى في الإصدارات القديمة، إخفاء التحذير لا يثبت أن البيانات تغيرت بالطريقة المطلوبة.

# لا تعتمد على هذا بوصفه إصلاحًا:
# pd.options.mode.chained_assignment = None

# الإصلاح الحقيقي:
df.loc[df["score"] < 75, "score"] = 75

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

كيف تحول التحذير إلى خطأ أثناء الاختبار؟

عند مراجعة مشروع قديم، يمكن الاستفادة من نظام warnings لالتقاط هذا النمط مبكرًا خلال الاختبارات:

import warnings
import pandas as pd

warnings.filterwarnings(
    "error",
    category=pd.errors.ChainedAssignmentError,
)

# أثناء الاختبار، يصبح ظهور التحذير استثناءً
# ويمنع مرور الكود الخاطئ دون ملاحظة.

هذه وسيلة للكشف عن المشكلة وليست بديلًا عن إصلاح Chained Assignment. وقد تختلف طريقة عرض التحذيرات بحسب إعدادات IDE أو بيئة الاختبار.

جدول تحويل الأكواد القديمة إلى صيغة صحيحة

النمط غير المناسب مع CoWالبديل الصحيحمتى تستخدمه؟
df["score"][mask] = 75df.loc[mask, "score"] = 75لتغيير عمود في DataFrame الأصلية
df.loc[mask]["score"] = 75df.loc[mask, "score"] = 75لتجنب فهرستين متتاليتين
df["score"].fillna(0, inplace=True)df["score"] = df["score"].fillna(0)لملء القيم المفقودة في العمود الأصلي
df["score"].replace(70, 75, inplace=True)df["score"] = df["score"].replace(70, 75)لاستبدال قيمة محددة
تعديل نتيجة تصفية مع توقع تغيير الأصلsubset = df.loc[mask].copy() ثم تعديلهاإذا كنت تحتاج نسخة مستقلة فقط
الفرق بين loc وcopy والتعديل المباشر عند حل ChainedAssignmentError وتعديل DataFrame في Pandas

مشروع تطبيقي: تعديل درجات الطلاب من ملف CSV

لجعل المشكلة قابلة للتجربة، تتضمن الحزمة ملف data/students.csv بهذه البيانات:

name,score,city
Ali,65,Aden
Sara,90,Sanaa
Omar,70,Taiz
Mona,82,Aden

نريد رفع الدرجات الأقل من 75 إلى 75 دون تغيير درجات بقية الطلاب، ثم حفظ نسخة جديدة باسم students-corrected.csv.

from pathlib import Path
import pandas as pd

source = Path("data/students.csv")
target = Path("output/students-corrected.csv")

df = pd.read_csv(source)
df.loc[df["score"] < 75, "score"] = 75

target.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(target, index=False)

print(df["score"].tolist())
[75, 90, 75, 82]

هذه هي نتيجة التعديل الصحيح الذي اختبرناه فعليًا. المشروع يحتوي أيضًا أمثلة منفصلة للتعديل المتسلسل و.iloc و.copy() وfillna()، حتى تتجنب الخلط بين تعديل الأصل وتعديل نسخة مشتقة.

طريقة تشغيل المشروع المرفق

بعد تنزيل الحزمة وفك ضغطها، افتح مجلد المشروع في الطرفية، ثم ثبت نسخة Pandas 3.x لتطبيق الأمثلة على الإصدار المستهدف:

python -m pip install -r requirements.txt
python pandas_cow_lab.py
python -m unittest discover -s tests -v

إذا كنت تعمل على Pandas 2.2 فإن السكربت يفعّل Copy-on-Write تلقائيًا لغرض التدريب؛ أما في Pandas 3.0 فالسلوك مفعّل أصلًا. يطبع البرنامج نسخة Pandas في بداية التقرير حتى تعرف البيئة التي شغّلت المثال.

المخرجات الفعلية ونتائج الاختبارات

أثناء إعداد الدرس شغّلنا المشروع على Python 3.13.5 وPandas 2.2.3 مع Copy-on-Write مفعّل. هذه مخرجات السكربت نفسه، دون تعديل يدوي في النتائج:

pandas version: 2.2.3
CoW mode: enabled for pandas 2.x
original scores: [65, 90, 70, 82]
warning types: ['ChainedAssignmentError']
after chained assignment: [65, 90, 70, 82]
after .loc fix: [75, 90, 75, 82]
independent subset scores: [75, 75]
inplace warning types: ['ChainedAssignmentError']
is middle value missing after inplace: True
after safe fillna: [65.0, 0.0, 70.0]
saved: output/students-corrected.csv

كما نجحت 21 من 21 اختبارًا باستخدام unittest. شملت الاختبارات بقاء الأصل دون تغيير عند Chained Assignment، ونجاح loc وiloc، واستقلال البيانات بعد .copy()، ومشكلة inplace=True، وإعادة قراءة ملف CSV المصحح.

Ran 21 tests in 0.031s

OK
{alertWarning}تم التحقق بالتشغيل على Pandas 2.2.3 مع تفعيل Copy-on-Write، ولم نتمكن من تشغيل Pandas 3.0 داخل بيئة إعداد المقال. تعتمد الأحكام الخاصة بالسلوك الإلزامي في الإصدار 3.0 على التوثيق الرسمي؛ وأُعد ملف requirements.txt لتثبيت Pandas 3.x عند تشغيل القارئ للمشروع.

نتائج 21 اختبارًا محليًا لإصلاح ChainedAssignmentError باستخدام loc وcopy وCopy-on-Write في Pandas 2.2.3

أخطاء شائعة يجب تجنبها

  • إخفاء التحذير بدل إصلاح الكود: قد تظل DataFrame الأصلية دون تغيير.
  • استخدام .copy() لتعديل الأصل: النسخة المستقلة مناسبة فقط إذا كنت تقصد الاحتفاظ بالأصل دون تعديل.
  • اعتبار inplace=True ضمانًا لتحديث DataFrame: لا تستخدمه على Series وسيطة وتتوقع أن تعدّل الأصل.
  • الخلط بين .loc و.iloc: الأولى تعتمد على التسميات، والثانية على مواقع الصفوف والأعمدة.
  • توقع سلوك Pandas 2.x القديم في Pandas 3.0: لم تعد فهرسة المتسلسلات وسيلة مقبولة لتعديل كائنين في عملية واحدة.
  • عدم إجراء اختبارات بعد التعديل: قارن القيم قبل الإصلاح وبعده، ولا تعتمد على غياب التحذير فقط.

المراجع الرسمية والدروس المرتبطة

الأسئلة الشائعة عن ChainedAssignmentError في Pandas

لماذا يظهر ChainedAssignmentError في Pandas 3.0؟

لأن الكود يحاول تعديل البيانات الأصلية عبر عمليتَي فهرسة متتاليتين. في Copy-on-Write لا تنتقل التغييرات من الكائن الوسيط إلى DataFrame الأصلية، لذلك استخدم فهرسة مباشرة مثل df.loc[mask, "score"] = 75.

هل ChainedAssignmentError استثناء يوقف البرنامج؟

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

ما الفرق بين SettingWithCopyWarning وChainedAssignmentError؟

الأول تحذير قديم مرتبط بغموض النسخ والعروض قبل Pandas 3.0. ومع سلوك Copy-on-Write الدائم في Pandas 3.0 أزيل التحذير القديم، وأصبح التعديل المتسلسل غير صالح لتحديث الأصل.

هل أستخدم loc أم copy لحل المشكلة؟

استخدم .loc عندما تريد تعديل DataFrame الأصلية، واستخدم .copy() عندما تريد إنشاء نسخة مستقلة ثم تعديلها دون التأثير في الأصل.

هل iloc صالحة مع Copy-on-Write؟

نعم؛ فالتعديل المباشر على DataFrame عبر .iloc[rows, column] يعمل عندما تحدد المواضع بصورة صحيحة. المشكلة هي سلسلة الاختيارات الوسيطة التي تحاول تعديل الأصل من خلالها.

كيف أصلح fillna(inplace=True) على عمود؟

استخدم الإسناد المباشر مثل df["score"] = df["score"].fillna(0)، أو طبّق عملية مناسبة على DataFrame نفسها حسب هدفك.

هل يمكن تعطيل Copy-on-Write في Pandas 3.0؟

لا. أصبح Copy-on-Write السلوك الإلزامي في Pandas 3.0، لذا الحل هو تحديث الكود المتوافق بدل محاولة العودة إلى سلوك قديم.

كيف أتأكد أن حل ChainedAssignmentError نجح؟

افحص DataFrame الأصلية بعد العملية، واكتب اختبارًا يقارن القيم المتوقعة بالقيم الفعلية، ثم تأكد أن ملف CSV أو Excel المصدّر يحتوي البيانات المصححة.

إرسال تعليق

أحدث أقدم