Les pandas sélectionnant par étiquette retournent parfois Series, parfois DataFrame

96

Dans Pandas, lorsque je sélectionne une étiquette qui n'a qu'une seule entrée dans l'index, je récupère une série, mais lorsque je sélectionne une entrée qui a plus d'une entrée, je récupère une trame de données.

Pourquoi donc? Existe-t-il un moyen de m'assurer de toujours récupérer une trame de données?

In [1]: import pandas as pd

In [2]: df = pd.DataFrame(data=range(5), index=[1, 2, 3, 3, 3])

In [3]: type(df.loc[3])
Out[3]: pandas.core.frame.DataFrame

In [4]: type(df.loc[1])
Out[4]: pandas.core.series.Series
travailleurs
la source

Réponses:

101

Certes, le comportement est incohérent, mais je pense qu'il est facile d'imaginer des cas où cela est pratique. Quoi qu'il en soit, pour obtenir un DataFrame à chaque fois, passez simplement une liste à loc. Il existe d'autres moyens, mais à mon avis, c'est le plus propre.

In [2]: type(df.loc[[3]])
Out[2]: pandas.core.frame.DataFrame

In [3]: type(df.loc[[1]])
Out[3]: pandas.core.frame.DataFrame
Dan Allan
la source
6
Merci. Il convient de noter que cela renvoie un DataFrame même si l'étiquette n'est pas dans l'index.
jobevers
7
Pour info, avec un index non dupliqué et un indexeur unique (par exemple une seule étiquette), vous récupérerez TOUJOURS une série, c'est uniquement parce que vous avez des doublons dans l'index qu'il s'agit d'un DataFrame.
Jeff
1
Notez qu'il existe un autre piège: si vous utilisez la solution de contournement suggérée, et qu'il n'y a pas de lignes correspondantes, le résultat sera un DataFrame avec une seule ligne, tous NaN.
Paul Oyster
2
Paul, quelle version de pandas utilisez-vous? Sur la dernière version, j'obtiens un KeyErrorquand j'essaye .loc[[nonexistent_label]].
Dan Allan
2
L'utilisation d'une liste dans .locest beaucoup plus lente que sans elle. Pour être toujours lisible mais aussi beaucoup plus rapide, mieux utiliserdf.loc[1:1]
Jonathan
16

Vous avez un index avec trois éléments d'index 3. Pour cette raison df.loc[3]retournera un dataframe.

La raison est que vous ne spécifiez pas la colonne. df.loc[3]Sélectionne donc trois éléments de toutes les colonnes (qui est colonne 0), tandis que df.loc[3,0]retournera une série. Par exemple, df.loc[1:2]renvoie également un dataframe, car vous découpez les lignes.

La sélection d'une seule ligne (as df.loc[1]) renvoie une série avec les noms de colonne comme index.

