Вложенная (двойная) построчная итерация Pandas DataFrame - PullRequest
0 голосов
/ 31 мая 2018

Привет! Я пытаюсь найти векторизованное (или более эффективное) решение итерационной задачи, где единственное найденное мной решение требует построчную итерацию объекта DataFrame с несколькими циклами.Фактический файл данных огромен, поэтому мое текущее решение практически неосуществимо.Я включил выходные данные линейного профилировщика в самом конце, если вы хотите посмотреть.Реальная проблема довольно сложна, поэтому я попытаюсь объяснить это на простом примере (мне понадобилось довольно много времени, чтобы упростить его:)):

Предположим, у нас есть аэропорт с двумя посадочными полосами рядом,Каждый самолет приземляется (время прибытия), некоторое время рулит на одной из посадочных полос, затем взлетает (время вылета).Все хранится в Pandas DataFrame, который отсортирован по времени прибытия, следующим образом (см. EDIT2 для большего набора данных для тестирования):

PLANE   STRIP   ARRIVAL   DEPARTURE
0       1       85.00     86.00
1       1       87.87     92.76
2       2       88.34     89.72
3       1       88.92     90.88
4       2       90.03     92.77
5       2       90.27     91.95
6       2       92.42     93.58
7       2       94.42     95.58

Поиск решений для двух случаев:

1. Создайте список событий, в которых одновременно на одной полосе присутствует более одной плоскости.Не включайте подмножества событий (например, не показывать [3,4], если есть действительный случай [3,4,5]).В списке должны храниться индексы фактических строк DataFrame.См. Функцию findSingleEvents (), чтобы найти решение для этого случая (работает около 5 мс).

2. Построить список событий, в которых одновременно на каждой полосе имеется хотя бы одна плоскость,Не считайте подмножества события, только запишите событие с максимальным количеством самолетов.(например, не показывать [3,4], если есть [3,4,5] случай).Не считайте события, которые полностью происходят на одной полосе.В списке должны храниться индексы фактических строк DataFrame.См. Функцию findMultiEvents () для решения этого случая (работает около 15 мс).

Рабочий код:

import numpy as np
import pandas as pd
import itertools
from __future__ import division

data =  [{'PLANE':0, 'STRIP':1, 'ARRIVAL':85.00, 'DEPARTURE':86.00},
         {'PLANE':1, 'STRIP':1, 'ARRIVAL':87.87, 'DEPARTURE':92.76},
         {'PLANE':2, 'STRIP':2, 'ARRIVAL':88.34, 'DEPARTURE':89.72},
         {'PLANE':3, 'STRIP':1, 'ARRIVAL':88.92, 'DEPARTURE':90.88},
         {'PLANE':4, 'STRIP':2, 'ARRIVAL':90.03, 'DEPARTURE':92.77},
         {'PLANE':5, 'STRIP':2, 'ARRIVAL':90.27, 'DEPARTURE':91.95},
         {'PLANE':6, 'STRIP':2, 'ARRIVAL':92.42, 'DEPARTURE':93.58},
         {'PLANE':7, 'STRIP':2, 'ARRIVAL':94.42, 'DEPARTURE':95.58}]

df = pd.DataFrame(data, columns = ['PLANE','STRIP','ARRIVAL','DEPARTURE'])

def findSingleEvents(df):
    events = []
    for row in df.itertuples():
        #Create temporary dataframe for each main iteration
        dfTemp = df[(row.DEPARTURE>df.ARRIVAL) & (row.ARRIVAL<df.DEPARTURE)]
        if len(dfTemp)>1:
            #convert index values to integers from long
            current_event = [int(v) for v in dfTemp.index.tolist()]
            #loop backwards to remove elements that do not comply
            for i in reversed(current_event):
                if (dfTemp.loc[i].ARRIVAL > dfTemp.DEPARTURE).any():
                    current_event.remove(i)
            events.append(current_event)
    #remove duplicate events
    events = map(list, set(map(tuple, events)))
    return events

