Python - для цикла миллионы строк - PullRequest
0 голосов
/ 11 сентября 2018

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

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

  • Необходимо только перебрать строки, которые были c['A_D'] == D и c['Already_linked'] == 0
  • Значение hour в кадре данных arr должно быть меньше, чем hour_aux в кадре данных c
  • Столбец 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, который минимизирует время между vb.START и c [x]. FightID, соответствующий этому индексу, затем сохраняется в кадре данных c в столбце a. Кроме того, поскольку прибытие было связано с отправлением, столбец Already_linked в кадре данных arr изменяется с 0 на 1.

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

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

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

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

Исходный кадр данных:

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']

Ответы [ 4 ]

0 голосов
/ 15 сентября 2018

В этом решении используется файл pd.DataFrame.isin, в котором используется numpy.in1d ​​

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

flight_record_linkage.ipynb

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

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

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

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()
0 голосов
/ 12 сентября 2018

Хотя это не векторизованное решение, оно должно ускорить процесс, если ваш выборочный набор данных имитирует ваш истинный набор данных. В настоящее время вы тратите время на цикл по каждой строке, но вы заботитесь только о циклах по строкам, где ['A_D'] == 'D' и ['Already_linked'] ==0. Вместо этого удалите if и переберите усеченный фрейм данных, который составляет только 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
0 голосов
/ 14 сентября 2018

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

У вас очень длинный 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

Конечно, в общем случае (с фактическими данными) вам потребуется еще один шаг - фильтр 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)
0 голосов
/ 12 сентября 2018

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

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

  • Так как вы все равно нарезаете arr каждый раз, сохраняйте вид только строк arr.Already_Linked==0, чтобывы перебираете объект меньшего размера.
  • Еще лучше, прежде чем делать какие-либо циклы, вы должны сначала сгруппировать arr по Terminal и Operator.Затем, вместо того, чтобы проходить через все arr, сначала выберите нужную группу, а затем выполните нарезку и фильтрацию.Для этого потребуется немного переосмыслить точную реализацию query_string, но преимущество заключается в том, что если у вас много терминалов и операторов, вы, как правило, работаете над объектом намного меньшего размера, чем arr.Более того, вам даже не придется запрашивать этот объект, так как это было неявно сделано groupby.
  • В зависимости от того, как 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(), чтобы эффективно найти крошечное подмножество данных, которые вас интересуют.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...