Tri personnalisé dans le dataframe pandas

89

J'ai un dataframe python pandas, dans lequel une colonne contient le nom du mois.

Comment puis-je effectuer un tri personnalisé à l'aide d'un dictionnaire, par exemple:

custom_dict = {'March':0, 'April':1, 'Dec':3}  
Kathirmani Sukumar
la source
1
Est-ce qu'une colonne contient le nom du mois signifie qu'il y a une colonne qui contient des noms de mois (comme ma réponse), ou de nombreuses colonnes avec des noms de colonne comme noms de mois (comme eumiro)?
Andy Hayden
1
La réponse acceptée est obsolète et est également techniquement incorrecte, car elle pd.Categoricaln'interprète pas les catégories comme ordonnées par défaut. Voyez cette réponse .
cs95

Réponses:

141

Pandas 0.15 a introduit la série catégorielle , qui permet une manière beaucoup plus claire de le faire:

Commencez par définir la colonne du mois comme catégorique et spécifiez l'ordre à utiliser.

In [21]: df['m'] = pd.Categorical(df['m'], ["March", "April", "Dec"])

In [22]: df  # looks the same!
Out[22]:
   a  b      m
0  1  2  March
1  5  6    Dec
2  3  4  April

Maintenant, lorsque vous triez la colonne du mois, elle sera triée par rapport à cette liste:

In [23]: df.sort_values("m")
Out[23]:
   a  b      m
0  1  2  March
2  3  4  April
1  5  6    Dec

Remarque: si une valeur ne figure pas dans la liste, elle sera convertie en NaN.


Une réponse plus ancienne pour ceux qui sont intéressés ...

Vous pourriez créer une série intermédiaire, et set_indexà ce sujet:

df = pd.DataFrame([[1, 2, 'March'],[5, 6, 'Dec'],[3, 4, 'April']], columns=['a','b','m'])
s = df['m'].apply(lambda x: {'March':0, 'April':1, 'Dec':3}[x])
s.sort_values()

In [4]: df.set_index(s.index).sort()
Out[4]: 
   a  b      m
0  1  2  March
1  3  4  April
2  5  6    Dec

Comme commenté, dans les pandas plus récents, Series a une replaceméthode pour le faire plus élégamment:

s = df['m'].replace({'March':0, 'April':1, 'Dec':3})

La légère différence est que cela n'augmentera pas s'il y a une valeur en dehors du dictionnaire (elle restera simplement la même).

Andy Hayden
la source
s = df['m'].replace({'March':0, 'April':1, 'Dec':3})fonctionne également pour la ligne 2 - juste pour le bien de quiconque apprend des pandas comme moi
kdauria
@kdauria bon endroit! (Cela fait un moment que j'ai écrit ceci!) remplace définitivement la meilleure option, une autre consiste à utiliser .apply({'March':0, 'April':1, 'Dec':3}.get):) En 0.15, nous aurons des séries / colonnes catégoriques, donc la meilleure façon sera de l'utiliser et ensuite le tri fonctionnera.
Andy Hayden
@AndyHayden J'ai pris la liberté de remplacer la deuxième ligne par la méthode «replace». J'espère que c'est bon.
Faheem Mitha le
@AndyHayden edit rejeté, mais je pense toujours que c'est un changement raisonnable.
Faheem Mitha
7
Assurez-vous simplement de l'utiliser df.sort_values("m")dans les nouveaux pandas (au lieu de df.sort("m")), sinon vous obtiendrez un AttributeError: 'DataFrame' object has no attribute 'sort';)
brainstorming du
17

pandas> = 1,1

Vous pourrez bientôt utiliser sort_valuesavec keyargument:

pd.__version__
# '1.1.0.dev0+2004.g8d10bfb6f'

custom_dict = {'March': 0, 'April': 1, 'Dec': 3} 
df

   a  b      m
0  1  2  March
1  5  6    Dec
2  3  4  April

df.sort_values(by=['m'], key=lambda x: x.map(custom_dict))

   a  b      m
0  1  2  March
2  3  4  April
1  5  6    Dec

L' keyargument prend comme entrée une série et renvoie une série. Cette série est triée en interne et les index triés sont utilisés pour réorganiser le DataFrame d'entrée. S'il y a plusieurs colonnes sur lesquelles trier, la fonction clé sera appliquée à chacune à son tour. Voir Tri avec des clés .


pandas <= 1.0.X

Une méthode simple consiste à utiliser la sortie Series.mapet Series.argsortà indexer en dfutilisant DataFrame.iloc(puisque argsort produit des positions entières triées); puisque vous avez un dictionnaire; cela devient facile.

df.iloc[df['m'].map(custom_dict).argsort()]

   a  b      m
0  1  2  March
2  3  4  April
1  5  6    Dec

Si vous devez trier par ordre décroissant , inversez le mappage.

df.iloc[(-df['m'].map(custom_dict)).argsort()]

   a  b      m
1  5  6    Dec
2  3  4  April
0  1  2  March

Notez que cela ne fonctionne que sur les éléments numériques. Sinon, vous devrez contourner ce problème en utilisant sort_valueset en accédant à l'index:

df.loc[df['m'].map(custom_dict).sort_values(ascending=False).index]

   a  b      m
1  5  6    Dec
2  3  4  April
0  1  2  March

Plus d'options sont disponibles avec astype(c'est désormais obsolète), ou pd.Categorical, mais vous devez spécifier ordered=Truepour que cela fonctionne correctement .