def findMultiEvents(df):
    events = []
    for row in df.itertuples():
        #Create temporary dataframe for each main iteration
        dfTemp = df[(row.DEPARTURE>df.ARRIVAL) & (row.ARRIVAL<df.DEPARTURE)]
        if len(dfTemp)>1:
            #convert index values to integers from long
            current_event = [int(v) for v in dfTemp.index.tolist()]
            #loop backwards to remove elements that do not comply
            for i in reversed(current_event):
                if (dfTemp.loc[i].ARRIVAL > dfTemp.DEPARTURE).any():
                    current_event.remove(i)
            #remove elements only on 1 strip
            if len(df.iloc[current_event].STRIP.unique()) > 1:
                events.append(current_event)
    #remove duplicate events
    events = map(list, set(map(tuple, events)))
    return events

print findSingleEvents(df[df.STRIP==1])
print findSingleEvents(df[df.STRIP==2])
print findMultiEvents(df)

Проверенный вывод:

[[1, 3]]
[[4, 5], [4, 6]]
[[1, 3, 4, 5], [1, 4, 6], [1, 2, 3]]

Очевидно, что это не эффективные и не элегантные решения.С огромным DataFrame, который у меня есть, запуск этого, вероятно, займет несколько часов.Я долго думал о векторизованном подходе, но не смог придумать ничего солидного.Любые указатели / помощь будут приветствоваться!Я также открыт для одобрений на основе Numpy / Cython / Numba.

Спасибо!

PS: Если вам интересно, что я буду делать со списками: я назначуEVENT номер для каждого EVENT и создайте отдельную базу данных со слиянием указанных выше данных, а номера EVENT в виде отдельного столбца, который будет использоваться для чего-то другого.Для случая 1 это будет выглядеть примерно так:

EVENT    PLANE   STRIP   ARRIVAL   DEPARTURE
0        4       2       90.03     92.77
0        5       2       90.27     91.95
1        5       2       90.27     91.95
1        6       2       92.42     95.58

РЕДАКТИРОВАТЬ: Исправлен код и набор тестовых данных.

РЕДАКТИРОВАТЬ2: Используйте приведенный ниже код для создания DataFrame длиной 1000 строк (или более) для целей тестирования.(согласно рекомендации @ImportanceOfBeingErnest)

import random
import pandas as pd
import numpy as np

data =  []
for i in range(1000):
    arrival = random.uniform(0,1000)
    departure = arrival + random.uniform(2.0, 10.0)
    data.append({'PLANE':i, 'STRIP':random.randint(1, 2),'ARRIVAL':arrival,'DEPARTURE':departure})

df = pd.DataFrame(data, columns = ['PLANE','STRIP','ARRIVAL','DEPARTURE'])
df = df.sort_values(by=['ARRIVAL'])
df = df.reset_index(drop=True)
df.PLANE  = df.index

EDIT3:

Измененная версия принятого ответа.Принятый ответ не смог удалить подмножества событий.Модифицированная версия удовлетворяет правилу "(например, не показывать [3,4], если есть действительный [3,4,5] случай)"

def maximal_subsets_modified(sets):
    sets.sort()
    maximal_sets = []
    s0 = frozenset()
    for s in sets:
        if not (s > s0) and len(s0) > 1:
            not_in_list = True
            for x in maximal_sets:
                if set(x).issubset(set(s0)):
                    maximal_sets.remove(x)
                if set(s0).issubset(set(x)):
                    not_in_list = False
            if not_in_list:
                maximal_sets.append(list(s0))
        s0 = s
    if len(s0) > 1:
        not_in_list = True
        for x in maximal_sets:
            if set(x).issubset(set(s0)):
                maximal_sets.remove(x)
            if set(s0).issubset(set(x)):
                not_in_list = False
        if not_in_list:
            maximal_sets.append(list(s0))
    return maximal_sets

