Текстовые столбцы: нормализация и разбор через .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.
