Как реализовать обработку датафрейма с помощью цепочки методов?

Рейтинг: 1Ответов: 1Опубликовано: 04.08.2023

У меня есть до неприличия кривой брокерский отчет. Ссылка на исходный файл

Я его считываю следующим образом:

df = (
    pd.read_excel(tinkoff_folder + tinkoff_files[0], skiprows=7)
    .dropna(axis=0, how='all')
    .dropna(axis=1, how='all')
)

Для обработки всего массива я его разделяю на 2 части:

  1. часть таблицы без смещения данных (фильтр по столбцу "Номер сделки" not .isna())
  2. часть таблицы со смещением (фильтр по столбцу "Номер сделки" .isna())

Так выглядит код для первой части таблицы (дата фрейм обрабатывается с помощью цепочки методов):

last_row = int(df[df.isin(['1.2 Информация о неисполненных сделках на конец отчетного периода']).any(axis=1)].index[0])
columns_lst = ['Номер сделки', 'Дата заключения', 'Время', 'Вид сделки', 'Сокращенное наименование', 'Цена за единицу', 'Количество', 'Сумма сделки', 'Валюта расчетов', 'Дата расчетов']

# Первая часть таблицы
df1 = (
    df.copy()
    [0: last_row]                                                       # оставляю только нужные разделы отчета
    [~df['Номер сделки'].isna()]                                        # убираю смещенные вправо данные
    .drop(df.filter(regex='Unnamed', axis=1), axis=1)                   # убираю столбцы без значений
    .dropna(axis=0, how='all')                                          # удаляем пустые стоки
    [
        df[df.columns[~df.columns.str.match('Unnamed')][7]]             # выбираем сделки с валютой
        .str.contains(match) == True
        ]
    .set_axis(                                                           # удаляем спец. символы и лишние пробелы в наименованиях стобцов
        [col.strip() for col
         in list(pd.Series(df.columns[~df.columns.str.match('Unnamed')]).replace('\n', '', regex=True))]
        , axis=1
        )
    [columns_lst]                                                        # оставляем нужные столбцы
)

В данном случае я уверен, что все операции с датафреймом будут проведены корректно. Но при обработке второй части возникают проблемы (мне приходится использовать мутации).

df2 = (
    df.copy()[0:last_row]                                   # оставляю только нужные разделы отчета
    [df['Номер сделки'].isna()][1:]                         # оставляю смещенные вправо данные
    .dropna(axis=1, how='all').dropna(axis=0, how='all')    # удаляем пустые стоки/столбцы
)

Результат следующий (прошу учесть, что в первой строке есть символы \n, поэтому первая строка разбивается на несколько).

Unnamed: 1 Unnamed: 3 Unnamed: 7 Unnamed: 9 Unnamed: 13 Unnamed: 18 Unnamed: 23 Unnamed: 30 Unnamed: 33 Unnamed: 39 Unnamed: 44 Unnamed: 46 Unnamed: 51 Unnamed: 54 Unnamed: 60 Unnamed: 65 Unnamed: 70 Unnamed: 76 Unnamed: 82 Unnamed: 87 Unnamed: 93 Unnamed: 97 Unnamed: 101 Unnamed: 107 Unnamed: 114 Unnamed: 120 Unnamed: 124 Unnamed: 131 Unnamed: 137 Unnamed: 141 Unnamed: 149 Unnamed: 154 Unnamed: 160 Unnamed: 165
14 Номер сделки Номер поруче Признак исполнения Дата заклю Время Торговая площадка Режим торгов Вид сделки Сокращен Код актива Цена за едини Валю Количество Сумма (без НКД) НКД Сумма сделки Валюта расчетов Комис Валю Комиссия биржи Валюта комиссии биржи Комиссия клир. центра Валюта комиссии клир. центра Гербовый сбор Валюта гербового сбора Ставка РЕПО(%) Контрагент / Брокер Дата расчетов Дата поставки Статус брокера Тип дог. Номер дог. Дата дог. Тип расчета по сделке
ния чения ное наименова цу та цены сия брокера та комис
ние сии
15 210002528 19110370310 nan 05.11.2019 10:01:12 ММВБ CNGD Покупка USDRUB_TOM USD000UTSTOM 63,37 RUB 940 59567,8 0 59567,8 RUB 178,7 RUB 0 nan 0 nan nan nan nan НКО НКЦ (АО) 06.11.2019 nan К nan nan nan CLR
16 210481436 19121563787 nan 08.11.2019 16:02:40 ММВБ CNGD Покупка USDRUB_TOM USD000UTSTOM 63,88 RUB 250 15970 0 15970 RUB 47,91 RUB 0 nan 0 nan nan nan nan НКО НКЦ (АО) 12.11.2019 nan К nan nan nan CLR
17 211812301 19150608966 nan 25.11.2019 16:38:58 ММВБ CNGD Покупка USDRUB_TOM USD000UTSTOM 63,9875 RUB 80 5119 0 5119 RUB 15,36 RUB 0 nan 0 nan nan nan nan НКО НКЦ (АО) 26.11.2019 nan К nan nan nan CLR
18 3053630966 18986970279 nan 10.12.2019 14:33:10 ММВБ TQTF Покупка FinEx USD Cash Equivalent FXTB 646,7 RUB 15 9700,5 0 9700,5 RUB 29,1 RUB 0.52 RUB 0.38 RUB nan nan nan НКО НКЦ (АО) 12.12.2019 12.12.2019 К nan nan nan CLR
19 213577271 19188947668 nan 13.12.2019 14:05:22 ММВБ CNGD Покупка USDRUB_TOM USD000UTSTOM 62,22 RUB 1 62,22 0 62,22 RUB 0,19 RUB 0 nan 0 nan nan nan nan НКО НКЦ (АО) 16.12.2019 nan К nan nan nan CLR
20 3058666741 19019713827 nan 18.12.2019 13:17:26 ММВБ TQBR Покупка ГАЗПРОМ ао GAZP 251,79 RUB 10 2517,9 0 2517,9 RUB 7,55 RUB 0.13 RUB 0.1 RUB nan nan nan НКО НКЦ (АО) 20.12.2019 20.12.2019 К nan nan nan CLR

В этом моменте начинаются проблемы. Мне нужно взять первую строку из полученного df2 и использовать ее в качестве новых заголовков для столбцов (предварительно удалив спец. символы и лишние пробелы).

Я могу это реализовать следующим образом:

col_list2 = list(df2.iloc[0].astype(str).replace('\n', '', regex=True))
col_list2_strip = [col.strip() for col in col_list2]

df2_1 = df2.copy().set_axis(col_list2_strip, axis=1)

Получаю результат:

Номер сделки Номер поручения Признак исполнения Дата заключения Время Торговая площадка Режим торгов Вид сделки Сокращенное наименование Код актива Цена за единицу Валюта цены Количество Сумма (без НКД) НКД Сумма сделки Валюта расчетов Комиссия брокера Валюта комиссии Комиссия биржи Валюта комиссии биржи Комиссия клир. центра Валюта комиссии клир. центра Гербовый сбор Валюта гербового сбора Ставка РЕПО(%) Контрагент / Брокер Дата расчетов Дата поставки Статус брокера Тип дог. Номер дог. Дата дог. Тип расчета по сделке
14 Номер сделки Номер поруче Признак исполнения Дата заклю Время Торговая площадка Режим торгов Вид сделки Сокращен Код актива Цена за едини Валю Количество Сумма (без НКД) НКД Сумма сделки Валюта расчетов Комис Валю Комиссия биржи Валюта комиссии биржи Комиссия клир. центра Валюта комиссии клир. центра Гербовый сбор Валюта гербового сбора Ставка РЕПО(%) Контрагент / Брокер Дата расчетов Дата поставки Статус брокера Тип дог. Номер дог. Дата дог. Тип расчета по сделке
ния чения ное наименова цу та цены сия брокера та комис
ние сии
15 210002528 19110370310 nan 05.11.2019 10:01:12 ММВБ CNGD Покупка USDRUB_TOM USD000UTSTOM 63,37 RUB 940 59567,8 0 59567,8 RUB 178,7 RUB 0 nan 0 nan nan nan nan НКО НКЦ (АО) 06.11.2019 nan К nan nan nan CLR
16 210481436 19121563787 nan 08.11.2019 16:02:40 ММВБ CNGD Покупка USDRUB_TOM USD000UTSTOM 63,88 RUB 250 15970 0 15970 RUB 47,91 RUB 0 nan 0 nan nan nan nan НКО НКЦ (АО) 12.11.2019 nan К nan nan nan CLR
17 211812301 19150608966 nan 25.11.2019 16:38:58 ММВБ CNGD Покупка USDRUB_TOM USD000UTSTOM 63,9875 RUB 80 5119 0 5119 RUB 15,36 RUB 0 nan 0 nan nan nan nan НКО НКЦ (АО) 26.11.2019 nan К nan nan nan CLR
18 3053630966 18986970279 nan 10.12.2019 14:33:10 ММВБ TQTF Покупка FinEx USD Cash Equivalent FXTB 646,7 RUB 15 9700,5 0 9700,5 RUB 29,1 RUB 0.52 RUB 0.38 RUB nan nan nan НКО НКЦ (АО) 12.12.2019 12.12.2019 К nan nan nan CLR
19 213577271 19188947668 nan 13.12.2019 14:05:22 ММВБ CNGD Покупка USDRUB_TOM USD000UTSTOM 62,22 RUB 1 62,22 0 62,22 RUB 0,19 RUB 0 nan 0 nan nan nan nan НКО НКЦ (АО) 16.12.2019 nan К nan nan nan CLR
20 3058666741 19019713827 nan 18.12.2019 13:17:26 ММВБ TQBR Покупка ГАЗПРОМ ао GAZP 251,79 RUB 10 2517,9 0 2517,9 RUB 7,55 RUB 0.13 RUB 0.1 RUB nan nan nan НКО НКЦ (АО) 20.12.2019 20.12.2019 К nan nan nan CLR