# Older version,
# df['m'].astype('category', 
#                categories=sorted(custom_dict, key=custom_dict.get), 
#                ordered=True)
df['m'] = pd.Categorical(df['m'], 
                         categories=sorted(custom_dict, key=custom_dict.get), 
                         ordered=True)

Maintenant, un simple sort_valuesappel fera l'affaire:

df.sort_values('m')
 
   a  b      m
0  1  2  March
2  3  4  April
1  5  6    Dec

L'ordre des catégories sera également respecté lors du groupbytri de la sortie.

cs95
la source
2
Vous l'avez déjà souligné, mais je voudrais le répéter au cas où quelqu'un d'autre le passerait à côté et le manquerait: Pandas Sets catégoriques ordered=Nonepar défaut. S'il n'est pas défini, la commande sera erronée ou sera interrompue sur V23. La fonction Max en particulier donne un TypeError (le catégoriel n'est pas ordonné pour l'opération max).
Dave Liu
16

Un peu tard dans le jeu, mais voici un moyen de créer une fonction qui trie les objets pandas Series, DataFrame et multiindex DataFrame à l'aide de fonctions arbitraires.

J'utilise la df.iloc[index]méthode, qui référence une ligne dans un Series / DataFrame par position (par rapport à df.loc, quelles références par valeur). En utilisant cela, nous devons juste avoir une fonction qui retourne une série d'arguments positionnels:

def sort_pd(key=None,reverse=False,cmp=None):
    def sorter(series):
        series_list = list(series)
        return [series_list.index(i) 
           for i in sorted(series_list,key=key,reverse=reverse,cmp=cmp)]
    return sorter

Vous pouvez l'utiliser pour créer des fonctions de tri personnalisées. Cela fonctionne sur le dataframe utilisé dans la réponse d'Andy Hayden:

df = pd.DataFrame([
    [1, 2, 'March'],
    [5, 6, 'Dec'],
    [3, 4, 'April']], 
  columns=['a','b','m'])

custom_dict = {'March':0, 'April':1, 'Dec':3}
sort_by_custom_dict = sort_pd(key=custom_dict.get)

In [6]: df.iloc[sort_by_custom_dict(df['m'])]
Out[6]:
   a  b  m
0  1  2  March
2  3  4  April
1  5  6  Dec

Cela fonctionne également sur les objets DataFrames et Series multiindex:

months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']

df = pd.DataFrame([
    ['New York','Mar',12714],
    ['New York','Apr',89238],
    ['Atlanta','Jan',8161],
    ['Atlanta','Sep',5885],
  ],columns=['location','month','sales']).set_index(['location','month'])

sort_by_month = sort_pd(key=months.index)

In [10]: df.iloc[sort_by_month(df.index.get_level_values('month'))]
Out[10]:
                 sales
location  month  
Atlanta   Jan    8161
New York  Mar    12714
          Apr    89238
Atlanta   Sep    5885

sort_by_last_digit = sort_pd(key=lambda x: x%10)

In [12]: pd.Series(list(df['sales'])).iloc[sort_by_last_digit(df['sales'])]
Out[12]:
2    8161
0   12714
3    5885
1   89238

Pour moi, cela semble propre, mais cela utilise beaucoup les opérations python plutôt que de s'appuyer sur des opérations pandas optimisées. Je n'ai fait aucun test de résistance, mais j'imagine que cela pourrait ralentir sur de très grands DataFrames. Je ne sais pas comment les performances se comparent à l'ajout, au tri, puis à la suppression d'une colonne. Tous les conseils pour accélérer le code seraient appréciés!

Michael Delgado
la source
Cela fonctionnerait-il pour trier plusieurs colonnes / index?
ConanG
oui, mais la réponse choisie est une bien meilleure façon de le faire. Si vous avez plusieurs index, organisez-les simplement selon l'ordre de tri que vous préférez, puis utilisez df.sort_index()pour trier tous les niveaux d'index.
Michael Delgado
9
import pandas as pd
custom_dict = {'March':0,'April':1,'Dec':3}

df = pd.DataFrame(...) # with columns April, March, Dec (probably alphabetically)

df = pd.DataFrame(df, columns=sorted(custom_dict, key=custom_dict.get))

renvoie un DataFrame avec des colonnes mars, avril, décembre

eumiro
la source
Cela trie les colonnes réelles, plutôt que de trier les lignes en fonction du prédicat personnalisé de la colonne?
cs95