← 回首頁

pandas 3 真正會弄壞你程式的那個改動,不會警告你

大家都在修 SettingWithCopyWarning,但那個寫法本來就沒有生效過。真正今天有效、在 Copy-on-Write 下會安靜失效的那個寫法,兩個版本都不發任何警告。

淡藍色方塊直接穿過一個空的透明格架落下,在下方地面堆積,而格架完全沒有任何變化。

你找得到的 pandas 3 遷移建議,大部分在講 SettingWithCopyWarning。 那是錯的擔心對象,而且值得把理由講精確:那個警告針對的寫法,本來就沒有生效過。 把它修掉,你的結果一個數字都不會變。

真正有生效、而且即將失效的那個寫法,兩個方向都是安靜的。

要緊的是這一個

在 pandas 2.3.3(Python 3.9.6)上實測:

df = pd.DataFrame({"a": [1, 2, 3], "b": [10, 20, 30]})
col = df["a"]
col.iloc[0] = 99
df["a"].tolist()
結果 警告
pandas 2 的預設 [99, 2, 3] 無
pandas 3 的預設(Copy-on-Write) [1, 2, 3] 無

那個值從 99 變成 1。兩個版本都一個字都沒說。

這就是問題的全部。拿 -W error::FutureWarning 跑測試——pandas 遷移的標準建議—— 找不到這個,因為根本沒有警告可以被升級成錯誤。

自己比對

同一段程式碼,pandas 2 與 pandas 3
col = df["a"]
col.iloc[0] = 99
df["a"].tolist()

pandas 2 的預設

結果 [99, 2, 3]

警告

無

pandas 3 的預設

結果 [1, 2, 3]

警告

無

要擔心的是這一格。結果變了,而且兩個版本都不吭一聲——所以拿 -W error::FutureWarning 跑測試找不到它。

警告過濾器抓得到的那些

這些今天就會發出警告,所以把警告變成錯誤跑一次就能找出全部出現的地方。它們是簡單的那一半。

  • df.applymap(fn)

    DataFrame.applymap has been deprecated. Use DataFrame.map instead.

  • s.fillna(method='ffill')

    Series.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.

  • s.replace({1: 1.5}) # object dtype

    Downcasting behavior in `replace` is deprecated and will be removed in a future version. To retain the old behavior, explicitly call `result.infer_objects(copy=False)`. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`

  • df.groupby("g").apply(fn)

    DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.

於 2026-08-07 在 pandas 2.3.3 / Python 3.9.6 上實測。

六種寫法,每一種在兩個預設下各跑一次,結果與警告逐字錄下。 六種裡面,剛好只有一種是結果會變而兩邊都不出聲的。

為什麼那個有名的警告是錯的目標

鏈式賦值在兩種預設下的結果完全相同:

df[df.a > 1]["b"] = 0
# pandas 2: [10, 20, 30]   SettingWithCopyWarning
# pandas 3: [10, 20, 30]   ChainedAssignmentError

它以前沒有更新到 DataFrame,現在也沒有。改善的是那句話的誠實程度。 pandas 2 說這個物件可能是副本;Copy-on-Write 說它永遠不會生效, 因為中間物件一律表現得像副本。

那是一個真正的進步——一個「可能」變成了一個保證——但它是文件層面的改善, 不是行為改變。你的結果本來是什麼就還是什麼。

inplace 那一種在中間

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

這一種確實會改變結果——[1.0, 0.0, 3.0] 變成 [1.0, nan, 3.0]—— 但 pandas 2 有警告,而且那句警告把話講完了:

The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.

它甚至直接給你改法:df.method({col: value}, inplace=True)。 所以這一種在升級前就找得到。這是好事。

哪些不用改

六種裡有兩種是穩定的,知道這件事會省下工:

df.loc[df.a > 1, "b"] = 0        # 兩邊完全相同,都沒有警告
renamed = df.rename(...)          # 本來就回傳副本,什麼都沒變

單步的 .loc 是你要把程式碼改成的那個目標。它不是一個繞路的作法—— 它是那個「行為不取決於你跑的是哪一版 pandas」的寫法。

實際上該怎麼找

不是用警告過濾器,是用那個開關:

pd.options.mode.copy_on_write = True

這個選項在 pandas 2 上就是 pandas 3 的行為。把它打開、跑你的測試, 任何失敗的地方都是升級之後遲早會壞的地方——而且是安靜地、在正式環境裡壞。 成本是一行程式碼加一次測試。

那些棄用警告仍然值得跑一輪——applymap、fillna(method=...)、replace 的降型警告、 groupby().apply() 對分組欄位的行為——而它們是真的簡單:它們會自己出聲, 所以把警告當成錯誤跑一次就找得出全部。

但那是你本來就會找到的那一半。把一個下午花在不會告訴你的那一半上。

常見追問

升級 pandas 3 前該修的是 SettingWithCopyWarning 嗎?

它最吵,但不是危險的那個。在 pandas 2.3.3 上實測,鏈式賦值在兩種預設下的結果 完全相同——它本來就沒有更新到 DataFrame。變的是訊息:從「可能是副本」變成 明說「永遠不會生效」。

那真正會改變結果的是哪一種寫法?

把中間物件綁成變數再寫入它。col = df["a"] 接著 col.iloc[0] = 99,在 pandas 2 上 會改到原始的 DataFrame,在 Copy-on-Write 下不會。換成 .iloc 切片也會變, 不過那一種 pandas 2 至少有警告。

升級前要怎麼找出這些地方?

設 pd.options.mode.copy_on_write = True,然後跑你的測試。這個選項在 pandas 2 上 就是 pandas 3 的行為,所以任何在它之下壞掉的東西,都是升級之後遲早會壞的東西。 警告過濾器取代不了它,因為真正要緊的那種寫法不發警告。

安全的寫法是什麼?

單步的 .loc 賦值。df.loc[df.a > 1, "b"] = 0 在兩種預設下結果完全相同、兩邊都沒有 警告——這就是值得把程式碼改成它的理由。

那些棄用警告就不重要了嗎?

重要,但它們是簡單的那一半——applymap、fillna(method=...)、replace 的降型警告、 groupby().apply() 對分組欄位的行為,全部都會自己出聲,所以把警告當成錯誤跑一次 就能找出全部出現的地方。