Chuyển đến nội dung chính

SettingWithCopyWarning trong Pandas: Cách khắc phục cảnh báo này

Tìm hiểu cách khắc phục SettingWithCopyWarning của Pandas: Đang cố gắng gán một giá trị trên bản sao của một lát cắt từ DataFrame.
· 8 phút đọc

Khám phá cùng AI

SettingWithCopyWarning là một cảnh báo mà Pandas có thể đưa ra khi chúng ta gán giá trị cho một DataFrame. Điều này có thể xảy ra khi dùng phép gán xâu chuỗi (chained assignment) hoặc khi làm việc với một DataFrame được tạo từ một lát cắt (slice). Đây là nguồn gốc thường gặp của lỗi trong mã Pandas mà hầu hết chúng ta từng gặp. Nó khó gỡ lỗi vì cảnh báo có thể xuất hiện ở những đoạn mã trông có vẻ vẫn chạy ổn.

Hiểu về SettingWithCopyWarning là quan trọng vì nó báo hiệu các vấn đề tiềm ẩn trong thao tác dữ liệu. Cảnh báo này cho thấy mã của bạn có thể không thay đổi dữ liệu như dự định, dẫn đến hậu quả không mong muốn và các lỗi khó lần ra.

Trong bài viết này, chúng ta sẽ tìm hiểu về SettingWithCopyWarning trong pandas và cách tránh nó. Chúng ta cũng sẽ bàn về tương lai của Pandas và cách tùy chọn copy_on_write sẽ thay đổi cách chúng ta làm việc với DataFrame.

View và Copy của DataFrame

Khi chúng ta chọn một lát cắt của DataFrame và gán nó cho một biến, ta có thể nhận được hoặc là một view, hoặc là một bản sao (copy) DataFrame mới.

Với view, bộ nhớ giữa hai DataFrame được chia sẻ. Nghĩa là khi sửa một ô có mặt ở cả hai DataFrame, cả hai sẽ cùng bị thay đổi.

Ví dụ về view của Pandas DataFrame

Với copy, bộ nhớ mới được cấp phát và một DataFrame độc lập với các giá trị giống bản gốc được tạo ra. Khi đó, hai DataFrame là hai thực thể tách biệt, nên sửa một giá trị ở cái này sẽ không ảnh hưởng đến cái kia.

Ví dụ về bản sao DataFrame

Pandas cố gắng tránh tạo bản sao khi có thể để tối ưu hiệu năng. Tuy nhiên, không thể dự đoán trước được ta sẽ nhận về view hay copy. SettingWithCopyWarning được đưa ra mỗi khi chúng ta gán giá trị cho một DataFrame mà không rõ đó là bản sao hay view từ một DataFrame khác.

Hiểu SettingWithCopyWarning với dữ liệu thực

Chúng ta sẽ dùng bộ dữ liệu Kaggle Real Estate Data London 2024 để tìm hiểu SettingWithCopyWarning xuất hiện như thế nào và cách khắc phục. 

Bộ dữ liệu này chứa dữ liệu bất động sản gần đây ở London. Dưới đây là tổng quan các cột có trong dữ liệu:

  • addedOn: Ngày đăng tin.
  • title: Tiêu đề tin đăng.
  • descriptionHtml: Mô tả HTML của tin.
  • propertyType: Loại bất động sản. Giá trị sẽ là "Not Specified" nếu không được chỉ định.
  • sizeSqFeetMax: Diện tích tối đa tính bằng foot vuông.
  • bedrooms: Số phòng ngủ.
  • listingUpdatedReason: Lý do cập nhật tin (ví dụ: tin mới, giảm giá).
  • price: Giá tin đăng tính bằng bảng Anh.

Ví dụ với biến tạm thời tường minh

