Урок курса

Временные столбцы: парсинг, .dt-компоненты, длительности и фильтрация

NumPy и pandas: практический тренажёр

Как преобразовать строку в дату pandas

pd.to_datetime преобразует строки в даты. Параметр format задаёт ожидаемый формат, а errors="coerce" заменяет неверные значения на NaT. После преобразования свойства .dt.year и .dt.month доступны у Series.

import pandas as pd
s = pd.Series(["2026-01-15", "не дата", "2026-03-02"])
dates = pd.to_datetime(s, format="%Y-%m-%d", errors="coerce")
print(dates.isna().tolist())
print(dates.dropna().dt.year.tolist())
print(dates.dropna().dt.month.tolist())

Результат:

[False, True, False]
[2026, 2026]
[1, 3]

Ошибка «Can only use .dt accessor with datetimelike values» означает, что Series ещё не имеет подходящего типа дат. Проверьте s.dtype и результат преобразования. После coerce отдельно посчитайте NaT, чтобы ошибочные даты не потерялись незаметно.

В прошлом уроке мы разбирали .str-аксессор — инструмент для поэлементной работы с текстовыми столбцами. Datetime-столбцы устроены по той же логике: сначала нужно привести данные к нужному типу, а затем работать через специальный аксессор. Начнём с первого шага — парсинга строк в datetime.

Парсинг строкового столбца в datetime через pd.to_datetime

Когда вы читаете CSV или получаете данные из базы, даты почти всегда приходят в виде строк: '2024-03-15', '2024-03-15 14:30:00' и тому подобное. Pandas не знает, что это дата — для него это просто object-столбец. Пока тип остаётся object, никакая временна́я арифметика и никакие .dt-компоненты не работают.

Есть два момента, когда обычно нужно явно преобразовывать тип. Первый — когда столбец уже загружен в DataFrame как строки (например, CSV прочитан без дополнительных параметров). В этом случае используют pd.to_datetime:

import pandas as pd

df = pd.DataFrame({
    'event': ['A', 'B', 'C', 'D'],
    'start_str': ['2024-01-10 08:00', '2024-02-15 09:30', '2024-03-05 14:00', '2024-04-20 07:00'],
    'end_str':   ['2024-01-10 10:30', '2024-02-16 09:30', '2024-03-05 18:00', '2024-04-21 07:00']
})

print(df['start_str'].dtype)  # object

df['start'] = pd.to_datetime(df['start_str'])
df['end']   = pd.to_datetime(df['end_str'])

print(df['start'].dtype)  # datetime64[ns]

pd.to_datetime принимает Series строк и возвращает новый Series с dtype datetime64[ns]. Исходный строковый столбец start_str при этом не трогается — результат кладётся в новый столбец start.

Второй момент — чтение CSV. Если вы знаете заранее, что столбец содержит даты, можно попросить pandas распарсить его прямо при загрузке через parse_dates:

# При чтении CSV: pandas сразу создаст datetime64[ns]-столбец
df = pd.read_csv('events.csv', parse_dates=['start', 'end'])

В этом случае pd.to_datetime не нужен — столбец уже будет нужного типа. Но если CSV уже загружен без parse_dates, или данные пришли не из файла, а из другого источника — pd.to_datetime это стандартный инструмент для преобразования.

Формат строки pandas распознаёт автоматически для распространённых вариантов: 'YYYY-MM-DD', 'YYYY-MM-DD HH:MM:SS', 'YYYY-MM-DD HH:MM' — всё это проходит без дополнительных настроек.

Типичная ошибка: .dt на строковом столбце

Если обратиться к .dt на столбце с dtype object, pandas бросит ошибку:

# Неверно: start_str имеет dtype object
print(df['start_str'].dt.year)
# AttributeError: Can only use .dt accessor with datetimelike values

Диагноз простой: проверьте df['start_str'].dtype. Если видите object — перед вами строки, и нужен pd.to_datetime. После преобразования df['start'].dtype покажет datetime64[ns], и аксессор заработает.

# Верно: сначала парсим, потом используем .dt
df['start'] = pd.to_datetime(df['start_str'])
print(df['start'].dt.year.tolist())  # [2024, 2024, 2024, 2024]

Правило: прежде чем использовать .dt или временну́ю арифметику, убедитесь, что столбец имеет dtype datetime64[ns] — через pd.to_datetime или parse_dates при чтении файла.

Извлечение компонентов даты и времени через .dt-аксессор

