SettingWithCopyWarning
是 Pandas 在对 DataFrame 进行赋值时可能引发的警告。这可能发生在我们使用链式赋值或者使用从切片创建的 DataFrame 时。这是 Pandas 代码中常见的错误源,我们都曾经面对过。由于警告可能出现在看起来应该正常工作的代码中,因此很难调试。
理解 SettingWithCopyWarning
很重要,因为它标志着数据操作可能存在潜在问题。这个警告表示您的代码可能没有按预期修改数据,这可能导致意外后果和难以跟踪的错误。
在本文中,我们将探讨 Pandas 中的 SettingWithCopyWarning
及如何避免它。我们还将讨论 Pandas 的未来以及 copy_on_write
选项将如何改变我们处理 DataFrames 的方式。
DataFrame 视图和副本
当我们选择 DataFrame 的切片并将其赋值给一个变量时,我们可能会得到一个视图或一个新的 DataFrame 副本。
使用视图时,两个 DataFrame 之间共享内存。这意味着修改存在于两个 DataFrame 中的单元格的值将同时修改这两个 DataFrame。
使用副本时,会分配新的内存,并创建一个与原始 DataFrame 具有相同值的独立 DataFrame。在这种情况下,两个 DataFrame 是不同的实体,因此在其中一个 DataFrame 中修改值不会影响另一个 DataFrame。
Pandas尝试避免在可能的情况下创建副本以优化性能。然而,事先无法预测我们会得到视图还是副本。每当我们为不清楚是副本还是从另一个DataFrame获取的视图的DataFrame分配值时,都会引发SettingWithCopyWarning
。
通过真实数据了解SettingWithCopyWarning
我们将使用这个Real Estate Data London 2024 Kaggle数据集来学习SettingWithCopyWarning
是如何发生以及如何解决的。
该数据集包含来自伦敦的最新房地产数据。以下是数据集中存在的列的概述:
addedOn
:添加清单的日期。title
:清单的标题。descriptionHtml
:清单的HTML描述。propertyType
:物业类型。如果未指定类型,则值将为"Not Specified"
。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"] # 更新这些行上的属性类型为“House” 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会发出警告,让我们知道这个事实。
即使我们的代码因为得到了一个视图而正确运行,但在随后的运行中可能得到一个副本,代码将无法按预期工作。因此,重要的是不要忽略此警告,并确保我们的代码始终能够按我们希望的方式运行。
带有隐藏临时变量的示例
在前面的示例中,很明显使用了临时变量,因为我们明确地将DataFrame的一部分分配给了一个名为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如果需要是副本就是副本,如果需要是视图就是视图。
使用loc
安全地修改原始DataFrame
让我们修复上面示例中想要修改原始DataFrame的代码。为了避免使用临时变量,使用loc
索引器属性。
df.loc[df["propertyType"] == "Not Specified", "propertyType"] = "House"
通过这段代码,我们直接通过loc
索引器属性在原始df
DataFrame上操作,因此不需要中间变量。这是我们想要直接修改原始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
进行赋值,而不使用额外的索引。
使用copy()
安全地处理原始DataFrame的副本
当我们想要确保我们在 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"
请注意,在这种情况下,未选择的列上的值将是未定义的,因此首选第一种方法,因为它允许我们为每一行指定一个值。
SettingWithCopyWarning
在 Pandas 3.0 中的错误
目前,SettingWithCopyWarning
只是一个警告,而不是一个错误。我们的代码仍然会执行,并且Pandas只是提醒我们要小心。
根据官方Pandas文档,SettingWithCopyWarning
不再在3.0版本及以后使用,而将被一个实际的错误默认取代,强制执行更严格的代码标准。
为了确保我们的代码与未来版本的pandas兼容,建议立即更新以引发错误而不是警告。
通过在导入pandas后设置以下选项来实现:
import pandas as pd pd.options.mode.copy_on_write = True
将这些代码添加到现有代码中,可以确保我们处理代码中的每个模棱两可的赋值,并在更新到pandas 3.0时确保代码仍然有效。
结论
SettingWithCopyWarning
在我们的代码使得修改的值是视图还是副本变得模棱两可时发生。我们可以通过始终明确我们的意图来解决这个问题:
- 如果我们想要使用副本,应该明确使用
copy()
方法复制它。 - 如果我们想要修改原始DataFrame,应该使用
loc
索引器属性,并在访问数据时直接赋值而不使用中间变量。
尽管这不是一个错误,但我们不应忽视这个警告,因为它可能导致意想不到的结果。此外,从 Pandas 3.0 开始,默认情况下它将变成一个错误,因此我们应该通过在当前代码中使用 pd.options.mode.copy_on_write = True
来打开写时复制,以未来证明我们的代码。这将确保代码在未来的 Pandas 版本中保持功能正常。
Source:
https://www.datacamp.com/tutorial/settingwithcopywarning-pandas