2016-03-15 1 views
0

I ont une trame de données de valeurs:Pandas - expansion fonction quantile inverse

df = pd.DataFrame(np.random.uniform(0,1,(500,2)), columns = ['a', 'b']) 
>>> print df 
      a   b 
1 0.277438 0.042671 
..  ...  ... 
499 0.570952 0.865869 

[500 rows x 2 columns] 

Je veux transformer ceci en remplaçant les valeurs de leur percentile, où le percentile est pris en charge la distribution de toutes les valeurs en avant lignes. c'est-à-dire, si vous faites df.T.unstack(), ce serait un échantillon en expansion pure. Cela peut être plus intuitif si vous considérez l'index comme DatetimeIndex, et je demande de prendre le percentile en expansion sur l'ensemble de l'historique transversal.

L'objectif est donc ce gars-là:

 a b 
0 99 99 
.. .. .. 
499 58 84 

(Idéalement Je voudrais prendre la distribution d'une valeur sur l'ensemble de toutes les valeurs dans toutes les lignes avant et y compris cette ligne, donc pas exactement un percentile élargissons;. mais si nous ne pouvons pas que, c'est très bien)

J'ai une vraiment façon laide de le faire, où je transposer et désempiler le dataframe, générer un masque percentile, et superposer tha masque t sur la trame de données en utilisant une boucle pour obtenir les centiles:

percentile_boundaries_over_time = pd.DataFrame({integer: 
            pd.expanding_quantile(df.T.unstack(), integer/100.0) 
            for integer in range(0,101,1)}) 

percentile_mask = pd.Series(index = df.unstack().unstack().unstack().index) 

for integer in range(0,100,1): 
    percentile_mask[(df.unstack().unstack().unstack() >= percentile_boundaries_over_time[integer]) & 
        (df.unstack().unstack().unstack() <= percentile_boundaries_over_time[integer+1])] = integer 

J'ai essayé d'obtenir quelque chose plus rapide à travailler, en utilisant scipy.stats.percentileofscore() et pd.expanding_apply(), mais il est ne pas donner la bonne sortie et je me rend fou en essayant de comprendre pourquoi. C'est ce que j'ai joué avec:

perc = pd.expanding_apply(df, lambda x: stats.percentileofscore(x, x[-1], kind='weak')) 

Quelqu'un at-il des idées sur pourquoi cela donne une sortie incorrecte? Ou un moyen plus rapide de faire tout cet exercice? Tout le monde aide beaucoup apprécié!

+0

Qu'est-ce qui vous fait penser l'expansion de votre application donne des résultats erronés? Cela me semble correct au premier coup d'œil (à l'intérieur de chaque colonne, il ne semble pas permettre de combiner les lignes). Peut-être faire un appel 'np.random.seed()' avant de générer vos données afin que d'autres personnes puissent vérifier les résultats par rapport aux mêmes données? – Marius

Répondre

1

Comme plusieurs autres intervenants ont souligné, percentiles de calcul pour chaque ligne implique probablement le tri des données à chaque fois. Ce sera probablement le cas pour toute solution préemballée, y compris pd.DataFrame.rank ou scipy.stats.percentileofscore. Le tri répétitif est coûteux en ressources et en calculs, nous voulons donc une solution qui minimise cela. En prenant du recul, trouver le quantile inverse d'une valeur par rapport à un ensemble de données existant est analogue à la recherche de la position dans laquelle on insèrerait cette valeur dans l'ensemble de données s'il était trié. Le problème est que nous avons également un ensemble de données en expansion. Heureusement, certains algorithmes de tri sont extrêmement rapides pour traiter les données les plus triées (et insérer un petit nombre d'éléments non triés). Notre stratégie consiste donc à conserver notre propre tableau de données triées et, à chaque itération de ligne, à l'ajouter à notre liste existante et à interroger leurs positions dans le nouvel ensemble trié. Cette dernière opération est également rapide étant donné que les données sont triées.