Как только столбец стал datetime64[ns], он открывает аксессор .dt — способ добраться до отдельных частей даты и времени. Работает это так же, как .str для строковых столбцов: пишете df['col'].dt.что-то и получаете новый Series той же длины.

Основные компоненты:

  • .dt.year — год (например, 2024)
  • .dt.month — номер месяца от 1 до 12
  • .dt.day — день месяца
  • .dt.dayofweek — день недели от 0 (понедельник) до 6 (воскресенье)
  • .dt.hour — час из временно́й части

Возьмём столбец start из нашего датафрейма с событиями:

import pandas as pd

df = pd.DataFrame({
    'event': ['A', 'B', 'C', 'D'],
    'start_str': ['2024-01-10 08:00', '2024-02-15 09:30', '2024-03-05 14:00', '2024-04-20 07:00'],
    'end_str':   ['2024-01-10 10:30', '2024-02-16 09:30', '2024-03-05 18:00', '2024-04-21 07:00']
})

df['start'] = pd.to_datetime(df['start_str'])

print(df['start'].dt.year.tolist())      # [2024, 2024, 2024, 2024]
print(df['start'].dt.month.tolist())     # [1, 2, 3, 4]
print(df['start'].dt.day.tolist())       # [10, 15, 5, 20]
print(df['start'].dt.dayofweek.tolist()) # [2, 3, 1, 5]
print(df['start'].dt.hour.tolist())      # [8, 9, 14, 7]

Здесь dayofweek=2 означает среду (событие A, 10 января 2024), а dayofweek=5 — субботу (событие D, 20 апреля 2024). Это полезно, когда нужно выделить, например, только рабочие дни: df[df['start'].dt.dayofweek < 5].

Каждый из этих вызовов возвращает обычный целочисленный Series, который можно сразу класть в новый столбец:

df['month'] = df['start'].dt.month
df['hour']  = df['start'].dt.hour
print(df[['event', 'month', 'hour']])
  event  month  hour
0     A      1     8
1     B      2     9
2     C      3    14
3     D      4     7

Так из одного datetime-столбца появляются числовые признаки, которые уже можно использовать в группировках, сравнениях и условиях.

Поведение при NaT. Если в столбце есть пропуски — они представлены как NaT (Not a Time). В этом случае .dt.year и другие компоненты вернут NaN на месте пропуска, а тип всего Series сменится с int64 на float64, потому что целые числа в pandas не умеют хранить NaN. Это не ошибка — просто нужно иметь в виду при дальнейшей работе с такими столбцами.

Самостоятельная практика. Закрепите работу с датами на архиве заказов: извлеките год, месяц и день недели, затем отберите нужный период. Решить задачу на даты и фильтрацию. Другие упражнения на таблицы, пропуски и группировки доступны в бесплатном тренажёре NumPy и pandas; после регистрации дальнейший прогресс сохраняется. Открыть бесплатный курс NumPy и pandas.

Вычисление длительности: Timedelta и перевод в числовые единицы

Когда у вас есть два datetime-столбца — момент начала и момент конца — разность между ними даёт не число, а специальный тип: timedelta64[ns]. Это логично: разница между двумя датами — это длительность, а не просто цифра.

df['start'] = pd.to_datetime(df['start_str'])
df['end']   = pd.to_datetime(df['end_str'])

df['duration'] = df['end'] - df['start']
print(df['duration'].dtype)  # timedelta64[ns]
print(df['duration'].tolist())
# [Timedelta('0 days 02:30:00'), Timedelta('1 days 00:00:00'),
#  Timedelta('0 days 04:00:00'), Timedelta('1 days 00:00:00')]

Timedelta-столбец уже умеет многое «из коробки»: его можно сравнивать (df['duration'] > pd.Timedelta(hours=3)), а стандартные агрегации — mean(), sum(), min(), max() — работают напрямую:

print(df['duration'].mean())  # 0 days 13:37:30
print(df['duration'].max())   # 1 days 00:00:00

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

Как получить числовое значение

Самый прямолинейный способ — .dt.total_seconds(). Он переводит длительность в полное количество секунд (тип float64):

df['duration_sec'] = df['duration'].dt.total_seconds()
print(df['duration_sec'].tolist())
# [9000.0, 86400.0, 14400.0, 86400.0]

Здесь 9000 секунд — это 2.5 часа события A, 86400 — ровно сутки события B.

Если нужны часы или дни, делят либо на число секунд в единице, либо на pd.Timedelta:

# Часы через деление total_seconds
df['hours'] = df['duration'].dt.total_seconds() / 3600
print(df['hours'].tolist())  # [2.5, 24.0, 4.0, 24.0]

# Дробные дни через pd.Timedelta
df['days'] = df['duration'] / pd.Timedelta(days=1)
print(df['days'].tolist())   # [0.10416..., 1.0, 0.16666..., 1.0]

Оба подхода дают float64. Деление на pd.Timedelta(hours=1) — то же самое, что total_seconds() / 3600, просто читается чуть понятнее в коде.

Почему не .dt.days

У Timedelta-столбца есть .dt.days, но он возвращает нормализованный компонент дней из внутреннего представления длительности — без учёта часов и минут. Для события A (02:30:00) .dt.days вернёт 0 — часы потеряны:

print(df['duration'].dt.days.tolist())  # [0, 1, 0, 1]
# Событие A: 0 — потеряли 2.5 часа

Для неотрицательных длительностей это ведёт себя как усечение к нулю дробной части суток. Но для отрицательных длительностей поведение другое: нормализация идёт в сторону уменьшения, поэтому, например, -2.5 часа дадут .dt.days == -1 (а не 0). По этой причине .dt.days лучше не использовать как универсальный способ перевода в дни — это источник неочевидных ошибок. Для точного перевода в любые единицы правильный путь — total_seconds() с последующим делением.

Фильтрация строк по временному интервалу через loc

Когда datetime-столбец готов, с ним можно строить булевы маски точно так же, как с числовыми столбцами: операторы >=, <=, >, < работают напрямую. Правая часть сравнения — строка в формате, который pandas понимает как дату, например '2024-02-01'.

import pandas as pd

df = pd.DataFrame({
    'event': ['A', 'B', 'C', 'D'],
    'start_str': ['2024-01-10 08:00', '2024-02-15 09:30', '2024-03-05 14:00', '2024-04-20 07:00'],
    'end_str':   ['2024-01-10 10:30', '2024-02-16 09:30', '2024-03-05 18:00', '2024-04-21 07:00']
})

df['start'] = pd.to_datetime(df['start_str'])
df['end']   = pd.to_datetime(df['end_str'])
df['duration'] = df['end'] - df['start']
df['hours'] = df['duration'].dt.total_seconds() / 3600

# Отбираем события, начавшиеся в феврале и марте 2024
mask = (df['start'] >= '2024-02-01') & (df['start'] < '2024-04-01')
result = df.loc[mask, ['event', 'start', 'hours']]
print(result)
  event               start  hours
1     B 2024-02-15 09:30:00   24.0
2     C 2024-03-05 14:00:00    4.0

События A и D не попали в выборку: A началось в январе, D — в апреле. Остались только B и C.

Про скобки. Каждое подусловие нужно оборачивать в скобки. Без них Python разбирает выражение не так, как вы ожидаете, и получается либо TypeError, либо неверный результат:

# Неверно — операторы сравнения и & имеют разные приоритеты
mask = df['start'] >= '2024-02-01' & df['start'] < '2024-04-01'

# Верно
mask = (df['start'] >= '2024-02-01') & (df['start'] < '2024-04-01')

Правило: если объединяете несколько условий через & или |, скобки обязательны вокруг каждого.

Вместо строки можно передать pd.Timestamp. Это тот же результат, просто более явный способ:

mask = (df['start'] >= pd.Timestamp('2024-02-01')) & (df['start'] < pd.Timestamp('2024-04-01'))

Поведение идентично — pandas всё равно приводит строки к Timestamp внутри сравнения. Разница чисто стилистическая.

Полный пайплайн. Всё, что разбиралось в этом уроке, ложится в одну последовательность:

  1. pd.to_datetime превращает строки в datetime64[ns].
  2. .dt.year, .dt.month, .dt.dayofweek, .dt.hour — числовые признаки из даты.
  3. Вычитание двух datetime-столбцов даёт timedelta64[ns]; total_seconds() или деление на pd.Timedelta переводит в числа.
  4. Булева маска с >= и < на datetime-столбце + df.loc[mask] отбирает нужный временной интервал.

Каждый шаг независим: можно извлечь .dt.month без вычисления длительности, или отфильтровать строки без создания новых компонентных столбцов. Но вместе они покрывают большинство задач, которые возникают при работе с временными данными.

Эти же числовые столбцы — month, dayofweek, hour — станут естественными ключами группировки, когда дойдёте до groupby.

