pandas: типы столбцов и очистка по правилу

Текстовые столбцы: нормализация и разбор через .str

Содержание курса

Фильтрация строк: .str.contains и .str.startswith

После нормализации и разбивки текста следующий практический шаг — отобрать строки по текстовому условию. Для этого в pandas есть два метода: .str.contains и .str.startswith. Оба возвращают булевый Series, который сразу можно передать в df.loc.

.str.startswith(prefix) проверяет, начинается ли каждая строка с заданного префикса. Всегда буквальный — без регулярных выражений.

.str.contains(pat) проверяет вхождение подстроки или паттерна. По умолчанию regex=True, то есть pat интерпретируется как регулярное выражение. Для буквального поиска нужно явно передать regex=False.

import pandas as pd

df = pd.DataFrame({
    'name': ['alice smith', 'bob jones', 'carol brown', 'alice johnson']
})

# Все строки, начинающиеся с 'alice'
mask_alice = df['name'].str.startswith('alice')
print(df.loc[mask_alice])
#            name
# 0   alice smith
# 3  alice johnson

# Все строки, содержащие 'jones' (буквально)
mask_jones = df['name'].str.contains('jones', regex=False)
print(df.loc[mask_jones])
#         name
# 1  bob jones

Маска работает ровно так же, как любая другая булева маска в df.loc — механизм тот же, что использовался для числовых условий.

Пропуски в исходном столбце: поведение зависит от dtype.

Если столбец имеет тип object (стандартный при загрузке через read_csv), то при наличии None в строке результат .str.contains для этой строки будет None — и передача такой маски в df.loc вызовет ValueError:

df_obj = pd.DataFrame({'name': ['alice smith', None, 'carol brown']})
# dtype: object

print(df_obj['name'].str.contains('alice', regex=False))
# 0     True
# 1     None
# 2    False
# dtype: object

# df_obj.loc[mask]  — вызовет ValueError из-за None в маске

Если столбец явно создан с dtype='string' (StringDtype), пропуск даёт pd.NA в nullable boolean-маске. В pandas 2.x df.loc принимает такую маску, трактуя pd.NA как «строка не выбрана» — ошибки нет.

Однако полагаться на это различие в коде неудобно: поведение зависит от dtype столбца, который не всегда очевиден. Универсальное решение — передавать na=False. Тогда пропуск явно трактуется как «не совпало», и маска всегда двухзначная (True/False) независимо от dtype:

# Работает одинаково для object и StringDtype:
mask = df_obj['name'].str.contains('alice', regex=False, na=False)
print(df_obj.loc[mask])
#           name
# 0  alice smith

na=False — стандартная страховка всякий раз, когда столбец мог прийти с пропусками.

Пример: нормализация → разбивка → фильтрация. Все три шага урока на одном наборе данных:

import pandas as pd

df = pd.DataFrame({
    'raw': ['  Alice Smith ', '  BOB jones', 'carol BROWN  ', '  Alice Johnson']
})

# Шаг 1: нормализация
df['clean'] = df['raw'].str.strip().str.lower()

# Шаг 2: разбивка на имя и фамилию
split_df = df['clean'].str.split(' ', expand=True)
df['first'] = split_df[0]
df['last']  = split_df[1]

# Шаг 3: фильтрация — только записи с именем 'alice'
mask = df['first'].str.startswith('alice')
result = df.loc[mask, ['first', 'last']]
print(result)
#    first     last
# 0  alice    smith
# 3  alice  johnson

После нормализации фильтрация работает предсказуемо: нет случайных промахов из-за разного регистра или пробелов. Именно поэтому strip + lower делают первыми, а .str.contains / .str.startswith применяют уже к чистым данным.

Если нужно составное условие — например, имя начинается с 'alice' и фамилия содержит 'sm' — маски комбинируют через &:

mask2 = (df['first'].str.startswith('alice') &
         df['last'].str.contains('sm', regex=False))
print(df.loc[mask2, ['first', 'last']])
#    first   last
# 0  alice  smith

Обе маски — обычные булевы Series, и правила их комбинирования те же, что для любых других условий фильтрации в pandas.