Je pense que insertion sort serait le tri le plus rapide pour cela, mais ses performances seront probablement plus lentes en Python que n'importe quel type NumPy natif. Fusionner tri semble être la meilleure des options disponibles dans NumPy. Une solution idéale impliquerait l'écriture de Cython, mais en utilisant notre stratégie ci-dessus avec NumPy nous obtient la plupart du temps.

C'est une solution à la main:

def quantiles_by_row(df): 
    """ Reconstruct a DataFrame of expanding quantiles by row """ 

    # Construct skeleton of DataFrame what we'll fill with quantile values 
    quantile_df = pd.DataFrame(np.NaN, index=df.index, columns=df.columns) 

    # Pre-allocate numpy array. We only want to keep the non-NaN values from our DataFrame 
    num_valid = np.sum(~np.isnan(df.values)) 
    sorted_array = np.empty(num_valid) 

    # We want to maintain that sorted_array[:length] has data and is sorted 
    length = 0 

    # Iterates over ndarray rows 
    for i, row_array in enumerate(df.values): 

     # Extract non-NaN numpy array from row 
     row_is_nan = np.isnan(row_array) 
     add_array = row_array[~row_is_nan] 

     # Add new data to our sorted_array and sort. 
     new_length = length + len(add_array) 
     sorted_array[length:new_length] = add_array 
     length = new_length 
     sorted_array[:length].sort(kind="mergesort") 

     # Query the relative positions, divide by length to get quantiles 
     quantile_row = np.searchsorted(sorted_array[:length], add_array, side="left").astype(np.float)/length 

     # Insert values into quantile_df 
     quantile_df.iloc[i][~row_is_nan] = quantile_row 

    return quantile_df 

Sur la base des données fournies bhalperin (hors ligne), cette solution est jusqu'à 10 fois plus rapide. Un dernier commentaire: np.searchsorted a des options pour 'left' et 'right' qui détermine si vous voulez que votre position potentielle insérée soit la première ou la dernière position appropriée possible. Cela est important si vous avez beaucoup de doublons dans vos données. Une version plus précise de la solution ci-dessus prendra la moyenne des 'left' et 'right':

# Query the relative positions, divide to get quantiles 
left_rank_row = np.searchsorted(sorted_array[:length], add_array, side="left") 
right_rank_row = np.searchsorted(sorted_array[:length], add_array, side="right") 
quantile_row = (left_rank_row + right_rank_row).astype(np.float)/(length * 2) 
+0

Vous êtes un gentleman et un érudit – bhalperin

0

Voici une tentative pour implémenter votre "centile sur l'ensemble de toutes les valeurs de toutes les lignes avant et en incluant cette exigence". stats.percentileofscore semble agir quand donné des données 2D, donc squeeze semble ing aider à obtenir des résultats corrects:

a_percentile = pd.Series(np.nan, index=df.index) 
b_percentile = pd.Series(np.nan, index=df.index) 

for current_index in df.index: 
    preceding_rows = df.loc[:current_index, :] 
    # Combine values from all columns into a single 1D array 
    # * 2 should be * N if you have N columns 
    combined = preceding_rows.values.reshape((1, len(preceding_rows) *2)).squeeze() 
    a_percentile[current_index] = stats.percentileofscore(
     combined, 
     df.loc[current_index, 'a'], 
     kind='weak' 
    ) 
    b_percentile[current_index] = stats.percentileofscore(
     combined, 
     df.loc[current_index, 'b'], 
     kind='weak' 
    ) 
+0

Je trouve que cette méthode est à peu près aussi rapide que ma méthode précédente. De plus, je n'arrive toujours pas à comprendre pourquoi stats.percentileofscore() donne une réponse différente de celle de pd.quantile()! Je pense que je dois mal interpréter pd.quantile() vs stats.percentileofscore() – bhalperin

0

Pourtant, pas tout à fait clair, mais ne voulez-vous une somme cumulée divisé par le total?

norm = 100.0/df.a.sum() 
df['cum_a'] = df.a.cumsum() 
df['cum_a'] = df.cum_a * norm 

idem pour b