Задача 1

Series s = pd.Series(['2024-03-15 10:00', '2024-06-01 22:30']). После выполнения s = pd.to_datetime(s) что вернёт s.dt.dayofweek.tolist()?

Подсказка: 2024-03-15 — пятница, 2024-06-01 — суббота.

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

DataFrame df имеет столбец 'created' dtype datetime64[ns]. Нужно отфильтровать строки за март 2024 года (с 1 марта включительно по 31 марта включительно). Какой код корректно строит маску?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Журнал сессий: парсинг, .dt-компоненты, длительность в минутах и фильтрация по диапазону

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая обрабатывает журнал пользовательских сессий:

  1. Читает из stdin целое число n — количество строк журнала.
  2. Читает следующие n строк; каждая содержит пять токенов через пробел: session_id, дату входа, время входа, дату выхода и время выхода. Пример строки: 1 2024-01-15 08:00 2024-01-15 09:30.

Внимание: каждая строка содержит ровно пять токенов: session_id, дату входа, время входа, дату выхода и время выхода. Читайте строку целиком через input() и разбивайте по пробелам: токены [1] и [2] объедините через пробел — это login; токены [3] и [4] объедините через пробел — это logout.

  1. Создаёт pd.DataFrame с именем df со столбцами 'session_id' (int), 'login' (str), 'logout' (str).
  2. Преобразует столбцы 'login' и 'logout' в datetime64[ns] через pd.to_datetime.
  3. Создаёт столбец 'login_hour' через df['login'].dt.hour и столбец 'login_weekday' через df['login'].dt.dayofweek.
  4. Вычисляет столбец 'duration_min' — длительность сессии в минутах как (df['logout'] - df['login']).dt.total_seconds() / 60.
  5. Читает из stdin строку с двумя токенами через пробел: date_from и date_to — границы временного интервала (строки вида YYYY-MM-DD).
  6. Фильтрует строки, где login >= date_from и login < date_to, через df.loc[mask]. Сохраняет результат в filtered.
  7. Выводит в stdout ровно len(filtered) + 2 строк:
  • Строки 1…len(filtered): для каждой строки filtered (в порядке исходного DataFrame) одну строку вида <session_id> <login_hour> <login_weekday> <duration_min>, где duration_min — целое число (используйте int(...)).
  • Предпоследняя строка: count=<len(filtered)>.
  • Последняя строка: max_duration=<значение>, где значение — максимальная duration_min среди отфильтрованных строк, целое число; если filtered пуст — выведите max_duration=0.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

n
session_id1 YYYY-MM-DD HH:MM YYYY-MM-DD HH:MM
...
date_from date_to

Дополнительные ограничения: n >= 1; таблица содержит явно описанные в условии столбцы.

Формат вывода

<session_id> <login_hour> <login_weekday> <duration_min>
...
count=<k>
max_duration=<m>
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 4

Найди и исправь: три ошибки в .dt-компонентах и переводе Timedelta в дни

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.

В редакторе уже есть сломанный код. В нём ровно три ошибки в трёх независимых строках:

  1. Столбец 'month' вычисляется через df['start'].dt.month — но перед этим df['start'] не преобразован в datetime64[ns], он остаётся строковым (object). Нужно добавить pd.to_datetime до обращения к .dt.
  2. День недели извлекается через df['start'].dt.day, хотя нужен день недели — используйте dt.dayofweek.
  3. Длительность в днях вычисляется через df['duration'].dt.days, хотя этот атрибут возвращает только целую часть суток (часы теряются). Нужно использовать .dt.total_seconds() / 86400.

Каждая ошибка находится в отдельной строке. Исправьте все три.

Что нужно сделать

Исправьте три ошибки так, чтобы программа:

  1. Читала из stdin целое число n — количество строк.
  2. Читала следующие n строк; каждая содержит пять токенов через пробел: id, дату начала, время начала, дату конца и время конца. Формат: id YYYY-MM-DD HH:MM YYYY-MM-DD HH:MM — итого 5 токенов; токены [1]+[2] образуют start, токены [3]+[4] — end.
  3. Строила pd.DataFrame со столбцами 'id', 'start' (str), 'end' (str).
  4. Преобразовывала 'start' и 'end' в datetime64[ns] через pd.to_datetime.
  5. Создавала столбец 'month' через df['start'].dt.month.
  6. Создавала столбец 'weekday' через df['start'].dt.dayofweek.
  7. Вычисляла столбец 'duration' = df['end'] - df['start'] и столбец 'days' = df['duration'].dt.total_seconds() / 86400.
  8. Для каждой строки выводила: <id> <month> <weekday> <days>, где days — с округлением до 4 знаков.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