def maximal_subsets_2_modified(sets, d):
    sets.sort()
    maximal_sets = []
    s0 = frozenset()
    for s in sets:
        if not (s > s0) and len(s0) > 1 and d.loc[list(s0), 'STRIP'].nunique() == 2:
            not_in_list = True
            for x in maximal_sets:
                if set(x).issubset(set(s0)):
                    maximal_sets.remove(x)
                if set(s0).issubset(set(x)):
                    not_in_list = False
            if not_in_list:
                maximal_sets.append(list(s0))
        s0 = s
    if len(s0) > 1 and d.loc[list(s), 'STRIP'].nunique() == 2:
        not_in_list = True
        for x in maximal_sets:
            if set(x).issubset(set(s0)):
                maximal_sets.remove(x)
            if set(s0).issubset(set(x)):
                not_in_list = False
        if not_in_list:
            maximal_sets.append(list(s0))
    return maximal_sets

# single

def hal_3_modified(d):
    sets = np.apply_along_axis(
        lambda x: frozenset(d.PLANE.values[(d.PLANE.values <= x[0]) & (d.DEPARTURE.values > x[2])]), 
        1, d.values
    )
    return maximal_subsets_modified(sets)

# multi

def hal_5_modified(d):
    sets = np.apply_along_axis(
        lambda x: frozenset(d.PLANE.values[(d.PLANE.values <= x[0]) & (d.DEPARTURE.values > x[2])]), 
        1, d.values
    )
    return maximal_subsets_2_modified(sets, d)

1 Ответ

0 голосов
/ 03 июня 2018

Я переписал решение, используя DataFrame.apply вместо итерации, и в качестве оптимизации использовали по возможности массивные массивы.Я использовал frozenset, потому что они неизменяемы и хэшируемы и, следовательно, Series.unique работает правильно.Series.unique терпит неудачу на элементах типа set.

Кроме того, я обнаружил, что d.loc[list(x), 'STRIP'].nunique() немного быстрее, чем d.loc[list(x)].STRIP.nunique().Я не уверен, почему, но я использовал более быстрое утверждение в приведенном ниже решении.

Алгоритм на простом английском языке:

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

Возвращает уникальные наборы, которые не являются подмножествами других наборов (и для 2-го алгоритма дополнительно отфильтруйте, что оба STRIP s упоминаются наборами)

(Обновление) 2-е улучшение:

1 небольшое улучшение достигается путем перехода к слоистому слою и использования np.apply_along_axis вместо использования df.apply.Это возможно, поскольку PLANE всегда равен индексу фрейма данных, и мы можем получить доступ к базовой матрице с помощью df.values

. Я обнаружил значительное улучшение в понимании списка, которое возвращает максимальные подмножества

[list(x) for x in sets if ~np.any(sets > x)]

Выше приведена операция порядка O (n ^ 2).На небольших наборах данных это очень быстро.Однако для больших наборов данных это утверждение становится узким местом.Чтобы оптимизировать это, сначала сортируйте sets и снова просматривайте элементы, чтобы найти максимальные подмножества.После сортировки достаточно проверить, что elem [n] не является подмножеством elem [n + 1], чтобы определить, является ли elem [n] максимальным подмножеством.Процедура сортировки сравнивает два элемента с операцией <

Время:

Хотя моя первоначальная реализация значительно улучшила производительность по сравнению с попыткой OP, алгоритм был упорядочен по экспоненте, так какследующий график показывает.

Я представляю только время для findMultiEvents, hal_2 & hal_5.Относительные показатели findSinglEvents, hal_1 и hal_3 аналогично сопоставимы.

algorithm execution time ~ input sizes

прокрутите ниже, чтобы увидеть код тестирования.

обратите внимание, что я прекратил тестирование findMumtiEvents & hal_2ранее, когда стало очевидно, что они были менее эффективными по экспоненциальному коэффициенту

Реализация


Улучшенная реализация:

