Урок курса
Текстовые столбцы: нормализация и разбор через .str
NumPy и pandas: практический тренажёрВ прошлом уроке мы разбирались с пропусками: обнаруживали их, удаляли или заполняли. Там же появился StringDtype — тип, специально заточенный под текстовые столбцы. В этом уроке переходим к следующему шагу: что делать с самим текстом, когда данные пришли грязными — с лишними пробелами, случайным регистром, склеенными подстроками.
Аксессор .str: механизм поэлементной работы со строками
Когда в таблице есть текстовый столбец, первый инстинкт — пройтись по нему циклом и применить нужную строковую функцию к каждой ячейке. Это работает, но требует явного кода и не даёт pandas возможности обрабатывать данные в своём стиле. pandas предлагает стандартный способ — аксессор .str.
Аксессор — это особый «мост» между Series и строковыми методами Python. Обращаясь к series.str, вы получаете объект, который применяет строковые операции ко всем элементам Series поэлементно, без явного цикла в вашем коде.
Механика простая: series.str.method(...) берёт каждый элемент Series, вызывает на нём метод и складывает результаты в новый Series той же длины и с тем же индексом.
import pandas as pd
s = pd.Series([' Alice ', 'BOB', 'carol'])
print(s.str.lower())
# 0 alice
# 1 bob
# 2 carol
# dtype: object
Это идиоматичный pandas-стиль: вместо for-цикла вы описываете операцию декларативно, и pandas выполняет её внутри себя. Для object-столбцов прирост скорости по сравнению с циклом не гарантирован — он зависит от конкретной операции и версии pandas. Ценность .str прежде всего в читаемости, единообразии и корректной обработке пропусков.
Какие столбцы поддерживают .str. Аксессор работает с Series, у которых dtype равен object или StringDtype. Большинство текстовых столбцов при загрузке через read_csv получают тип object — это стандартный случай. StringDtype (pd.StringDtype()) — более строгий nullable-вариант, который явно обозначает, что в столбце только строки или pd.NA.
Поведение при пропусках. Если элемент в Series равен NaN или pd.NA, аксессор не падает с ошибкой. Он просто пропускает такой элемент и возвращает на его месте тот же NaN/pd.NA.
s_with_na = pd.Series(['Alice', None, 'carol'])
print(s_with_na.str.lower())
# 0 alice
# 1 None
# 2 carol
# dtype: object
Пропуски не превращаются в строку 'none' или пустую строку — они остаются пропусками. Если дальше нужна булева маска, это поведение стоит контролировать отдельно — об этом позже.
Исходный Series не изменяется. Каждый вызов .str.method() возвращает новый Series. Чтобы сохранить результат, нужно явно присвоить его столбцу или переменной.
Аксессор .str — стандартный инструмент для массовой работы с текстом в pandas. Всё, что разбирается в этом уроке — нормализация, разбивка, фильтрация — работает именно через него.
Нормализация текста: .str.lower, .str.strip, .str.replace
Самая частая причина, по которой одинаковые значения не совпадают при сравнении — это лишние пробелы и разный регистр. Строки 'Alice', ' alice ' и 'ALICE' для Python разные, хотя по смыслу одно и то же. Три метода закрывают этот класс проблем.
.str.strip() убирает пробельные символы с обоих концов строки — пробелы, табуляции, переносы строк. Если нужно чистить только одну сторону, есть .str.lstrip() (левый конец) и .str.rstrip() (правый).
.str.lower() приводит все символы к нижнему регистру. Для верхнего — .str.upper(), для заглавной первой буквы — .str.capitalize().
.str.replace(pat, repl) заменяет вхождения pat на repl. В pandas 2.x параметр regex=False по умолчанию, то есть pat трактуется буквально.
На практике эти три метода почти всегда идут цепочкой:
import pandas as pd
df = pd.DataFrame({
'name': [' Alice Smith ', ' BOB jones', 'carol BROWN ']
})
df['name_clean'] = df['name'].str.strip().str.lower()
print(df['name_clean'].tolist())
# ['alice smith', 'bob jones', 'carol brown']
Сначала снимаем пробелы по краям, потом приводим к нижнему регистру. Порядок здесь не критичен для этих двух методов, но привычка ставить strip первым полезна: тогда последующие операции работают с уже чистым текстом.
Для замены, например, дефисов на подчёркивания:
df['name_underscored'] = df['name_clean'].str.replace(' ', '_', regex=False)
print(df['name_underscored'].tolist())
# ['alice_smith', 'bob_jones', 'carol_brown']
Ловушка с regex в .str.replace и .str.contains. Здесь легко ошибиться, потому что у двух методов разные дефолты.
В .str.replace — regex=False по умолчанию. Это значит, что строка '.' будет искаться буквально как точка, а не как «любой символ» из регулярных выражений. Если вы хотите использовать паттерн вроде r'\s+', нужно явно написать regex=True.
В .str.contains — наоборот, regex=True по умолчанию. Паттерн '.' там найдёт что угодно, потому что точка в регулярных выражениях означает любой символ.
Антипример:
s = pd.Series(['3.14', '314', 'abc'])
# Хотим найти строки с буквальной точкой:
print(s.str.contains('.')) # regex=True по умолчанию — найдёт всё!
# 0 True
# 1 True
# 2 True
print(s.str.contains('.', regex=False)) # правильно — только '3.14'
# 0 True
# 1 False
# 2 False
Правило простое: для буквального поиска в .str.contains всегда передавайте regex=False. Для .str.replace с простой подстрокой regex=False уже стоит по умолчанию и менять его не нужно.
Каждый вызов возвращает новый Series — исходный столбец не затрагивается. Если результат нужен в таблице, присваивайте явно: df['col'] = df['col'].str.strip().
Разбивка строк через .str.split и извлечение подстрок
Нормализация приводит строки к единому виду, но иногда нужно пойти дальше — разрезать строку на части и положить каждую в отдельный столбец. Именно это делает .str.split.
Базовый вызов принимает строку-разделитель и возвращает Series, где каждый элемент — список подстрок:
import pandas as pd
s = pd.Series(['Иван Петров', 'Анна Смирнова', 'Дмитрий Орлов'])
print(s.str.split(' '))
# 0 [Иван, Петров]
# 1 [Анна, Смирнова]
# 2 [Дмитрий, Орлов]
# dtype: object
Списки внутри Series неудобны для дальнейшей работы. Чтобы сразу получить нормальный DataFrame с отдельными столбцами, передаём expand=True:
split_df = s.str.split(' ', expand=True)
print(split_df)
# 0 1
# 0 Иван Петров
# 1 Анна Смирнова
# 2 Дмитрий Орлов
Столбцы получают целочисленные имена — 0, 1, 2 и так далее. Их сразу можно присвоить обратно в таблицу:
df = pd.DataFrame({'full_name': ['Иван Петров', 'Анна Смирнова', 'Дмитрий Орлов']})
split_df = df['full_name'].str.split(' ', expand=True)
df['first'] = split_df[0]
df['last'] = split_df[1]
print(df)
# full_name first last
# 0 Иван Петров Иван Петров
# 1 Анна Смирнова Анна Смирнова
# 2 Дмитрий Орлов Дмитрий Орлов
Параметр n ограничивает максимальное число разбиений. Это полезно, когда разделитель может встречаться несколько раз, а нужно отрезать только первую часть:
s2 = pd.Series(['2024-01-15', '2023-12-03', '2024-07-22'])
split2 = s2.str.split('-', n=1, expand=True)
print(split2)
# 0 1
# 0 2024 01-15
# 1 2023 12-03
# 2 2024 07-22
С n=1 строка разбивается ровно один раз: до первого дефиса и всё остальное. Без n были бы три столбца.
Извлечение подстроки по позиции. Когда не нужно разбивать строку, а нужно вырезать конкретный диапазон символов, используют .str.slice(start, stop) или краткую форму — индексацию через .str[start:stop]:
s3 = pd.Series(['2024-01-15', '2023-12-03', '2024-07-22'])
print(s3.str.slice(0, 4)) # только год
# 0 2024
# 1 2023
# 2 2024
# dtype: object
print(s3.str[5:7]) # то же через краткую форму — месяц
# 0 01
# 1 12
# 2 07
# dtype: object
Обе формы эквивалентны. .str[i] без среза извлекает один символ по индексу — первый символ строки это .str[0].
Результат .str.split(expand=True) — полноценный DataFrame, с которым можно работать как с любой другой таблицей: переименовывать столбцы, фильтровать, склеивать обратно. Это основной способ структурировать «сжатые» данные, когда несколько значений хранятся в одной ячейке через разделитель.
Фильтрация строк: .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.
Задача 1
Series s = pd.Series(['Hello World', 'foo bar', 'PYTHON']). Что вернёт s.str.lower().tolist()?
Задача 2
DataFrame df содержит столбец 'code' со значениями ['RU-001', 'US-RU-042', 'RU-007', 'DE-100']. Какой вызов вернёт булев Series с True только для строк, начинающихся с 'RU'?
Задача 3
Каталог товаров: split, startswith и contains без циклов
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая обрабатывает каталог товаров:
- Читает из
stdinцелое числоn— количество строк каталога. - Читает следующие
nстрок; каждая содержит один токен — артикул товара в форматеКАТЕГОРИЯ-МОДЕЛЬ-НОМЕР, напримерPhone-Galaxy-12илиTablet-iPad-5. Токены не содержат пробелов внутри. - Создаёт
pd.Seriesс именемcodesиз прочитанных строк. - Разбивает
codesпо символу'-'через.str.split('-', expand=True)и сохраняет результат вparts. Это DataFrame с тремя столбцами0,1,2. - Создаёт
pd.Seriesс именемcategory— столбецparts[0], приведённый к нижнему регистру через.str.lower(). - Создаёт булев
pd.Seriesс именемis_phone—Trueтам, гдеcategoryначинается с'phone', через.str.startswith('phone'). - Создаёт булев
pd.Seriesс именемhas_digit—Trueтам, где исходныйcodesсодержит хотя бы одну цифру, через.str.contains(r'\d', regex=True). - Выводит в
stdoutровно четыре строки:
- Строка 1: значения
categoryчерез пробел. - Строка 2: значения
is_phoneчерез пробел (True/False). - Строка 3: значения
has_digitчерез пробел (True/False). - Строка 4: артикулы из
codes, для которыхis_phoneравноTrue, через пробел. Если таких нет — пустая строка.
Запрещено использовать Python-циклы для вычислений в шагах 4–7. Генераторные выражения разрешены только для форматирования строк вывода в шаге 8.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
Первая строка — целое число n (1 ≤ n ≤ 20).
Следующие n строк — артикулы вида КАТЕГОРИЯ-МОДЕЛЬ-НОМЕР без пробелов.
Формат вывода
Ровно четыре строки в порядке, описанном выше. Значения внутри строки разделяются одним пробелом.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 4
Нормализация адресов: strip, lower и replace без циклов
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая нормализует таблицу адресов:
- Читает из
stdinцелое числоn— количество строк. - Читает следующие
nстрок; каждая содержит один токен — «грязный» адрес (строка, которая может начинаться и заканчиваться пробелами, содержать буквы в произвольном регистре и дефисы вместо пробелов между словами). Токен считывается черезinput()— пробелы внутри токена сохраняются. - Создаёт
pd.Seriesс именемrawиз прочитанных строк. - Создаёт
pd.Seriesс именемstep1— результат.str.strip()наraw. - Создаёт
pd.Seriesс именемstep2— результат.str.lower()наstep1. - Создаёт
pd.Seriesс именемstep3— результат.str.replace('-', ' ', regex=False)наstep2. - Выводит в
stdoutровно три строки:
- Строка 1: значения
step1через|(пробел-вертикальная черта-пробел). - Строка 2: значения
step2через|. - Строка 3: значения
step3через|.
Запрещено использовать Python-циклы для шагов 4–7.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
Первая строка — целое число n (1 ≤ n ≤ 10).
Следующие n строк — адреса; каждый адрес — одна строка, возможно с пробелами по краям и дефисами внутри.
Формат вывода
Ровно три строки. Значения внутри строки разделяются |.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 5
Найди и исправь: три ошибки в .str.replace, .str.contains и .str.split
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.
В редакторе уже есть сломанный код. В нём ровно три ошибки в трёх независимых строках:
slugвычисляется через.str.replace('-', '_', regex=False)— заменяется дефис, хотя по условию нужно заменить пробел на подчёркивание через.str.replace(' ', '_', regex=False).mask_digitстроится через.str.contains(r'\d', regex=False)— из-заregex=Falseпаттерн\dищется буквально как строка\d, а не как цифра. Нужно убратьregex=Falseили явно написатьregex=True.partsполучается через.str.split('-', expand=False)— возвращается Series из списков вместо DataFrame. Нужноexpand=True.
Каждая ошибка — в отдельной строке. Исправьте все три.
Что нужно сделать
Программа должна:
- Читать из
stdinцелое числоn— количество строк. - Читать следующие
nстрок; каждая содержит одну строку видаСЛОВО МЕТКА-СУФФИКС, напримерhello 3-proилиworld x-lite. Токен читается черезinput()— пробел внутри токена сохраняется. - Создавать
pd.Seriesс именемrawиз прочитанных строк. - Создавать
pd.Seriesс именемslug— результат.str.replace(' ', '_', regex=False)наraw(заменить пробел на подчёркивание). - Создавать булев
pd.Seriesс именемmask_digit—Trueтам, гдеrawсодержит хотя бы одну цифру, через.str.contains(r'\d', regex=True). - Создавать
pd.DataFrameс именемpartsчерез.str.split('-', expand=True)наslug— столбцы0и1. - Выводить в
stdoutровно три строки:
- Строка 1: значения
slugчерез пробел. - Строка 2: значения
mask_digitчерез пробел (True/False). - Строка 3: значения столбца
parts[1]через пробел.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
Первая строка — целое число n (1 ≤ n ≤ 10).
Следующие n строк — строки вида СЛОВО МЕТКА-СУФФИКС, например hello 3-pro или world x-lite.
Формат вывода
Ровно три строки в порядке, описанном выше. Значения внутри строки разделяются одним пробелом.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 6
Журнал регистраций: домен через .str.split и фильтрация по домену через .str.contains
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая анализирует журнал регистраций пользователей:
- Читает из
stdinцелое числоn— количество строк журнала. - Читает следующие
nстрок; каждая содержит адрес электронной почты в форматеимя@домен, напримерalice@gmail.comилиBOB@MAIL.RU. Вокруг адреса могут быть пробелы, но внутри самого адреса пробелов нет. - Создаёт
pd.Seriesс именемemailsиз прочитанных строк. - Создаёт
pd.Seriesс именемemails_clean— результат.str.strip().str.lower()наemails. - Разбивает
emails_cleanпо символу'@'через.str.split('@', expand=True)и сохраняет результат вparts(DataFrame с двумя столбцами0и1). - Создаёт
pd.Seriesс именемdomain— столбецparts[1](доменная часть адреса). - Создаёт булев
pd.Seriesс именемis_gmail—Trueтам, гдеdomainсодержит подстроку'gmail', через.str.contains('gmail', regex=False). - Создаёт булев
pd.Seriesс именемis_ru—Trueтам, гдеdomainоканчивается на'.ru', через.str.endswith('.ru'). - Выводит в
stdoutровно четыре строки:
- Строка 1: значения
emails_cleanчерез пробел. - Строка 2: значения
domainчерез пробел. - Строка 3: значения
is_gmailчерез пробел (True/False). - Строка 4: адреса из
emails_clean, для которыхis_ruравноTrue, через пробел. Если таких нет — пустая строка.
Запрещено использовать Python-циклы для шагов 4–9.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
Первая строка — целое число n (1 ≤ n ≤ 15).
Следующие n строк — адреса электронной почты; вокруг адреса допустимы пробелы; после .str.strip() внутри адреса пробелов нет.
Формат вывода
Ровно четыре строки в порядке, описанном выше. Значения внутри строки разделяются одним пробелом.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задачи к уроку
- Текстовые столбцы: нормализация и разбор через .str — задача по NumPy и pandas
Какой вызов вернёт булев Series с True только для строк, начинающихся с 'RU'? Проверьте ответ в задаче из бесплатного курса NumPy и pandas.
- pandas str.strip, lower и replace: очистка строк без циклов
Уберите пробелы по краям, приведите строки к нижнему регистру и замените символы в pandas. Рабочий пример и задача с проверкой.
- pandas str.split и str.contains: фильтрация по домену email
Выделите домен адреса через .str.split и отфильтруйте записи по нему с .str.contains. Практическая задача.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