n
id YYYY-MM-DD HH:MM YYYY-MM-DD HH:MM
...

Дополнительные ограничения: n >= 1; таблица содержит явно описанные в условии столбцы.

Формат вывода

<id> <month> <weekday> <days>
...
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Архив заказов: год, месяц, день недели и фильтрация по месяцу

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая обрабатывает архив заказов:

  1. Читает из stdin целое число n — количество строк архива.
  2. Читает следующие n строк; каждая содержит два токена через пробел: order_id (целое число) и order_date (строка вида YYYY-MM-DD). Пример строки: 101 2024-03-15.
  3. Создаёт pd.DataFrame с именем df со столбцами 'order_id' (int) и 'order_date' (str).
  4. Преобразует столбец 'order_date' в datetime64[ns] через pd.to_datetime. Сохраняет обратно в df['order_date'].
  5. Создаёт столбец 'year' через df['order_date'].dt.year, столбец 'month' через df['order_date'].dt.month, столбец 'weekday' через df['order_date'].dt.dayofweek.
  6. Читает из stdin целое число target_month — номер месяца для фильтрации (от 1 до 12).
  7. Фильтрует строки, где month == target_month, через df.loc[mask]. Сохраняет в filtered.
  8. Выводит в stdout ровно len(filtered) + 2 строк:
  • Строки 1…len(filtered): для каждой строки filtered (в порядке исходного DataFrame) одну строку вида <order_id> <year> <month> <weekday>.
  • Предпоследняя строка: count=<len(filtered)>.
  • Последняя строка: weekdays=<список>, где список — значения weekday отфильтрованных строк через запятую и пробел, в порядке исходного DataFrame. Если filtered пуст — выведите weekdays=.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

n
order_id YYYY-MM-DD
...
target_month

Дополнительные ограничения: n >= 1; таблица содержит явно описанные в условии столбцы.

Формат вывода

<order_id> <year> <month> <weekday>
...
count=<k>
weekdays=<w1>, <w2>, ...
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 6

Бронирование переговорных: длительность в часах и фильтрация по дате

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая обрабатывает журнал бронирования переговорных комнат:

  1. Читает из stdin целое число n — количество записей.
  2. Читает следующие n строк. Каждая строка содержит пять токенов через пробел:
  • booking_id (целое число)
  • дата начала YYYY-MM-DD
  • время начала HH:MM
  • дата конца YYYY-MM-DD
  • время конца HH:MM

Объедините токены [1] и [2] через пробел — это строка start; токены [3] и [4] — строка end.

  1. Создаёт pd.DataFrame с именем df со столбцами 'booking_id' (int), 'start' (str), 'end' (str).
  2. Преобразует оба строковых столбца в datetime64[ns] через pd.to_datetime.
  3. Создаёт столбец 'start_day' через df['start'].dt.day, столбец 'start_hour' через df['start'].dt.hour.
  4. Вычисляет столбец 'duration_h' — длительность бронирования в часах как (df['end'] - df['start']).dt.total_seconds() / 3600. Значения могут быть дробными.
  5. Читает из stdin строку с двумя токенами через пробел: date_from и date_to — границы фильтрации (строки вида YYYY-MM-DD).
  6. Фильтрует строки, где start >= date_from и start < date_to, через df.loc[mask]. Сохраняет результат в filtered.
  7. Выводит в stdout ровно len(filtered) + 2 строк:
  • Строки 1…len(filtered): для каждой строки filtered (в порядке исходного DataFrame) одну строку вида <booking_id> <start_day> <start_hour> <duration_h>, где duration_h округлён до 2 знаков после запятой.
  • Предпоследняя строка: count=<len(filtered)>.
  • Последняя строка: total_hours=<значение> — сумма duration_h по отфильтрованным строкам, округлённая до 2 знаков; если filtered пуст — выведите total_hours=0.00.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

n
booking_id YYYY-MM-DD HH:MM YYYY-MM-DD HH:MM
...
date_from date_to

Дополнительные ограничения: n >= 1; таблица содержит явно описанные в условии столбцы.

Формат вывода

<booking_id> <start_day> <start_hour> <duration_h>
...
count=<k>
total_hours=<s>
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку