2025, Nov 21 12:01

Почему в Pandas 2.x str.replace ломает парсинг 'nan' и как это исправить

Обновление до Pandas 2.x ломает str.replace: regex по умолчанию отключен, из‑за чего to_numeric падает на 'nan'. Разбираем причину и даем фикс regex=True.

Преобразование квартальной таблицы в аккуратный датафрейм Pandas с двумя колонками — рутинная задача. Но после обновления библиотеки тот же код внезапно падает с ValueError при разборе строки "nan". Если ситуация знакома, дело не в ваших данных — причина в незаметном изменении Pandas, которое перевернуло поведение важной строковой операции.

Что мы хотим получить

Исходник — «широкая» таблица с пронумерованными столбцами. Цель — привести её к «длинному» виду: два столбца, list_number и item_code, по одной строке на значение. После преобразования пустые ячейки нужно удалить, чтобы конвертация в числа проходила без проблем.

Код, который стал падать

Ниже — упрощённый конвейер преобразований, использовавшийся изначально. Он меняет форму датафрейма, удаляет всё, кроме цифр, и приводит значения к числам.

frame_raw = source_df
frame_raw.dropna(axis=1, how='all', inplace=True)
frame_long = pd.melt(frame_raw, var_name='LIST_NUM', value_name='ITEM_ID')

frame_long['LIST_NUM'] = pd.to_numeric(
    frame_long['LIST_NUM'].astype(str).str.replace(r'[^\d]', ''),
    errors='raise')

frame_long['ITEM_ID'] = pd.to_numeric(
    frame_long['ITEM_ID'].astype(str).str.replace(r'[^\d]', ''),
    errors='raise')

После обновления до Pandas 2.1.4 (с версии до 2.0) код стал выбрасывать ValueError: Unable to parse string "nan". Любопытно, что у коллеги на Pandas 1.4.4 он продолжал работать.

Что именно сломалось и почему

Причина — несовместимое изменение значения по умолчанию в Pandas 2.0.0. В этом релизе параметр regex метода Series.str.replace() по умолчанию сменился с True на False. Поскольку в коде не указано regex=True, шаблон r"[^\d]" больше не интерпретируется как регулярное выражение. В итоге замена перестаёт удалять нецифровые символы и, что важно, после astype(str) отсутствующее значение превращается в буквальную строку "nan". С посторонним текстом на месте to_numeric(..., errors='raise') натыкается на "nan" и падает.

Есть и второй источник путаницы. В исходном конвейере dropna вызывается с axis=1, how='all' — это удаляет только те столбцы, которые целиком состоят из NA. Отдельные пропуски в «расплавленном» столбце значений оно не трогает. После изменения формы записи остаются, а после astype(str) превращаются в строки "nan". Иными словами, проблема не в melt, а в порядке очистки и в новом значении по умолчанию у str.replace.

Решение: явно указать поведение regex

Минимальное и точечное исправление — передать regex=True в str.replace, чтобы шаблон трактовался как регулярное выражение во всех версиях Pandas.

frame_raw = source_df
frame_raw.dropna(axis=1, how='all', inplace=True)
frame_long = pd.melt(frame_raw, var_name='LIST_NUM', value_name='ITEM_ID')

frame_long['LIST_NUM'] = pd.to_numeric(
    frame_long['LIST_NUM'].astype(str).str.replace(r'[^\d]', '', regex=True),
    errors='raise')

frame_long['ITEM_ID'] = pd.to_numeric(
    frame_long['ITEM_ID'].astype(str).str.replace(r'[^\d]', '', regex=True),
    errors='raise')

Так мы возвращаем семантику str.replace из эпохи до 2.0 и не даём строкам "nan" просочиться в числовую конвертацию.

Порядок действий, который не оставляет лишних NaN дальше по конвейеру

Если цель — убрать пустые значения из столбца с элементами после изменения формы, сделайте это сразу после melt. Так шаг очистки совпадёт с тем местом, где реально возникают пропуски.

base_df = source_df
reshaped = pd.melt(base_df, var_name='LIST_NUM', value_name='ITEM_ID').dropna(subset=['ITEM_ID'])

reshaped['LIST_NUM'] = pd.to_numeric(
    reshaped['LIST_NUM'].astype(str).str.replace(r'[^\d]', '', regex=True),
    errors='raise')

reshaped['ITEM_ID'] = pd.to_numeric(
    reshaped['ITEM_ID'].astype(str).str.replace(r'[^\d]', '', regex=True),
    errors='raise')

Такой порядок удаляет проблемные строки с NA до любой строковой конвертации — они не успевают превратиться в буквальный текст "nan".

Почему это важно

При обновлениях версии значения по умолчанию в популярных API могут незаметно поменяться, и конвейеры, завязанные на неявное поведение, начнут падать или, что хуже, давать скрытые искажения данных. Явно задавайте намерение в именованных параметрах — особенно там, где речь о текстовой обработке и приведении типов: это простой способ сохранить стабильность кода в разных окружениях. Помогают и понятные имена переменных: различайте исходный «широкий» датафрейм и его «расплавленную» версию, чтобы проще читать и отлаживать цепочки преобразований.

Итоги

Если ранее стабильный конвейер в Pandas после обновления падает с ошибкой ValueError при разборе "nan", проверьте вызовы .str.replace(), которые опираются на синтаксис регулярных выражений, и добавьте regex=True. Обрабатывайте пропуски там, где они действительно появляются — в данном случае после melt, — чтобы они не превращались в строки, ломающие числовой парсинг. И, наконец, при разборе различий между окружениями сведите пример к минимально воспроизводимому — так проще обнаружить и исправить проблемы, вызванные версией.