Python - для цикла миллионы строк

У меня есть dataframe c с множеством разных столбцов. Кроме того, arr - это кадр данных, который соответствует подмножеству c: arr = c[c['A_D'] == 'A'].

Основная идея моего кода - перебрать все строки в c -dataframe и выполнить поиск всех возможных случаев (в фрейме данных arr), где должны выполняться некоторые конкретные условия:

  • Нужно только итератировать по строкам: c['A_D'] == D и c['Already_linked'] == 0
  • hour в hour_aux arr должен быть меньше, чем hour_aux в c кадре данных
  • Столбец Already_linked из Already_linked данных arr должен быть равен нулю: arr.Already_linked == 0
  • Terminal и Operator должны быть одинаковыми в фрейме c и arr

В настоящее время условия сохраняются с использованием как булевых индексирования, так и groupby get_group:

  • Группируйте рамку данных arr, чтобы выбрать один и тот же оператор и терминал: g = groups.get_group((row.Operator, row.Terminal))
  • Выберите только те места, где час меньше часа в c кадре данных и где Already_linked == 0: vb = g[(g.Already_linked==0) & (g.hour<row.hour_aux)]

Для каждой из строк в c кадре данных, которые проверяют все условия, создается vb. Естественно, этот кадр данных имеет разную длину на каждой итерации. После создания фрейма vb моя цель - выбрать индекс vb.START vb который минимизирует время между vb.START и c [ x ]. Идентификатор FightID, соответствующий этому индексу, затем сохраняется в c кадре c в столбце a. Кроме того, поскольку приезд был связан с отъездом, столбец Already_linked в Already_linked arr изменяется с 0 на 1.

Важно заметить, что столбец Already_linked из Already_linked arr может меняться на каждой итерации (и arr.Already_linked == 0 является одним из условий для создания arr.Already_linked == 0 vb). Поэтому распараллеливать этот код невозможно.

Я уже использовал c.itertuples() для эффективности, но поскольку c имеет миллионы строк, этот код все еще слишком трудоемкий.

Другим вариантом также будет использование pd.apply для каждой строки. Тем не менее, это не так просто, поскольку в каждом цикле есть значения, изменяющиеся как в c и в arr (также, я считаю, что даже с pd.apply это было бы очень медленно).

Есть ли какой-либо возможный способ преобразования этого цикла в векторизованное решение (или уменьшить время работы на 10X (если возможно, даже больше))?

Исходный фрейм данных:

START     END       A_D     Operator     FlightID    Terminal   TROUND_ID   tot
0   2017-03-26 16:55:00 2017-10-28 16:55:00 A   QR  QR001   4   QR002       70
1   2017-03-26 09:30:00 2017-06-11 09:30:00 D   DL  DL001   3   "        "  84
2   2017-03-27 09:30:00 2017-10-28 09:30:00 D   DL  DL001   3   "        "  78
3   2017-10-08 15:15:00 2017-10-22 15:15:00 D   VS  VS001   3   "        "  45
4   2017-03-26 06:50:00 2017-06-11 06:50:00 A   DL  DL401   3   "        "  9
5   2017-03-27 06:50:00 2017-10-28 06:50:00 A   DL  DL401   3   "        "  19
6   2017-03-29 06:50:00 2017-04-19 06:50:00 A   DL  DL401   3   "        "  3
7   2017-05-03 06:50:00 2017-10-25 06:50:00 A   DL  DL401   3   "        "  32
8   2017-06-25 06:50:00 2017-10-22 06:50:00 A   DL  DL401   3   "        "  95
9   2017-03-26 07:45:00 2017-10-28 07:45:00 A   DL  DL402   3   "        "  58

Желаемый результат (некоторые столбцы были исключены в приведенной ниже таблице данных). Учитываются только столбцы a и Already_linked):

    START                    END             A_D  Operator  a   Already_linked
0   2017-03-26 16:55:00 2017-10-28 16:55:00 A   QR  0               1
1   2017-03-26 09:30:00 2017-06-11 09:30:00 D   DL  DL402           1
2   2017-03-27 09:30:00 2017-10-28 09:30:00 D   DL  DL401           1
3   2017-10-08 15:15:00 2017-10-22 15:15:00 D   VS  No_link_found   0
4   2017-03-26 06:50:00 2017-06-11 06:50:00 A   DL  0               0
5   2017-03-27 06:50:00 2017-10-28 06:50:00 A   DL  0               1
6   2017-03-29 06:50:00 2017-04-19 06:50:00 A   DL  0               0
7   2017-05-03 06:50:00 2017-10-25 06:50:00 A   DL  0               0
8   2017-06-25 06:50:00 2017-10-22 06:50:00 A   DL  0               0
9   2017-03-26 07:45:00 2017-10-28 07:45:00 A   DL  0               1

Код:

groups = arr.groupby(['Operator', 'Terminal'])
for row in c[(c.A_D == "D") & (c.Already_linked == 0)].itertuples():
    try:
        g = groups.get_group((row.Operator, row.Terminal))
        vb = g[(g.Already_linked==0) & (g.hour<row.hour_aux)]
        aux = (vb.START - row.x).abs().idxmin()
        c.loc[row.Index, 'a'] = vb.loc[aux].FlightID
        arr.loc[aux, 'Already_linked'] = 1
        continue
    except:
        continue

c['Already_linked'] = np.where((c.a != 0) & (c.a != 'No_link_found') & (c.A_D == 'D'), 1, c['Already_linked'])
c.Already_linked.loc[arr.Already_linked.index] = arr.Already_linked
c['a'] = np.where((c.Already_linked  == 0) & (c.A_D == 'D'),'No_link_found',c['a'])

Код для исходного c кадра:

import numpy as np
import pandas as pd
import io

s = '''
 A_D     Operator     FlightID    Terminal   TROUND_ID   tot
 A   QR  QR001   4   QR002       70
 D   DL  DL001   3   "        "  84
 D   DL  DL001   3   "        "  78
 D   VS  VS001   3   "        "  45
 A   DL  DL401   3   "        "  9
 A   DL  DL401   3   "        "  19
 A   DL  DL401   3   "        "  3
 A   DL  DL401   3   "        "  32
 A   DL  DL401   3   "        "  95
 A   DL  DL402   3   "        "  58
'''

data_aux = pd.read_table(io.StringIO(s), delim_whitespace=True)
data_aux.Terminal = data_aux.Terminal.astype(str)
data_aux.tot= data_aux.tot.astype(str)

d = {'START': ['2017-03-26 16:55:00', '2017-03-26 09:30:00','2017-03-27 09:30:00','2017-10-08 15:15:00',
           '2017-03-26 06:50:00','2017-03-27 06:50:00','2017-03-29 06:50:00','2017-05-03 06:50:00',
           '2017-06-25 06:50:00','2017-03-26 07:45:00'], 'END': ['2017-10-28 16:55:00' ,'2017-06-11 09:30:00' ,
           '2017-10-28 09:30:00' ,'2017-10-22 15:15:00','2017-06-11 06:50:00' ,'2017-10-28 06:50:00', 
           '2017-04-19 06:50:00' ,'2017-10-25 06:50:00','2017-10-22 06:50:00' ,'2017-10-28 07:45:00']}    

aux_df = pd.DataFrame(data=d)
aux_df.START = pd.to_datetime(aux_df.START)
aux_df.END = pd.to_datetime(aux_df.END)
c = pd.concat([aux_df, data_aux], axis = 1)
c['A_D'] = c['A_D'].astype(str)
c['Operator'] = c['Operator'].astype(str)
c['Terminal'] = c['Terminal'].astype(str)

c['hour'] = pd.to_datetime(c['START'], format='%H:%M').dt.time
c['hour_aux'] = pd.to_datetime(c['START'] - pd.Timedelta(15, unit='m'), 
format='%H:%M').dt.time
c['start_day'] = c['START'].astype(str).str[0:10]
c['end_day'] = c['END'].astype(str).str[0:10]
c['x'] = c.START -  pd.to_timedelta(c.tot.astype(int), unit='m')
c["a"] = 0
c["Already_linked"] = np.where(c.TROUND_ID != "        ", 1 ,0)

arr = c[c['A_D'] == 'A']

Ответы

Ответ 1

Это решение использует pd.DataFrame.isin, который использует numpy.in1d

По-видимому, "isin" не обязательно быстрее для небольших наборов данных (например, этот образец), но значительно быстрее для больших наборов данных. Вам нужно будет запустить его против ваших данных, чтобы определить производительность.

flight_record_linkage.ipynb

Развернул набор данных, используя c = pd.concat([c] * 10000, ignore_index=True)

  • Увеличьте длину набора данных на 3 порядка (всего 10000 строк).
    • Оригинальный метод: Настенное время: 8.98s
    • Новый метод: Настенное время: 16,4 с
  • Увеличьте длину набора данных на 4 порядка (всего 100000 строк).
    • Оригинальный метод: Настенное время: 8 мин. 17 сек.
    • Новый метод: Настенное время: 1 мин. 14 сек.
  • Увеличьте длину набора данных на 5 порядков (всего 1000000 строк).
    • Новый метод: Настенное время: 11мин 33s

Новый метод: использование isin и применение

def apply_do_g(it_row):
    """
    This is your function, but using isin and apply
    """

    keep = {'Operator': [it_row.Operator], 'Terminal': [it_row.Terminal]}  # dict for isin combined mask

    holder1 = arr[list(keep)].isin(keep).all(axis=1)  # create boolean mask
    holder2 = arr.Already_linked.isin([0])  # create boolean mask
    holder3 = arr.hour < it_row.hour_aux  # create boolean mask

    holder = holder1 & holder2 & holder3  # combine the masks

    holder = arr.loc[holder]

    if not holder.empty:

        aux = np.absolute(holder.START - it_row.x).idxmin()

        c.loc[it_row.name, 'a'] = holder.loc[aux].FlightID  # use with apply 'it_row.name'

        arr.loc[aux, 'Already_linked'] = 1


def new_way_2():
    keep = {'A_D': ['D'], 'Already_linked': [0]}
    df_test = c[c[list(keep)].isin(keep).all(axis=1)].copy()  # returns the resultant df
    df_test.apply(lambda row: apply_do_g(row), axis=1)  # g is multiple DataFrames"


#call the function
new_way_2()

Ответ 2

Ваш вопрос был, если есть способ векторизации цикла for, но я думаю, что этот вопрос скрывает то, что вы действительно хотите, что является простым способом ускорить ваш код. Для вопросов производительности хорошая отправная точка всегда профилируется. Однако у меня есть сильное подозрение, что доминирующей операцией в вашем коде является .query(row.query_string). Выполнение этого для каждой строки является дорогостоящим, если arr является большим.

Для произвольных запросов эта среда выполнения вообще не может быть улучшена без удаления зависимостей между итерациями и распараллеливания дорогостоящего шага. Возможно, вам будет немного повезло. Строка запроса всегда проверяет два разных столбца, чтобы узнать, соответствуют ли они тому, что вам нужно. Тем не менее, для каждой строки, требующей прохождения всего вашего фрагмента arr. Поскольку срез меняется каждый раз, это может вызвать проблемы, но вот некоторые идеи:

  • Поскольку вы каждый раз arr.Already_Linked==0 arr каждый раз, поддерживайте представление только arr.Already_Linked==0 чтобы вы arr.Already_Linked==0 итерацию над меньшим объектом.
  • Еще лучше, прежде чем делать какой-либо цикл, вы должны сначала сгруппировать arr Terminal и Operator. Затем вместо того, чтобы проходить через весь arr, сначала выберите нужную группу, а затем выполните нарезку и фильтрацию. Это потребовало бы немного переосмыслить точную реализацию query_string, но преимущество в том, что если у вас много терминалов и операторов, вы обычно будете работать над гораздо меньшим объектом, чем arr. Более того, вам даже не придется запрашивать этот объект, поскольку это было неявно сделано группой.
  • В зависимости от того, как aux.hour обычно относится к row.hour_aux, у вас могут быть улучшения путем сортировки aux в начале по отношению к hour. Просто используя оператор неравенства, вы, вероятно, не увидите никаких выигрышей, но вы можете связать это с логарифмическим поиском точки отсечки, а затем просто нарезать до этой точки отсечки.
  • И так далее. Опять же, я подозреваю, что любой способ реструктуризации запроса, который вы делаете во всех случаях arr для каждой строки, будет иметь значительно больший выигрыш, чем просто переключение фреймворков или векторизация битов и фрагментов.

Несколько расширив некоторые из этих точек и немного адаптировав код @DJK, посмотрите, что происходит, когда мы имеем следующие изменения.

groups = arr.groupby(['Operator', 'Terminal'])

for row in c[(c.A_D == 'D') & (c.Already_linked == 0)].itertuples():
    g = groups.get_group((row.Operator, row.Terminal))
    vb = g[(g.Already_linked==0) & (g.hour<row.hour_aux)]
    try:
        aux = (vb.START - row.x).abs().idxmin()
        print(row.x)
        c.loc[row.Index, 'a'] = vb.loc[aux,'FlightID']
        g.loc[aux, 'Already_linked'] = 1
        continue
    except:
        continue

Причина, по которой ваш запрос настолько медленный, заключается в том, что он каждый раз arr все сообщения. Напротив, .groupby() выполняется примерно в то же время, что и один запрос, но затем для каждой последующей итерации вы можете просто использовать .get_group() для эффективного поиска крошечного подмножества данных, о которых вы заботитесь.

Полезное (крайне грубое) правило, когда бенчмаркинг состоит в том, что миллиард вещей занимает второе место. Если вы видите гораздо больше времени, чем то, что измеряется миллионами вещей, например ваши миллионы строк, это означает, что для каждого из этих строк вы делаете массу вещей, чтобы получить до миллиардов операций. Это оставляет тонну потенциала для улучшения алгоритмов, чтобы уменьшить количество операций, тогда как векторизация действительно дает только постоянные улучшения факторов (и для многих операций с строками/запросами даже не большие улучшения).

Ответ 3

Несмотря на то, что это не решение, ориентированное на vecterized, оно должно быстро ускорить работу, если ваш набор данных образцов подражает вашему истинному набору данных. В настоящее время вы тратите время на все строки, но вам все равно, что они перебирают строки, где ['A_D'] == 'D' и ['Already_linked'] ==0. Вместо этого удалите if и loop по усеченному фреймворку данных, который составляет только 30% от исходного кадра данных

for row in c[(c.A_D == 'D') & (c.Already_linked == 0)].itertuples():

    vb = arr[(arr.Already_linked == 0) & (arr.hour < row.hour_aux)].copy().query(row.query_string)
    try:
        aux = (vb.START - row.x).abs().idxmin()
        print(row.x)
        c.loc[row.Index, 'a'] = vb.loc[aux,'FlightID']
        arr.loc[aux, 'Already_linked'] = 1
        continue
    except:
        continue

Ответ 4

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

У вас очень длинная table со столбцами time, FlightID, Operator, Terminal, A_D. Другие столбцы и даты не имеют значения, если я правильно вас понимаю. Также start_time и end_time одинаковы в каждой строке. Кстати, вы можете получить столбец time с кодом table.loc[:, 'time'] = table.loc[:, 'START'].dt.time.

  1. table = table.drop_duplicates(subset=['time', 'FlightID', 'Operator', 'Terminal']). И ваш table станет значительно короче.

  2. Разделить table на table_arr и table_dep соответствии со значением A_D: table_arr = table.loc[table.loc[:, 'A_D'] == 'A', ['FlightID', 'Operator', 'Terminal', 'time']], table_dep = table.loc[table.loc[:, 'A_D'] == 'D', ['FlightID', 'Operator', 'Terminal', 'time']]

  3. Кажется, что все, что вы пытались получить с помощью циклов, вы можете получить с одной строкой: table_result = table_arr.merge(table_dep, how='right', on=['Operator', 'Terminal'], suffixes=('_arr', '_dep')). Это в основном та же операция, что и JOIN в SQL.

Согласно моему пониманию вашей проблемы и наличию крошечной части данных, которую вы предоставили, вы получаете только желаемый результат (соответствие между FlightID_dep и FlightID_arr для всех значений FlightID_dep) без какого-либо цикла намного быстрее. table_result:

  FlightID_arr Operator  Terminal  time_arr FlightID_dep  time_dep
0        DL401       DL         3  06:50:00        DL001  09:30:00
1        DL402       DL         3  07:45:00        DL001  09:30:00
2          NaN       VS         3       NaN        VS001  15:15:00

Конечно, в общем случае (с фактическими данными) вам понадобится еще один шаг - filter table_result при условии time_arr < time_dep или любое другое условие, которое у вас есть. К сожалению, данных, которые вы предоставили, недостаточно, чтобы полностью решить вашу проблему.

Полный код:

import io
import pandas as pd

data = '''
START,END,A_D,Operator,FlightID,Terminal,TROUND_ID,tot
2017-03-26 16:55:00,2017-10-28 16:55:00,A,QR,QR001,4,QR002,70
2017-03-26 09:30:00,2017-06-11 09:30:00,D,DL,DL001,3,,84
2017-03-27 09:30:00,2017-10-28 09:30:00,D,DL,DL001,3,,78
2017-10-08 15:15:00,2017-10-22 15:15:00,D,VS,VS001,3,,45
2017-03-26 06:50:00,2017-06-11 06:50:00,A,DL,DL401,3,,9
2017-03-27 06:50:00,2017-10-28 06:50:00,A,DL,DL401,3,,19
2017-03-29 06:50:00,2017-04-19 06:50:00,A,DL,DL401,3,,3
2017-05-03 06:50:00,2017-10-25 06:50:00,A,DL,DL401,3,,32
2017-06-25 06:50:00,2017-10-22 06:50:00,A,DL,DL401,3,,95
2017-03-26 07:45:00,2017-10-28 07:45:00,A,DL,DL402,3,,58
'''

table = pd.read_csv(io.StringIO(data), parse_dates=[0, 1])
table.loc[:, 'time'] = table.loc[:, 'START'].dt.time
table = table.drop_duplicates(subset=['time', 'FlightID', 'Operator', 'Terminal'])
table_arr = table.loc[table.loc[:, 'A_D'] == 'A', ['FlightID', 'Operator', 'Terminal', 'time']]
table_dep = table.loc[table.loc[:, 'A_D'] == 'D', ['FlightID', 'Operator', 'Terminal', 'time']]

table_result = table_arr.merge(
    table_dep,
    how='right',
    on=['Operator', 'Terminal'],
    suffixes=('_arr', '_dep'))
print(table_result)