2016-08-04 12 views
6

Jaki jest najszybszy sposób (w granicach rozsądnej pythonowości) zliczania różnych wartości w kolumnach tego samego dtype, dla każdego wiersza w DataFrame?efektywna liczba różnych w kolumnach DataFrame, pogrupowanych według wierszy

Szczegóły: Mam DataFrame kategorycznych wyników tematycznie (w rzędach) za dzień (w kolumnach), podobne do czegoś generowanego przez poniżej.

import numpy as np 
import pandas as pd 

def genSampleData(custCount, dayCount, discreteChoices): 
    """generate example dataset""" 
    np.random.seed(123)  
    return pd.concat([ 
       pd.DataFrame({'custId':np.array(range(1,int(custCount)+1))}), 
       pd.DataFrame(
       columns = np.array(['day%d' % x for x in range(1,int(dayCount)+1)]), 
       data = np.random.choice(a=np.array(discreteChoices), 
             size=(int(custCount), int(dayCount)))  
       )], axis=1) 

Na przykład, jeśli zbiór danych, który mówi nam pić każdy klient zamówił na każdej wizycie w sklepie, chciałbym znać liczbę odrębnych drinków klienta.

# notional discrete choice outcome   
drinkOptions, drinkIndex = np.unique(['coffee','tea','juice','soda','water'], 
            return_inverse=True) 

# integer-coded discrete choice outcomes 
d = genSampleData(2,3, drinkIndex) 
d 
# custId day1 day2 day3 
#0  1  1  4  1 
#1  2  3  2  1 

# Count distinct choices per subject -- this is what I want to do efficiently on larger DF 
d.iloc[:,1:].apply(lambda x: len(np.unique(x)), axis=1) 
#0 2 
#1 3 

# Note: I have coded the choices as `int` rather than `str` to speed up comparisons. 
# To reconstruct the choice names, we could do: 
# d.iloc[:,1:] = drinkOptions[d.iloc[:,1:]] 

Co próbowałem: zbiorów danych w tym przypadku zastosowania będą miały o wiele więcej przedmiotów niż dni (przykład testDf poniżej), więc starałem się znaleźć najbardziej efektywne row-mądry operacji:

testDf = genSampleData(100000,3, drinkIndex) 

#---- Original attempts ---- 
%timeit -n20 testDf.iloc[:,1:].apply(lambda x: x.nunique(), axis=1) 
# I didn't wait for this to finish -- something more than 5 seconds per loop 
%timeit -n20 testDf.iloc[:,1:].apply(lambda x: len(x.unique()), axis=1) 
# Also too slow 
%timeit -n20 testDf.iloc[:,1:].apply(lambda x: len(np.unique(x)), axis=1) 
#20 loops, best of 3: 2.07 s per loop 

Aby poprawić na mojej oryginalnej próbie, możemy zauważyć, że pandas.DataFrame.apply() przyjmuje argument:

Jeśli raw=True p funkcja assed otrzyma zamiast tego obiekty ndarray. Jeśli tylko stosując funkcję redukcji NumPy ten osiągnie znacznie lepsza wydajność

To było wyciąć czasu pracy przez ponad pół:

%timeit -n20 testDf.iloc[:,1:].apply(lambda x: len(np.unique(x)), axis=1, raw=True) 
#20 loops, best of 3: 721 ms per loop *best so far* 

Byłem zaskoczony, że to czysty roztwór numpy, który wydaje się być równoważne z powyższym z raw=True był faktycznie nieco wolniej:

%timeit -n20 np.apply_along_axis(lambda x: len(np.unique(x)), axis=1, arr = testDf.iloc[:,1:].values) 
#20 loops, best of 3: 1.04 s per loop 