def maximal_subsets(sets):
    sets.sort()
    maximal_sets = []
    s0 = frozenset()
    for s in sets[::-1]:
        if s0 > s or len(s) < 2:
            continue
        maximal_sets.append(list(s))
        s0 = s
    return maximal_sets

def maximal_subsets_2(sets, d):
    sets.sort()
    maximal_sets = []
    s0 = frozenset()
    for s in sets[::-1]:
        if s0 > s or len(s) < 2 or d.loc[list(s), 'STRIP'].nunique() < 2:
            continue
        maximal_sets.append(list(s))
        s0 = s
    return maximal_sets

# single
def hal_3(d):
    sets = np.apply_along_axis(
        lambda x: frozenset(d.PLANE.values[(d.PLANE.values <= x[0]) & (d.DEPARTURE.values > x[2])]), 
        1, d.values
    )
    return maximal_subsets(sets)
# multi
def hal_5(d):
    sets = np.apply_along_axis(
        lambda x: frozenset(d.PLANE.values[(d.PLANE.values <= x[0]) & (d.DEPARTURE.values > x[2])]), 
        1, d.values
    )
    return maximal_subsets_2(sets, d)

Исходная реализация:

# findSingleEvents
def hal_1(d):
    sets = d.apply(
       lambda x: frozenset(
           d.index.values[(d.index.values <= x.name) & (d.DEPARTURE.values > x.ARRIVAL)]
       ),
       axis=1
    ).unique()
    return [list(x) for x in sets if ~np.any(sets > x) and len(x) > 1]

# findMultiEvents
def hal_2(d):
    sets = d.apply(
        lambda x: frozenset(
            d.index.values[(d.index.values <= x.name) & (d.DEPARTURE.values > x.ARRIVAL)]
        ),
        axis=1
    ).unique()
    return [list(x) for x in sets 
            if ~np.any(sets > x) and
               len(x) > 1 and 
               d.loc[list(x), 'STRIP'].nunique() == 2]

Выходы:

Выходы идентичны реализации OP.

hal_1(df[df.STRIP==1])
[[1, 3]]
hal_1(df[df.STRIP==2])
[[4, 5], [4, 6]]
hal_2(df)
[[1, 2, 3], [1, 3, 4, 5], [1, 4, 6]]
hal_3(df[df.STRIP==1])
[[1, 3]]
hal_3(df[df.STRIP==2])
[[4, 5], [4, 6]]
hal_5(df)
[[1, 2, 3], [1, 3, 4, 5], [1, 4, 6]]

Детали тестовой системы:

os: windows 10
python: 3.6 (Anaconda)
pandas: 0.22.0
numpy: 1.14.3

Код бенчмаркинга:


import random

def mk_random_df(n):
    data =  []
    for i in range(n):
        arrival = random.uniform(0,1000)
        departure = arrival + random.uniform(2.0, 10.0)
        data.append({'PLANE':i, 'STRIP':random.randint(1, 2),'ARRIVAL':arrival,'DEPARTURE':departure})

    df = pd.DataFrame(data, columns = ['PLANE','STRIP','ARRIVAL','DEPARTURE'])
    df = df.sort_values(by=['ARRIVAL'])
    df = df.reset_index(drop=True)
    df.PLANE = df.index
    return df

dfs = {i: mk_random_df(100*(2**i)) for i in range(0, 10)}
times, times_2, times_5 = [], [], []

for i, v in dfs.items():
    if i < 5:
        t = %timeit -o -n 3 -r 3 findMultiEvents(v)
        times.append({'size(pow. of 2)': i, 'timings': t})

for i, v in dfs.items():
    t = %timeit -o -n 3 -r 3 hal_5(v)
    times_5.append({'size(pow. of 2)': i, 'timings': t})

for i, v in dfs.items():
    if i < 9:
        t = %timeit -o -n 3 -r 3 hal_2(v)
        times_2.append({'size(pow. of 2)': i, 'timings': t})
...