Далее я вынужен опять прибегать к мутации, так как на этот раз я буду обращаться к новым названиям столбцов:

columns_lst = ['Номер сделки', 'Дата заключения', 'Время', 'Вид сделки', 'Сокращенное наименование', 'Цена за единицу', 'Количество', 'Сумма сделки', 'Валюта расчетов', 'Дата расчетов']
df2_2 = (
    df2_1.copy()
    [columns_lst] # оставляем нужные столбцы
    [df2_1['Сокращенное наименование'].str.contains(match) == True] # выбираем сделки с валютой
)

И только после нескольких мутаций исходной таблицы я получаю корректную вторую часть таблицы:

Номер сделки Дата заключения Время Вид сделки Сокращенное наименование Цена за единицу Количество Сумма сделки Валюта расчетов Дата расчетов
15 210002528 05.11.2019 10:01:12 Покупка USDRUB_TOM 63,37 940 59567,8 RUB 06.11.2019
16 210481436 08.11.2019 16:02:40 Покупка USDRUB_TOM 63,88 250 15970 RUB 12.11.2019
17 211812301 25.11.2019 16:38:58 Покупка USDRUB_TOM 63,9875 80 5119 RUB 26.11.2019
19 213577271 13.12.2019 14:05:22 Покупка USDRUB_TOM 62,22 1 62,22 RUB 16.12.2019

Есть ли подход избежать данных мутаций или хотя бы быть уверенным, что последовательность преобразований массива будет выполнена корректно и избежать создания нескольких датафреймов?

Update: решил немного по другому подойти к решению проблемы. В первую очередь выделяю данные без смещения, схожим образом выделяю данные со смещением, привожу датафреймы в одному размеру и объединяю их.

last_row = int(df[df.isin(['1.2 Информация о неисполненных сделках на конец отчетного периода']).any(axis=1)].index[0])
columns_lst = ['Номер сделки', 'Дата заключения', 'Время', 'Вид сделки', 'Сокращенное наименование', 'Цена за единицу', 'Количество', 'Сумма сделки', 'Валюта расчетов', 'Дата расчетов']

dfs = []    # list для хранения датафреймов

# Первая часть таблицы
df1 = (
    df.copy()
    [0: last_row]                          # оставляю только нужные разделы отчета
    [~df['Номер сделки'].isna()]           # оставляю несмещенные вправо данные
    [df.columns[:-1]]                      # удаляю последний столбец (в нем нет данных)
    )

dfs.append(df1)

# Вторая часть таблицы
df2 = (
    df.copy()[0:last_row]                   # оставляю только нужные разделы отчета
    [df['Номер сделки'].isna()][1:]         # оставляю смещенные вправо данные
    [df.columns[1:]]                        # удаляю первый столбец (в нем нет данных)
    .set_axis(df1.columns, axis=1)
)

dfs.append(df2)

col_list = list(pd.Series(df1.columns).replace('\n', '', regex=True)) # удаляем спец символы 
col_list_strip = [col.strip() for col in col_list] # удаляем лишние пробелы