Wreszcie, ja też próbowałem Transpo zaśpiewaj dane, aby wykonać column-wise count distinct, co moim zdaniem może być bardziej efektywne (przynajmniej dla DataFrame.apply(), ale nie wydaje się, żeby było znaczącą różnicą.

%timeit -n20 testDf.iloc[:,1:].T.apply(lambda x: len(np.unique(x)), raw=True) 
#20 loops, best of 3: 712 ms per loop *best so far* 
%timeit -n20 np.apply_along_axis(lambda x: len(np.unique(x)), axis=0, arr = testDf.iloc[:,1:].values.T) 
# 20 loops, best of 3: 1.13 s per loop 

tej pory moim najlepszym rozwiązaniem jest dziwna mieszanka df.apply z len(np.unique()), ale co jeszcze powinienem spróbować?

+0

jest przedstawicielem Ilość dni? Wydaje się znacznie wpływać na różnice występów. – ayhan

+0

@ayhan ciekawe ... liczba dni jest reprezentatywna dla mojego konkretnego przypadku użycia, ale jeśli coś innego działa lepiej dla szerszych zestawów danych, które byłyby warte odnotowania dla innych użytkowników – C8H10N4O2

+0

Wręcz przeciwnie. Wygląda na to, że porównywanie każdej kolumny z innymi jest znacznie szybsze, gdy masz małą liczbę kolumn. Wysłałem wyniki jako odpowiedź. – ayhan

Odpowiedz

3

Rozumiem, że nunique jest zoptymalizowany dla dużych seriach. Masz tutaj tylko 3 dni.Porównanie każdej kolumny z innymi wydaje się szybsze:

testDf = genSampleData(100000,3, drinkIndex) 
days = testDf.columns[1:] 

%timeit testDf.iloc[:, 1:].stack().groupby(level=0).nunique() 
10 loops, best of 3: 46.8 ms per loop 

%timeit pd.melt(testDf, id_vars ='custId').groupby('custId').value.nunique() 
10 loops, best of 3: 47.6 ms per loop 

%%timeit 
testDf['nunique'] = 1 
for col1, col2 in zip(days, days[1:]): 
    testDf['nunique'] += ~((testDf[[col2]].values == testDf.ix[:, 'day1':col1].values)).any(axis=1) 
100 loops, best of 3: 3.83 ms per loop 

Traci swoją przewagę, gdy dodajesz więcej kolumn oczywiście. Dla różnej liczby kolumn (tej samej kolejności: stack().groupby(), pd.melt().groupby() i pętle):

10 columns: 143ms, 161ms, 30.9ms 
50 columns: 749ms, 968ms, 635ms 
100 columns: 1.52s, 2.11s, 2.33s 
+0

wow, pętla 'for' za wygraną? – C8H10N4O2

+0

Tak, ponieważ ta pętla działa tylko kilka razy. Dodałem także czasy dla większej liczby kolumn. – ayhan

+1

świetna odpowiedź! +1, wciąż kręcę kółkami, nalegając na lepsze rozwiązanie. – piRSquared

2

pandas.melt z DataFrame.groupby i groupby.SeriesGroupBy.nunique wydaje wiać inne rozwiązania z dala:

%timeit -n20 pd.melt(testDf, id_vars ='custId').groupby('custId').value.nunique() 
#20 loops, best of 3: 67.3 ms per loop 
+0

Być może zastrzeżenie tutaj, że jest to grupowanie według wartości kolumny, a nie numeru wiersza. Jeśli nie masz unikalnej zmiennej identyfikatora wiersza, możesz wziąć pod uwagę (mały) koszt utworzenia jednego dla sprawiedliwego testu porównawczego. – C8H10N4O2

+1

We wszystkich innych rozwiązaniach pętla 'apply' dzieje się w pythonie - tutaj' Groupby.nunique' używa kilku lew (patrz [tutaj] (https://github.com/pydata/pandas/blob/master/pandas/ core/groupby.py # L2896)), aby zrobić to wszystko za pomocą operacji wektoryzacji. – chrisb

1

Nie trzeba custId. Ja bym stack, następnie groupby

testDf.iloc[:, 1:].stack().groupby(level=0).nunique() 

enter image description here

Powiązane problemy