Si vous voulez être sûr d'avoir toujours un DataFrame, vous pouvez découper comme df.loc[1:1]. Une autre option est l'indexation booléenne ( df.loc[df.index==1]) ou la méthode take ( df.take([0]), mais cet emplacement utilisé n'est pas des étiquettes!).

joris
la source
3
C'est le comportement auquel je m'attendais. Je ne comprends pas la décision de conception de convertir des lignes uniques en série - pourquoi pas un bloc de données avec une ligne?
jobevers
Ah, pourquoi sélectionner une seule ligne renvoie une série, je ne sais pas vraiment.
joris
6

Le TLDR

Lors de l'utilisation loc

df.loc[:]= Dataframe

df.loc[int]= Dataframe si vous avez plus d'une colonne et Series si vous n'avez qu'une seule colonne dans le dataframe

df.loc[:, ["col_name"]]= Dataframe

df.loc[:, "col_name"]= Série

N'utilise pas loc

df["col_name"]= Série

df[["col_name"]]= Dataframe

Colin Anthony
la source
5

Utilisez df['columnName']pour obtenir une série et df[['columnName']]pour obtenir un Dataframe.

user4422
la source
1
Attention, cela prend une copie du df original.
smci
3

Vous avez écrit dans un commentaire à la réponse de Joris:

"Je ne comprends pas la décision de conception de convertir des lignes uniques en série - pourquoi pas un bloc de données avec une ligne?"

Une seule ligne n'est pas convertie en série.
Il EST une série:No, I don't think so, in fact; see the edit

La meilleure façon de penser aux structures de données pandas est de créer des conteneurs flexibles pour des données de moindre dimension. Par exemple, DataFrame est un conteneur pour Series et Panel est un conteneur pour les objets DataFrame. Nous aimerions pouvoir insérer et supprimer des objets de ces conteneurs à la manière d'un dictionnaire.

http://pandas.pydata.org/pandas-docs/stable/overview.html#why-more-than-1-data-structure

Le modèle de données des objets Pandas a été choisi comme ça. La raison réside certainement dans le fait que cela garantit des avantages que je ne connais pas (je ne comprends pas complètement la dernière phrase de la citation, peut-être que c'est la raison)

.

Edit: je ne suis pas d'accord avec moi

Une trame de données ne peut pas être composé d'éléments qui être série, car le code suivant donne le même type « série » et pour une ligne que pour une colonne:

import pandas as pd

df = pd.DataFrame(data=[11,12,13], index=[2, 3, 3])

print '-------- df -------------'
print df

print '\n------- df.loc[2] --------'
print df.loc[2]
print 'type(df.loc[1]) : ',type(df.loc[2])

print '\n--------- df[0] ----------'
print df[0]
print 'type(df[0]) : ',type(df[0])

résultat

-------- df -------------
    0
2  11
3  12
3  13

------- df.loc[2] --------
0    11
Name: 2, dtype: int64
type(df.loc[1]) :  <class 'pandas.core.series.Series'>

--------- df[0] ----------
2    11
3    12
3    13
Name: 0, dtype: int64
type(df[0]) :  <class 'pandas.core.series.Series'>

Donc, il n'y a aucun sens à prétendre qu'un DataFrame est composé de Series parce que ce que cesdites Series seraient supposées être: des colonnes ou des lignes? Question et vision stupides.

.

Alors qu'est-ce qu'un DataFrame?

Dans la version précédente de cette réponse, j'ai posé cette question, en essayant de trouver la réponse à la Why is that?partie de la question du PO et à l'interrogatoire similaire single rows to get converted into a series - why not a data frame with one row?dans l'un de ses commentaires,
alors que la Is there a way to ensure I always get back a data frame?partie a été répondue par Dan Allan.

Ensuite, comme les documents des Pandas cités ci-dessus indiquent que les structures de données des pandas sont mieux vues comme des conteneurs de données de dimension inférieure, il m'a semblé que la compréhension du pourquoi se trouverait dans les caractéristiques de la nature des structures DataFrame.

Cependant, j'ai réalisé que cet avis cité ne doit pas être considéré comme une description précise de la nature des structures de données de Pandas.
Ce conseil ne signifie pas qu'un DataFrame est un conteneur de Series.
Il exprime que la représentation mentale d'un DataFrame comme conteneur de Series (soit des lignes, soit des colonnes selon l'option considérée à un moment d'un raisonnement) est un bon moyen de considérer les DataFrames, même si ce n'est pas strictement le cas dans la réalité. «Bon» signifie que cette vision permet d'utiliser les DataFrames avec efficacité. C'est tout.

.

Alors qu'est-ce qu'un objet DataFrame?

La classe DataFrame produit des instances qui ont une structure particulière issue de la classe de base NDFrame , elle-même dérivée de la classe de base PandasContainer qui est également une classe parente de la classe Series .
Notez que cela est correct pour Pandas jusqu'à la version 0.12. Dans la prochaine version 0.13, Series dérivera également de la classe NDFrame uniquement.

# with pandas 0.12

from pandas import Series
print 'Series  :\n',Series
print 'Series.__bases__  :\n',Series.__bases__

from pandas import DataFrame
print '\nDataFrame  :\n',DataFrame
print 'DataFrame.__bases__  :\n',DataFrame.__bases__

print '\n-------------------'

from pandas.core.generic import NDFrame
print '\nNDFrame.__bases__  :\n',NDFrame.__bases__

from pandas.core.generic import PandasContainer
print '\nPandasContainer.__bases__  :\n',PandasContainer.__bases__

from pandas.core.base import PandasObject
print '\nPandasObject.__bases__  :\n',PandasObject.__bases__

from pandas.core.base import StringMixin
print '\nStringMixin.__bases__  :\n',StringMixin.__bases__

résultat

Series  :
<class 'pandas.core.series.Series'>
Series.__bases__  :
(<class 'pandas.core.generic.PandasContainer'>, <type 'numpy.ndarray'>)

DataFrame  :
<class 'pandas.core.frame.DataFrame'>
DataFrame.__bases__  :
(<class 'pandas.core.generic.NDFrame'>,)

-------------------

NDFrame.__bases__  :
(<class 'pandas.core.generic.PandasContainer'>,)

PandasContainer.__bases__  :
(<class 'pandas.core.base.PandasObject'>,)

PandasObject.__bases__  :
(<class 'pandas.core.base.StringMixin'>,)

StringMixin.__bases__  :
(<type 'object'>,)

Donc, je crois comprendre maintenant qu'une instance de DataFrame a certaines méthodes qui ont été conçues afin de contrôler la façon dont les données sont extraites des lignes et des colonnes.

Le fonctionnement de ces méthodes d'extraction est décrit dans cette page: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing
On y retrouve la méthode donnée par Dan Allan et d'autres méthodes.

Pourquoi ces méthodes d'extraction ont-elles été élaborées telles quelles?
C'est certainement parce qu'ils ont été évalués comme ceux offrant les meilleures possibilités et la facilité d'analyse des données.
C'est précisément ce qui est exprimé dans cette phrase:

La meilleure façon de penser aux structures de données pandas est de créer des conteneurs flexibles pour des données de moindre dimension.

Le pourquoi de l'extraction des données d'une instance DataFRame ne réside pas dans sa structure, il réside dans le pourquoi de cette structure. Je suppose que la structure et le fonctionnement de la structure de données des Pandas ont été ciselés afin d'être le plus intellectuellement intuitif possible, et que pour comprendre les détails, il faut lire le blog de Wes McKinney.

eyquem
la source
1
Pour info, DataFrame n'est PAS une sous-classe ndarray, ni une série (à partir de 0,13, avant cela, c'était bien). Ce sont plus des dict-like que n'importe quoi.
Jeff
Merci de m'informer. J'apprécie vraiment parce que je suis nouveau dans l'apprentissage des pandas. Mais j'ai besoin de plus d'informations pour bien comprendre. Pourquoi est-il écrit dans la documentation qu'une série est une sous-classe de ndarray?
eyquem
c'était avant la 0.13 (sortie sous peu), voici les documents de développement: pandas.pydata.org/pandas-docs/dev/dsintro.html#series
Jeff
D'ACCORD. Merci beaucoup. Cependant, cela ne change pas la base de mon raisonnement et de ma compréhension, n'est-ce pas? - Dans Pandas inférieurs à 0.13, DataFrame et autres objets de Pandas différents de Series: de quoi sont-ils sous-classe?
eyquem
@Jeff Merci. J'ai modifié ma réponse après vos informations. Je serais ravi de savoir ce que vous pensez de mon montage.
eyquem
1

Si l'objectif est d'obtenir un sous-ensemble de l'ensemble de données à l'aide de l'index, il est préférable d'éviter d'utiliser locou iloc. Au lieu de cela, vous devez utiliser une syntaxe similaire à celle-ci:

df = pd.DataFrame(data=range(5), index=[1, 2, 3, 3, 3])
result = df[df.index == 3] 
isinstance(result, pd.DataFrame) # True

result = df[df.index == 1]
isinstance(result, pd.DataFrame) # True
Ajit
la source
0

Si vous sélectionnez également sur l'index de la trame de données, le résultat peut être une trame de données ou d' une série ou il peut être une série ou un scalaire (valeur unique).

Cette fonction garantit que vous obtenez toujours une liste de votre sélection (si le df, l'index et la colonne sont valides):

def get_list_from_df_column(df, index, column):
    df_or_series = df.loc[index,[column]] 
    # df.loc[index,column] is also possible and returns a series or a scalar
    if isinstance(df_or_series, pd.Series):
        resulting_list = df_or_series.tolist() #get list from series
    else:
        resulting_list = df_or_series[column].tolist() 
        # use the column key to get a series from the dataframe
    return(resulting_list)
Wouter
la source