Giả sử ta được thông báo rằng các bất động sản không chỉ định loại đều là nhà. Ta muốn cập nhật tất cả các hàng có propertyType bằng "Not Specified" thành "House". Một cách là lọc các hàng này vào một biến DataFrame tạm và cập nhật cột propertyType như sau:

import pandas as pd
dataset_name = "realestate_data_london_2024_nov.csv"
df = pd.read_csv(dataset_name)

# Obtain all rows with unspecified property type
no_property_type = df[df["propertyType"] == "Not Specified"]

# Update the property type to “House” on those rows
no_property_type["propertyType"] = "House"

Chạy đoạn mã này sẽ khiến pandas đưa ra 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"

Lý do là pandas không thể biết no_property_type là view hay copy của df. 

Khi gán một phần DataFrame cho biến, có thể nhận view hoặc copy nhưng không biết trước được

Đây là vấn đề vì hành vi của đoạn mã tiếp theo có thể khác nhau rất nhiều tùy thuộc đó là view hay copy. 

Trong ví dụ này, mục tiêu là sửa DataFrame gốc. Điều này chỉ xảy ra nếu no_property_type là view. Nếu phần còn lại của mã giả định rằng df đã được sửa, giả định đó có thể sai vì không có cách nào đảm bảo điều này. Do hành vi không chắc chắn đó, Pandas phát cảnh báo để nhắc chúng ta.

Ngay cả khi mã chạy đúng vì ta tình cờ nhận được view, ở những lần chạy sau có thể ta lại nhận copy và mã sẽ không hoạt động như mong muốn. Vì vậy, đừng bỏ qua cảnh báo này và hãy đảm bảo mã luôn làm đúng điều ta muốn.

Ví dụ với biến tạm thời ẩn

Trong ví dụ trước, dễ thấy là ta dùng biến tạm vì ta gán tường minh một phần DataFrame cho biến no_property_type.

Tuy nhiên, đôi khi điều này không hiển thị rõ ràng. Trường hợp phổ biến nhất gây ra SettingWithCopyWarning là lập chỉ mục xâu chuỗi (chained indexing). Giả sử ta thay hai dòng cuối bằng một dòng:

df[df["propertyType"] == "Not Specified"]["propertyType"] = "House"

Nhìn qua thì có vẻ không tạo biến tạm. Nhưng chạy lên vẫn nhận SettingWithCopyWarning. 

Cách mã này được thực thi là:

  1. df[df["propertyType"] == "Not Specified"] được đánh giá và lưu tạm vào bộ nhớ. 
  2. Chỉ mục ["propertyType"] của vị trí bộ nhớ tạm đó được truy cập. 

Các lần truy cập chỉ mục được đánh giá tuần tự nên chained indexing gây cảnh báo tương tự vì không biết kết quả trung gian là view hay copy

Các lần truy cập chỉ mục được đánh giá từng bước, nên chained indexing dẫn đến cùng một cảnh báo vì không biết kết quả trung gian là view hay copy. Đoạn mã trên về bản chất giống hệt như:

tmp = df[df["propertyType"] == "Not Specified"]
tmp["propertyType"] = "House"

Ví dụ này thường được gọi là chained indexing vì ta xâu chuỗi các lần truy cập chỉ mục bằng []. Đầu tiên truy cập [df["propertyType"] == "Not Specified"] rồi đến ["propertyType"].

Chained indexing

Cách khắc phục SettingWithCopyWarning

Hãy học cách viết mã sao cho không còn mơ hồ và không kích hoạt SettingWithCopyWarning. Ta đã biết cảnh báo xuất phát từ sự mơ hồ về việc một DataFrame là view hay copy của DataFrame khác.

Cách sửa là đảm bảo mỗi DataFrame ta tạo ra là copy nếu ta muốn copy, hoặc là view nếu ta muốn view.

Sửa an toàn DataFrame gốc bằng loc 

Hãy sửa đoạn mã ở ví dụ trên, nơi ta muốn chỉnh trực tiếp DataFrame gốc. Để tránh dùng biến tạm, hãy dùng thuộc tính lập chỉ mục loc.

