2025, Sep 29 09:16

Как в pandas read_csv различить пустые строки и NaN в CSV

Как в pandas read_csv сохранить различие между пустой строкой и NaN: quoting=3, очистка кавычек в текстовых столбцах, осторожное приведение чисел. С нюансами.

Различить намеренно пустую строку и отсутствующее значение в CSV кажется простым, пока pandas незаметно не уравнивает одно с другим. Если в файле вперемешку встречаются пустые поля в кавычках вроде "" и действительно пропущенные записи как ,, — вы, скорее всего, хотите, чтобы первые остались пустой строкой, а вторые превратились в NaN. Из коробки read_csv() эту грань не сохраняет.

Исходные данные: что теряется при чтении

Посмотрим на небольшой CSV, где оба случая идут рядом:

id,name,age
1,,"25"
2,"",3
3,John,

При стандартном разборе кавычки не несут смысла: парсер выведет числовые типы и будет считать пустоты NaN:

import io
import pandas as pd
sample = """id,name,age
1,,"25"
2,"",3
3,John,""""""
frame_a = pd.read_csv(io.StringIO(sample))
print(frame_a.dtypes)
# id        int64
# name     object
# age     float64
# dtype: object

Почему так происходит

Кавычки в CSV — это всего лишь механизм отделения полей и экранирования; это не подсказка для типов. Они позволяют оставлять запятые и специальные символы внутри поля, но не задают тип. Поэтому "25" обычно читается так же, как 25, и будет интерпретирована числом. Аналогично, полностью пустое поле между разделителями трактуется как пропущенное.

Попытка поменять quotechar на другой символ может сохранить "" как буквальную строку, но всё сломается, как только двойные кавычки действительно понадобятся, чтобы защитить текст с запятыми. К тому же затем придётся вычищать кавычки из каждой затронутой строки.

Рабочий приём: отключить интерпретацию кавычек, затем нормализовать

Если в ваших кавычках никогда не встречается разделитель, можно читать файл с quoting=3 (csv.QUOTE_NONE), чтобы воспринимать кавычки как обычные символы, а потом снимать начальные/конечные кавычки только в текстовых столбцах. Так сохраняется различие: ,, превращается в NaN, а "" — в пустую строку после очистки.

import io
import pandas as pd
payload = """id,name,age
1,,"25"
2,"",3
3,John,""""""
grid = pd.read_csv(io.StringIO(payload), quoting=3)
#    id  name   age
# 0   1   NaN  "25"
# 1   2    ""     3
# 2   3  John   NaN
grid.update(
    grid.select_dtypes('O').apply(
        lambda col: col.str.replace(r'^"|"$', '', regex=True)
    )
)
print(grid)
#    id  name  age
# 0   1   NaN   25
# 1   2        3
# 2   3  John  NaN

На этом этапе все ненулевые значения в age — строки, даже если выглядят как числа:

print(grid['age'].tolist())
# ['25', '3', nan]

Вариант: числа оставить числами, а строки в кавычках — строками

Если нужно, чтобы "25" осталась строкой, а 3 — числом, сначала приводите значения к числам, а для всего остального используйте текст без обрамляющих кавычек:

import io
import pandas as pd
payload = """id,name,age
1,,"25"
2,"",3
3,John,""""""
canvas = pd.read_csv(io.StringIO(payload), quoting=3)
canvas.update(
    canvas.select_dtypes('O').apply(
        lambda col: pd.to_numeric(col, errors='coerce').combine_first(
            col.str.replace(r'^"|"$', '', regex=True)
        )
    )
)
print(canvas)
#    id  name  age
# 0   1   NaN   25
# 1   2        3.0
# 2   3  John  NaN
print(canvas['name'].tolist())
# [nan, '', 'John']
print(canvas['age'].tolist())
# ['25', 3.0, nan]

Важное ограничение

Подход с quoting=3 предполагает, что в кавычках нет разделителя. Он сломается, как только в кавычках действительно понадобится запятая:

id,name,age
1,,"25"
2,",",3
3,John,

В таких случаях безопаснее заранее предобработать файл или написать собственный парсер, если нужно выводить типы, опираясь на кавычки.

Зачем это важно

Между пустой строкой и пропущенным значением часто есть смысловая разница. От неё зависят аналитика, соединения данных, проверки и экспорт для пользователей. Сохранение этого различия на раннем этапе загрузки предотвращает скрытые ошибки и избавляет от переделок позже.

Итоги

CSV сам по себе не кодирует типы; кавычки этого не меняют. Если нужно трактовать ,, как NaN, а "" — как пустую строку, читайте с отключённой обработкой кавычек и нормализуйте текстовые столбцы. Если при этом хотите оставить числовые поля числами, а текст в кавычках — строками, сперва приводите к числам и для остальных значений используйте текст без кавычек. Когда данные действительно требуют кавычек для защиты разделителей, закладывайтесь на предобработку или собственный парсер.

Статья основана на вопросе на StackOverflow от пользователя Despicable me и ответе от mozway.