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
.
-
table = table.drop_duplicates(subset=['time', 'FlightID', 'Operator', 'Terminal'])
. И ваш table
станет значительно короче.
-
Разделить 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']]
-
Кажется, что все, что вы пытались получить с помощью циклов, вы можете получить с одной строкой: 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)