df.loc[df["propertyType"] == "Not Specified", "propertyType"] = "House"

Với đoạn mã này, ta thao tác trực tiếp trên DataFrame gốc df thông qua thuộc tính loc, nên không cần biến trung gian. Đây là cách cần làm khi muốn sửa trực tiếp DataFrame gốc.

Nhìn thoáng qua, điều này giống chained indexing vì vẫn có tham số, nhưng không phải. Điều xác định mỗi lần lập chỉ mục là dấu ngoặc vuông [].

Loc double index vs chained indexing

Lưu ý rằng chỉ dùng loc mới an toàn nếu ta gán giá trị trực tiếp, như trên. Nếu dùng biến tạm, ta lại gặp vấn đề cũ. Dưới đây là hai ví dụ không khắc phục được vấn đề: 

  1. Dùng loc với biến tạm:
# Using loc plus temporary variable doesn’t fix the issue
no_property_type = df.loc[df["propertyType"] == "Not Specified"]
no_property_type["propertyType"] = "House"
  1. Dùng loc kết hợp truy cập chỉ mục (tương tự chained indexing):
# Using loc plus indexing is the same as chained indexing
df.loc[df["propertyType"] == "Not Specified"]["propertyType"] = "House"

Cả hai ví dụ này thường gây nhầm lẫn vì nhiều người nghĩ chỉ cần có loc là ta đang sửa dữ liệu gốc. Điều đó không đúng. Cách duy nhất để đảm bảo đang gán giá trị lên DataFrame gốc là gán trực tiếp bằng một lần loc duy nhất, không kèm truy cập chỉ mục tách rời.

Làm việc an toàn với một bản sao của DataFrame gốc bằng copy()

Khi muốn chắc chắn rằng ta thao tác trên một bản sao của DataFrame, hãy dùng phương thức .copy().

Giả sử ta được yêu cầu phân tích giá trên mỗi foot vuông của các bất động sản. Ta không muốn chỉnh sửa dữ liệu gốc. Mục tiêu là tạo một DataFrame mới với kết quả phân tích để gửi cho nhóm khác.

Bước đầu là lọc bớt hàng và làm sạch dữ liệu. Cụ thể, cần:

  • Loại các hàng có sizeSqFeetMax không được định nghĩa.
  • Loại các hàng có price là "POA" (giá cung cấp khi yêu cầu).
  • Chuyển giá sang kiểu số (trong dữ liệu gốc, giá là chuỗi theo định dạng: "£25,000,000")

Ta có thể thực hiện các bước trên bằng đoạn mã sau:

# 1. Filter out all properties without a size or a price
properties_with_size_and_price = df[df["sizeSqFeetMax"].notna() & (df["price"] != "POA")]

# 2. Remove the £ and , characters from the price columns
properties_with_size_and_price["price"] = properties_with_size_and_price["price"].str.replace("£", "", regex=False).str.replace(",", "", regex=False)

# 3. Convert the price column to numeric values
properties_with_size_and_price["price"] = pd.to_numeric(properties_with_size_and_price["price"])

Để tính giá trên mỗi foot vuông, ta tạo một cột mới với giá trị bằng kết quả chia cột price cho cột sizeSqFeetMax:

properties_with_size_and_price["pricePerSqFt"] = properties_with_size_and_price["price"] / properties_with_size_and_price["sizeSqFeetMax"]

Nếu chạy đoạn mã này, ta lại gặp SettingWithCopyWarning. Điều này không bất ngờ vì ta đã tạo và chỉnh sửa một biến DataFrame tạm thời properties_with_size_and_price.

Vì ta muốn làm việc trên một bản sao của dữ liệu thay vì DataFrame gốc, ta có thể khắc phục bằng cách đảm bảo properties_with_size_and_price là một bản sao DataFrame mới, không phải view, bằng cách dùng phương thức .copy() ngay ở dòng đầu:

properties_with_size_and_price = df[df["sizeSqFeetMax"].notna() & (df["price"] != "POA")].copy()

Thêm cột mới một cách an toàn

Tạo cột mới có hành vi giống như gán giá trị. Mỗi khi mơ hồ giữa copy hay view, pandas sẽ đưa ra SettingWithCopyWarning.

Nếu muốn làm việc với một bản sao, ta nên sao chép tường minh bằng phương thức .copy(). Khi đó, ta có thể gán cột mới theo cách nào cũng được. Ta đã làm điều này khi tạo cột pricePerSqFt ở ví dụ trước.

Ngược lại, nếu muốn sửa DataFrame gốc, có hai trường hợp cần xét. 

  1. Nếu cột mới áp dụng cho mọi hàng, ta có thể sửa trực tiếp DataFrame gốc. Điều này sẽ không gây cảnh báo vì ta không chọn một tập con của các hàng. Ví dụ, có thể thêm cột note cho mọi hàng thiếu loại nhà: 
df["notes"] = df["propertyType"].apply(lambda house_type: "Missing house type" if house_type == "Not Specified" else "")
  1. Nếu cột mới chỉ có giá trị cho một tập con hàng, ta có thể dùng thuộc tính lập chỉ mục loc. Ví dụ:
df.loc[df["propertyType"] == "Not Specified", "notes"] = "Missing house type"

Lưu ý trong trường hợp này, giá trị ở các hàng không được chọn sẽ không được định nghĩa, nên cách đầu tiên được ưu tiên vì cho phép chỉ định giá trị cho từng hàng.

SettingWithCopyWarning trở thành lỗi trong Pandas 3.0

Hiện tại, SettingWithCopyWarning chỉ là cảnh báo chứ không phải lỗi. Mã vẫn chạy và Pandas chỉ nhắc ta cẩn thận. 

Theo tài liệu chính thức của Pandas, bắt đầu từ phiên bản 3.0, SettingWithCopyWarning sẽ không còn được dùng nữa và mặc định được thay bằng một lỗi thực sự, áp dụng tiêu chuẩn mã nghiêm ngặt hơn.

Thông tin cập nhật Pandas 3.0

Để đảm bảo mã của chúng ta vẫn tương thích với các phiên bản pandas tương lai, nên chủ động cập nhật để nâng cảnh báo thành lỗi ngay từ bây giờ.

Thực hiện bằng cách đặt tùy chọn sau sau khi import pandas:

import pandas as pd
pd.options.mode.copy_on_write = True

Thêm tùy chọn này vào mã hiện có sẽ buộc chúng ta xử lý từng phép gán mơ hồ và đảm bảo mã vẫn hoạt động khi nâng cấp lên pandas 3.0.

Kết luận

SettingWithCopyWarning xuất hiện khi mã của chúng ta khiến mơ hồ việc giá trị đang sửa là view hay copy. Ta có thể khắc phục bằng cách luôn tường minh điều mình muốn:

  • Nếu muốn làm việc với bản sao, hãy sao chép tường minh bằng phương thức copy().
  • Nếu muốn sửa DataFrame gốc, hãy dùng thuộc tính lập chỉ mục loc và gán giá trị trực tiếp khi truy cập dữ liệu, không dùng biến trung gian.

Dù không phải lỗi, ta không nên bỏ qua cảnh báo này vì nó có thể dẫn đến kết quả bất ngờ. Hơn nữa, từ Pandas 3.0, nó sẽ mặc định trở thành lỗi, nên chúng ta nên chuẩn bị trước bằng cách bật Copy-on-Write trong mã hiện tại với pd.options.mode.copy_on_write = True. Cách này giúp mã vẫn hoạt động ở các phiên bản Pandas tương lai.

Để tìm hiểu thêm về Pandas, hãy tham khảo các tài nguyên sau:

FAQs

Tại sao tôi vẫn nhận SettingWithCopyWarning dù đã dùng .loc?

Dùng loc luôn an toàn và sẽ không kích hoạt cảnh báo hay gây hành vi bất ngờ, với điều kiện df không được tạo ra từ một DataFrame khác. Ví dụ, nếu df là DataFrame được tạo khi đọc tập dữ liệu, thì an toàn. Tuy nhiên, nếu nó là kết quả của một phép tính trung gian, thì chỉ an toàn nếu DataFrame đó đã được sao chép bằng phương thức copy().

Làm thế nào để tắt SettingWithCopyWarning?

Chúng ta có thể dùng gói warnings và thêm đoạn mã:

import warnings
warnings.simplefilter(action='ignore', category=pd.errors.SettingWithCopyWarning)

Tuy nhiên, rất không khuyến khích việc dập tắt hay bỏ qua cảnh báo này, vì nó có nghĩa là mã của bạn có thể không làm điều bạn nghĩ.

Làm sao để bỏ qua SettingWithCopyWarning?

Ta có thể tạm thời bỏ qua cảnh báo bằng gói warnings và bao đoạn mã của mình trong:

import warnings
with warnings.catch_warnings():
    warnings.simplefilter("ignore", category=pd.errors.SettingWithCopyWarning)
  # Our code goes here

Tuy nhiên, rất không khuyến khích việc dập tắt hay bỏ qua cảnh báo này, vì nó cho thấy mã của bạn có thể không làm điều bạn nghĩ.

Làm sao sửa SettingWithCopyWarning khi thêm cột mới?

Khắc phục cảnh báo khi thêm cột mới cũng giống như khi gán giá trị. Hãy đảm bảo sao chép DataFrame trước nếu bạn định làm việc trên một bản sao dữ liệu, hoặc dùng loc.

Tôi đã làm theo hướng dẫn mà vẫn gặp SettingWithCopyWarning. Sửa thế nào?

Với các phiên bản pandas mới nhất, làm theo thực hành trong hướng dẫn này sẽ không kích hoạt cảnh báo.

Nếu bạn dùng phiên bản cũ, hãy cập nhật lên bản ổn định mới nhất. Các phiên bản cũ đã biết có lỗi quanh cảnh báo này và có thể kích hoạt cảnh báo ngay cả khi mã không có vấn đề.

Nếu phiên bản pandas đã cập nhật, nhiều khả năng vấn đề gốc là một trường hợp chained indexing không hiển nhiên. Hãy xem kỹ DataFrame được tạo ra như thế nào.

François Aubry

Tác giả

François Aubry

Kỹ sư full-stack & nhà sáng lập tại CheapGPT. Giảng dạy luôn là niềm đam mê của tôi. Từ những ngày còn là sinh viên, tôi đã háo hức tìm kiếm cơ hội kèm cặp và hỗ trợ các bạn học khác. Niềm đam mê đó đã thôi thúc tôi theo học tiến sĩ, nơi tôi cũng đảm nhiệm vai trò trợ giảng để hỗ trợ con đường học thuật của mình. Trong những năm ấy, tôi tìm thấy sự mãn nguyện lớn lao trong môi trường lớp học truyền thống, xây dựng kết nối và thúc đẩy việc học tập. Tuy nhiên, với sự ra đời của các nền tảng học trực tuyến, tôi nhận ra tiềm năng thay đổi của giáo dục số. Thực tế, tôi đã trực tiếp tham gia phát triển một nền tảng như vậy tại trường đại học của chúng tôi. Tôi tận tâm kết hợp các nguyên tắc giảng dạy truyền thống với những phương pháp số đổi mới. Đam mê của tôi là tạo ra các khóa học không chỉ hấp dẫn và giàu thông tin mà còn dễ tiếp cận với người học trong thời đại số này.

Chủ đề

Liên quan