SettingWithCopyWarning
هو تحذير يمكن أن تثيره Pandas عندما نقوم بتعيين قيمة لإطار بيانات. يمكن أن يحدث هذا عند استخدام تعيينات متسلسلة أو عند استخدام إطار بيانات تم إنشاؤه من شريحة. إنه مصدر شائع للأخطاء في كود Pandas التي واجهناها جميعًا من قبل. يمكن أن يكون من الصعب تصحيح الأخطاء لأن التحذير يمكن أن يظهر في الكود الذي يبدو وكأنه يجب أن يعمل بشكل جيد.
فهم SettingWithCopyWarning
مهم لأنه يشير إلى قضايا محتملة في تعديل البيانات. يقترح هذا التحذير أن الكود الخاص بك قد لا يقوم بتعديل البيانات كما هو المقصود، مما يمكن أن يؤدي إلى عواقب غير مقصودة وأخطاء غامضة يصعب تتبعها.
في هذه المقالة، سنستكشف SettingWithCopyWarning
في pandas وكيفية تجنبه. سنناقش أيضًا مستقبل Pandas وكيف سيغير خيار copy_on_write
الطريقة التي نعمل بها مع إطارات البيانات.
عروض البيانات والنسخ في الإطارات
عند اختيار شريحة من إطار بيانات وتعيينها إلى متغير، يمكننا الحصول على عرض أو نسخة جديدة من إطار البيانات.
مع العرض، يتم مشاركة الذاكرة بين كلا الإطارين. وهذا يعني أن تعديل قيمة من خلية موجودة في كل من إطارات البيانات سيعدل كل منهما.
مع النسخة، يتم تخصيص ذاكرة جديدة، ويتم إنشاء إطار بيانات مستقل بنفس القيم كما في النسخة الأصلية. في هذه الحالة، كل من إطارات البيانات هما كيانات متميزة، لذا تعديل قيمة في أحدهما لا يؤثر على الآخر.
تحاول Pandas تجنب إنشاء نسخة عندما يمكنها ذلك لتحسين الأداء. ومع ذلك، من المستحيل التنبؤ مسبقًا ما إذا كنا سنحصل على عرض أو نسخة. يتم رفع التحذير SettingWithCopyWarning
كلما قمنا بتعيين قيمة إلى DataFrame حيث يكون من غير الواضح ما إذا كانت نسخة أو عرضًا من DataFrame آخر.
فهم SettingWithCopyWarning
مع بيانات حقيقية
سنستخدم مجموعة بيانات العقارات الحقيقية في لندن 2024 من كاجل لتعلم كيف يحدث SettingWithCopyWarning
وكيفية إصلاحه.
تحتوي هذه المجموعة على بيانات عقارية حديثة من لندن. إليك نظرة عامة على الأعمدة الموجودة في مجموعة البيانات:
addedOn
: التاريخ الذي تم فيه إضافة القائمة.title
: عنوان القائمة.descriptionHtml
: وصف HTML للقائمة.propertyType
: نوع العقار. ستكون القيمة"غير محدد"
إذا لم يتم تحديد النوع.sizeSqFeetMax
: الحد الأقصى للحجم بالأقدام المربعة.bedrooms
: عدد غرف النوم.listingUpdatedReason
: سبب تحديث القائمة (مثل، قائمة جديدة، تخفيض السعر).price
: سعر القائمة بالجنيه الإسترليني.
مثال مع متغير مؤقت صريح
يُقال لنا أن الخصائص التي ليس لها نوع خاص محدد هي منازل. وبالتالي، نريد تحديث جميع الصفوف التي تحتوي على propertyType
يساوي "Not Specified"
إلى "House"
. إحدى الطرق للقيام بذلك هي تصفية الصفوف التي ليس لها نوع خاص في متغير DataFrame مؤقت وتحديث قيم عمود propertyType
على النحو التالي:
import pandas as pd dataset_name = "realestate_data_london_2024_nov.csv" df = pd.read_csv(dataset_name) # الحصول على جميع الصفوف التي ليس لها نوع خاص محدد no_property_type = df[df["propertyType"] == "Not Specified"] # تحديث نوع الملكية إلى "منزل" في تلك الصفوف no_property_type["propertyType"] = "House"
تنفيذ هذا الكود سيسبب في إنتاج pandas تحذير SettingWithCopyWarning
:
SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy no_property_type["propertyType"] = "House"
سبب ذلك هو أن pandas لا يمكنه معرفة ما إذا كان no_property_type
DataFrame هو عرض أو نسخة من df
.
هذه مشكلة لأن سلوك الكود التالي يمكن أن يكون مختلفًا جدًا اعتمادًا على ما إذا كان عرضًا أو نسخة.
في هذا المثال، هدفنا هو تعديل DataFrame الأصلي. سيحدث ذلك فقط إذا كان no_property_type
عرضًا. إذا افترض بقية كودنا أن df
تم تعديله، فقد يكون ذلك خاطئًا لأنه لا يوجد طريقة لضمان أن هذه هي الحالة. بسبب هذا السلوك غير المؤكد، تقوم Pandas بإصدار التحذير لإعلامنا بهذه الحقيقة.
حتى لو كان كودنا يعمل بشكل صحيح لأننا حصلنا على عرض، فقد نحصل على نسخة في عمليات التشغيل اللاحقة، ولن يعمل الكود كما هو متوقع. لذلك، من المهم عدم تجاهل هذا التحذير والتأكد من أن كودنا سيفعل دائمًا ما نريد منه أن يفعله.
مثال مع متغير مؤقت مخفي
في المثال السابق، من الواضح أن متغيرًا مؤقتًا يُستخدم لأننا نُعين جزءًا من الإطار إلى متغير يدعى no_property_type
.
ومع ذلك، في بعض الحالات، هذا ليس واضحًا. أحد الأمثلة الشائعة حيث يحدث SettingWithCopyWarning
هو مع فهرسة متسلسلة. لنفترض أننا نستبدل السطرين الأخيرين بسطر واحد:
df[df["propertyType"] == "Not Specified"]["propertyType"] = "House"
عند النظر الأول، لا يبدو أن متغيرًا مؤقتًا يتم إنشاؤه. ومع ذلك، تنفيذه يؤدي إلى ظهور SettingWithCopyWarning
أيضًا.
الطريقة التي يتم بها تنفيذ هذا الكود هي:
- يتم تقييم
df[df["propertyType"] == "Not Specified"]
وتخزينه مؤقتًا في الذاكرة. - يتم الوصول إلى فهرس
["propertyType"]
لذلك الموقع المؤقت في الذاكرة.
يتم تقييم الوصول إلى الفهرس بشكل تسلسل، وبالتالي، تؤدي الفهرسة المتسلسلة إلى نفس التحذير لأننا لا نعرف ما إذا كانت النتائج الوسيطة عروضًا أم نسخًا. الكود أعلاه في الأساس هو نفسه كما لو أننا نقوم بعمل:
tmp = df[df["propertyType"] == "Not Specified"] tmp["propertyType"] = "House"
يُشار إلى هذا المثال غالبًا بفهرسة متسلسلة لأننا نربط الوصولات المُفهرسة باستخدام []
. أولاً، نصل إلى [df["propertyType"] == "Not Specified"]
ثم ["propertyType"]
.
كيفية حل SettingWithCopyWarning
لنتعلم كيف نكتب أكوادنا بحيث لا تكون هناك غموض ولا تُشغَّل تحذيرات SettingWithCopyWarning
. تعلمنا أن التحذير ينشأ نتيجة للغموض حول ما إذا كان DataFrame هو عرض أو نسخة من DataFrame آخر.
الطريقة لإصلاح ذلك هي ضمان أن كل DataFrame ننشئه هو نسخة إذا أردنا أن تكون نسخة أو عرض إذا أردنا أن تكون عرض.
تعديل الDataFrame الأصلي بأمان باستخدام loc
لنقم بإصلاح الكود من المثال أعلاه حيث نريد تعديل الDataFrame الأصلي. لتجنب استخدام متغير مؤقت، استخدم خاصية فهرسة الموقع loc
.
df.loc[df["propertyType"] == "Not Specified", "propertyType"] = "House"
مع هذا الكود، نعمل مباشرةً على DataFrame df
الأصلي عبر خاصية فهرسة الموقع loc
، لذا لا حاجة لمتغيرات وسيطة. هذا هو ما نحتاج إليه عندما نريد تعديل الDataFrame الأصلي مباشرةً.
قد يبدو هذا كالفهرسة المتسلسلة بالنظرة الأولى لوجود معلمات، ولكنه ليس كذلك. ما يحدد كل فهرسة هي الأقواس الزاوية []
.
لاحظ أن استخدام loc
آمن فقط إذا قمنا بتعيين قيمة مباشرةً، كما فعلنا أعلاه. إذا استخدمنا متغيرًا مؤقتًا بدلاً من ذلك، سنواجه نفس المشكلة مرة أخرى. إليك مثالان على كود لا يصحح المشكلة:
- عند استخدام
loc
مع متغير مؤقت:
# استخدام loc بالإضافة إلى متغير مؤقت لا يحل المشكلة no_property_type = df.loc[df["propertyType"] == "Not Specified"] no_property_type["propertyType"] = "House"
- عند استخدام
loc
مع فهرس (نفس الفهرس المتسلسل):
# استخدام loc بالإضافة إلى فهرس هو نفسه مثل الفهرس المتسلسل df.loc[df["propertyType"] == "Not Specified"]["propertyType"] = "House"
كل هذه الأمثلة تربك الناس لأنه من اعتقاد شائع أنه طالما كان هناك loc
، فإننا نقوم بتعديل البيانات الأصلية. هذا غير صحيح. الطريقة الوحيدة لضمان أن القيمة تُسند إلى الDataFrame الأصلي هو تعيينها مباشرة باستخدام loc
وبدون أي فهرس منفصل.
العمل بأمان مع نسخة من الDataFrame الأصلي باستخدام copy()
عندما نريد التأكد من أننا نعمل على نسخة من DataFrame، يجب علينا استخدام طريقة .copy()
.
لنفرض أنه طُلب منا تحليل سعر القدم المربع للعقارات. لا نريد تعديل البيانات الأصلية. الهدف هو إنشاء DataFrame جديد يحتوي على نتائج التحليل لإرسالها إلى فريق آخر.
الخطوة الأولى هي تصفية بعض الصفوف وتنظيف البيانات. على وجه التحديد، نحتاج إلى:
- إزالة الصفوف التي لا يتم تعريف
sizeSqFeetMax
فيها. - إزالة الصفوف التي تكون قيمة
price
تساوي"POA"
(السعر عند الطلب). - تحويل الأسعار إلى قيم رقمية (في مجموعة البيانات الأصلية، الأسعار عبارة عن سلاسل نصية بالصيغة التالية:
"£25,000,000"
)
يمكننا تنفيذ الخطوات أعلاه باستخدام الكود التالي:
# 1. تصفية جميع الخصائص دون حجم أو سعر properties_with_size_and_price = df[df["sizeSqFeetMax"].notna() & (df["price"] != "POA")] # 2. إزالة علامات £ و , من أعمدة الأسعار properties_with_size_and_price["price"] = properties_with_size_and_price["price"].str.replace("£", "", regex=False).str.replace(",", "", regex=False) # 3. تحويل عمود الأسعار إلى قيم رقمية properties_with_size_and_price["price"] = pd.to_numeric(properties_with_size_and_price["price"])
لحساب السعر لكل قدم مربع، نقوم بإنشاء عمود جديد تكون قيمه نتيجة قسمة عمود price
على عمود sizeSqFeetMax
:
properties_with_size_and_price["pricePerSqFt"] = properties_with_size_and_price["price"] / properties_with_size_and_price["sizeSqFeetMax"]
إذا قمنا بتنفيذ هذا الكود، سنحصل مرة أخرى على تحذير SettingWithCopyWarning
. يجب ألا يكون هذا مفاجئًا لأننا قمنا بإنشاء وتعديل متغير DataFrame مؤقت بشكل صريح properties_with_size_and_price
.
نظرًا لأننا نرغب في العمل على نسخة من البيانات بدلاً من DataFrame الأصلي، يمكننا حل المشكلة عن طريق التأكد من أن properties_with_size_and_price
هو نسخة جديدة من DataFrame وليس عرضًا عن طريق استخدام طريقة .copy()
في السطر الأول:
properties_with_size_and_price = df[df["sizeSqFeetMax"].notna() & (df["price"] != "POA")].copy()
إضافة أعمدة جديدة بأمان
إنشاء أعمدة جديدة يتصرف بنفس الطريقة التي يتم فيها تعيين القيم. كلما كان من غير الواضح ما إذا كنا نعمل على نسخة أم عرض، ستقوم pandas برفع SettingWithCopyWarning
.
إذا أردنا العمل مع نسخة من البيانات، يجب علينا نسخها بوضوح باستخدام الطريقة .copy()
. ثم، نحن حريصون على تعيين عمود جديد بالطريقة التي نريد. لقد فعلنا ذلك عند إنشاء عمود pricePerSqFt
في المثال السابق.
من ناحية أخرى، إذا أردنا تعديل الDataFrame الأصلي هناك حالتان للنظر فيهما.
- إذا امتد العمود الجديد عبر كل صف، يمكننا تعديل الDataFrame الأصلي مباشرة. لن يتسبب هذا في تحذير لأننا لن نختار مجموعة فرعية من الصفوف. على سبيل المثال، يمكننا إضافة عمود
note
لكل صف حيث يكون نوع المنزل مفقودًا.
df["notes"] = df["propertyType"].apply(lambda house_type: "Missing house type" if house_type == "Not Specified" else "")
- إذا كان العمود الجديد يحدد قيمًا فقط لمجموعة فرعية من الصفوف، فيمكننا استخدام خاصية مؤشر الموقع
loc
. على سبيل المثال:
df.loc[df["propertyType"] == "Not Specified", "notes"] = "Missing house type"
يرجى ملاحظة أن في هذه الحالة، قيم الأعمدة التي لم يتم اختيارها ستكون غير معرفة لذلك يُفضل النهج الأول نظراً لأنه يتيح لنا تحديد قيمة لكل صف.
تحذير SettingWithCopy
في خطأ باندا 3.0
في الوقت الحالي، تحذير SettingWithCopy
هو مجرد تحذير، ليس خطأ. لا يزال كودنا يُنفذ، وباندا تُبلغنا ببساطة بضرورة الحذر.
وفقًا للوثائق الرسمية لـ Pandas، لن يتم استخدام SettingWithCopyWarning
بعد الآن بدءًا من الإصدار 3.0 وسيتم استبداله بخطأ فعلي افتراضيًا، لفرض معايير أكثر صرامة للشفرة.
للتأكد من أن شيفرتنا تظل متوافقة مع الإصدارات المستقبلية من pandas، يُوصى بالفعل بتحديثها لرفع خطأ بدلاً من تحذير.
يتم ذلك عن طريق تعيين الخيار التالي بعد استيراد pandas:
import pandas as pd pd.options.mode.copy_on_write = True
إضافة هذا إلى الكود الموجود ستضمن أننا نتعامل مع كل تعيين غامض في كودنا ونتأكد من أن الكود لا يزال يعمل عندما نقوم بالتحديث إلى pandas 3.0.
الاستنتاج
يحدث SettingWithCopyWarning
كلما جعل كودنا غير واضح ما إذا كانت القيمة التي نقوم بتعديلها هي عرض أو نسخة. يمكننا إصلاح ذلك من خلال أن نكون دائمًا واضحين بشأن ما نريد:
- إذا أردنا العمل مع نسخة، يجب أن نقوم بنسخها بشكل صريح باستخدام طريقة
copy()
. - إذا أردنا تعديل إطار البيانات الأصلي، يجب أن نستخدم خاصية فهرس
loc
ونقوم بتعيين القيمة مباشرة عند الوصول إلى البيانات دون استخدام متغيرات وسيطة.
على الرغم من عدم كونه خطأ، يجب ألا نتجاهل هذا التحذير لأنه يمكن أن يؤدي إلى نتائج غير متوقعة. علاوة على ذلك، ابتداءً من إصدار Pandas 3.0، سيصبح خطأ بشكل افتراضي، لذلك يجب علينا تأمين كودنا للمستقبل من خلال تفعيل نسخة على الكتابة في كودنا الحالي باستخدام pd.options.mode.copy_on_write = True
. سيضمن ذلك استمرار عمل الكود للإصدارات المستقبلية من Pandas.
Source:
https://www.datacamp.com/tutorial/settingwithcopywarning-pandas