df_con = (
    pd.concat(dfs, ignore_index=True) # объединяем датафремы
    .set_axis(col_list_strip, axis=1) # переименовываем столбцы
    .dropna(axis=1, how='all').dropna(axis=0, how='all') # удаляем пустые столбцы/строки
)

# фильтруем полученный датафрейм
df_con_filtered = (
    df_con[df_con['Сокращенное наименование'].str.contains(match)][columns_lst]
)

Но по прежнему остается вопрос: как в данном случае применить цепочку методов?

Особенно это касается операции объединения датафреймов и фильтрации полученного датафрейма.

Ответы

▲ 0

В одну цепочку

columns_lst, match = ['Номер сделки', 'Дата заключения', 'Время', 'Вид сделки', 'Сокращенное наименование',
                      'Цена за единицу', 'Количество', 'Сумма сделки', 'Валюта расчетов', 'Дата расчетов'], 'USDRUB_TOM'
df = pd.read_excel(r'broker-report-2019-09-01-2019-12-31.xlsx', skiprows=7)

df = (pd.concat(
    [(df := df[:df[df.eq('1.2 Информация о неисполненных сделках на конец отчетного периода').any(axis=1)].index[0]])[
         df['Номер сделки'].notnull()],
     df[df['Номер сделки'].isna()].shift(-1, axis=1)  # сдвигаем данные в столбцах
         .iloc[2:, :]])  # удаляем второй заголовок внутри таблицы
      .rename(columns=lambda x: x.replace('\n', '').strip())  # чистим названия колонок
      .astype({'Номер сделки': 'int64'})  # приводим формат Номера сделки к целому числу
      .dropna(axis=0, how='all').dropna(axis=1, how='all')[columns_lst]  # удаляем пустые и ненужные строки/столбцы
      .assign(**{'Сокращенное наименование': lambda x: x['Сокращенное наименование'].where(
    x['Сокращенное наименование'].eq(match))}).dropna(
    subset='Сокращенное наименование'))  # фильтруем по Сокр. наименованию
print(df)
    Номер сделки Дата заключения     Время Вид сделки Сокращенное наименование Цена за единицу Количество Сумма сделки Валюта расчетов Дата расчетов
0      207044733      01.10.2019  13:24:59    Покупка               USDRUB_TOM         65,0625      100.0      6506,25             RUB    02.10.2019
1      207917839      10.10.2019  13:27:14    Покупка               USDRUB_TOM         64,7625      100.0      6476,25             RUB    11.10.2019
2      208111821      11.10.2019  18:46:35    Покупка               USDRUB_TOM         64,1175       50.0      3205,88             RUB    15.10.2019
3      208678396      21.10.2019  10:05:32    Покупка               USDRUB_TOM          63,835       25.0      1595,88             RUB    22.10.2019
4      208688386      21.10.2019  10:25:13    Покупка               USDRUB_TOM         63,7725       25.0      1594,31             RUB    22.10.2019
5      208703014      21.10.2019  11:40:00    Покупка               USDRUB_TOM          63,695       50.0      3184,75             RUB    22.10.2019
6      208714382      21.10.2019  13:15:44    Покупка               USDRUB_TOM         63,7175       54.0      3440,75             RUB    22.10.2019
7      209258652      25.10.2019  17:17:27    Покупка               USDRUB_TOM          63,825      100.0       6382,5             RUB    28.10.2019
8      209399355      28.10.2019  16:28:01    Покупка               USDRUB_TOM          63,595       70.0      4451,65             RUB    29.10.2019
9      209481193      29.10.2019  10:44:24    Покупка               USDRUB_TOM         63,7975       50.0      3189,88             RUB    30.10.2019
10     209496869      29.10.2019  12:44:40    Покупка               USDRUB_TOM          63,945      160.0      10231,2             RUB    30.10.2019
15     210002528      05.11.2019  10:01:12    Покупка               USDRUB_TOM           63,37        940      59567,8             RUB    06.11.2019
16     210481436      08.11.2019  16:02:40    Покупка               USDRUB_TOM           63,88        250        15970             RUB    12.11.2019
17     211812301      25.11.2019  16:38:58    Покупка               USDRUB_TOM         63,9875         80         5119             RUB    26.11.2019
19     213577271      13.12.2019  14:05:22    Покупка               USDRUB_TOM           62,22          1        62,22             RUB    16.12.2019

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