Условие
Дан исходный реестр медицинских услуг с «вертикальной» структурой: ФИО+полис в одной строке, ниже — даты приёмов, врачи, под датами — услуги. Нужно превратить в плоскую таблицу:
ФИО | НомерПолиса | ДатаОказанияУслуги | КодУслуги | Диагноз | НаименованиеУслуги | Кол-во | ЦенаПоПрейскуранту | СуммаСоСкидкой.
Решение
Подход
Структура иерархическая: верхний уровень — пациент (ФИО + полис), второй — дата + врач, третий — конкретные услуги. Используем forward-fill (ffill) для протяжки заголовков на строки услуг.
Реализация
import re
import pandas as pd
raw = pd.read_excel('Задание_1_реестр.xlsx', sheet_name='Исходные данные', header=None)
raw.columns = ['fio_polis','diagnosis','code','service','col1','col2','price','price_disc','qty','sum_disc']
# Регулярки
PAT_PATIENT = re.compile(r'^(?P<fio>[А-ЯЁ][\w\s]+)\s+Полис\s*№\s*(?P<polis>[\d/-]+)$')
# 1) Опознать строки заголовков
def parse_patient(row):
if pd.notna(row.fio_polis):
m = PAT_PATIENT.match(str(row.fio_polis).strip())
if m:
return m.group('fio').strip(), m.group('polis')
return pd.NA, pd.NA
raw[['fio','polis']] = raw.apply(parse_patient, axis=1, result_type='expand')
# 2) ffill пациента
raw[['fio','polis']] = raw[['fio','polis']].ffill()
# 3) Дата приёма + врач (в "diagnosis" со столбце или в service-столбце)
def parse_date(val):
try:
return pd.to_datetime(val, dayfirst=True, errors='raise')
except Exception:
return pd.NaT
raw['date_visit'] = raw.diagnosis.apply(parse_date)
raw['doctor'] = raw.service.where(raw.date_visit.notna())
raw[['date_visit','doctor']] = raw[['date_visit','doctor']].ffill()
# 4) Оставляем только строки с реальным кодом услуги
result = raw[raw.code.notna()].copy()
result = result.rename(columns={
'code':'service_code', 'service':'service_name'
})[['fio','polis','date_visit','service_code','diagnosis',
'service_name','qty','price','sum_disc']]
result.to_excel('result.xlsx', index=False)Альтернатива
Если столбцы фиксированы — можно итерировать по строкам и накапливать состояние:
state = {'fio': None, 'polis': None, 'date': None, 'doctor': None}
rows = []
for _, r in raw.iterrows():
if patient_match: state['fio'], state['polis'] = ...
elif is_date_row: state['date'], state['doctor'] = ...
elif is_service_row: rows.append({**state, ...service fields})Подводные камни
- ФИО без шаблона распознается плохо — нужен фолбэк (
Иванов Иван Ивановичвсегда 3 слова). - Даты в формате
13.07.2022—dayfirst=True, иначе спутает с американским форматом. - Пустые строки между секциями — нельзя
dropna(how='all')сразу, сначала получить иерархию через ffill. - На уровне «услуги» может быть несколько услуг подряд — ffill для пациента и даты сохраняется для всех.
- Полис содержит
-и/— не опознать как число.
Эталонный ответ
Парсинг иерархии: regex для пациента, попытка to_datetime для строк-дат → ffill обоих уровней → отфильтровать строки с заполненным service_code.