Empêcher la coercition des trames de données pandas lors de l'indexation et de l'insertion de lignes

16

Je travaille avec des lignes individuelles de trames de données pandas, mais je trébuche sur des problèmes de coercition lors de l'indexation et de l'insertion de lignes. Les pandas semblent toujours vouloir contraindre d'un type mixte int / flottant à tous les types flottants, et je ne vois aucun contrôle évident sur ce comportement.

Par exemple, voici une simple trame de données avec acomme intet bcomme float:

import pandas as pd
pd.__version__  # '0.25.2'

df = pd.DataFrame({'a': [1], 'b': [2.2]})
print(df)
#    a    b
# 0  1  2.2
print(df.dtypes)
# a      int64
# b    float64
# dtype: object

Voici un problème de coercition lors de l'indexation d'une ligne:

print(df.loc[0])
# a    1.0
# b    2.2
# Name: 0, dtype: float64
print(dict(df.loc[0]))
# {'a': 1.0, 'b': 2.2}

Et voici un problème de coercition lors de l'insertion d'une ligne:

df.loc[1] = {'a': 5, 'b': 4.4}
print(df)
#      a    b
# 0  1.0  2.2
# 1  5.0  4.4
print(df.dtypes)
# a    float64
# b    float64
# dtype: object

Dans les deux cas, je souhaite que la acolonne reste en tant que type entier, plutôt que d'être contrainte à un type flottant.

Mike T
la source
J'ai trouvé cela , mais je n'ai pas pu trouver si le problème était effectivement résolu. En attendant, je suppose que vous pourriez faire:df.loc[[0], df.columns]
Dani Mesejo
On dirait que pd.DataFrame ne prend pas en charge le mélange de types lors de l'instanciation? pandas.pydata.org/pandas-docs/stable/reference/api/… dtype param ne prend en charge qu'un seul type. .read_[type]prend en charge plusieurs dtypes si ...
Quentin

Réponses:

4

Après quelques recherches, voici quelques solutions de contournement terriblement laides. (Une meilleure réponse sera acceptée.)

Une bizarrerie trouvée ici est que les colonnes non numériques arrêtent la coercition, alors voici comment indexer une ligne sur a dict:

dict(df.assign(_='').loc[0].drop('_', axis=0))
# {'a': 1, 'b': 2.2}

Et l'insertion d'une ligne peut être effectuée en créant un nouveau bloc de données avec une ligne:

df = df.append(pd.DataFrame({'a': 5, 'b': 4.4}, index=[1]))
print(df)
#    a    b
# 0  1  2.2
# 1  5  4.4

Ces deux astuces ne sont pas optimisées pour les trames de données volumineuses, j'apprécierais donc grandement une meilleure réponse!

Mike T
la source
Vous pouvez toujours simplement forcer l'ajout de post df['a'] = df.a.astype(mytype)... C'est quand même sale et probablement pas efficace.
Quentin
.astype()est dangereux pour float -> integer; il n'a aucun problème à se changer 1.1en 1, vous devez donc vraiment vous assurer que toutes vos valeurs sont de type entier avant de le faire. Il est probablement préférable d'utiliser pd.to_numericavecdowncast='integer'
ALollz
2

La racine du problème est que

  1. L'indexation de la base de données des pandas renvoie une série de pandas

On peut voir ça:

type(df.loc[0])
# pandas.core.series.Series

Et une série ne peut avoir qu'un seul type, dans votre cas, int64 ou float64.

Deux solutions de contournement me viennent à l'esprit:

print(df.loc[[0]])
# this will return a dataframe instead of series
# so the result will be
#    a    b
# 0  1  2.2

# but the dictionary is hard to read
print(dict(df.loc[[0]]))
# {'a': 0    1
# Name: a, dtype: int64, 'b': 0    2.2
# Name: b, dtype: float64}

ou

print(df.astype(object).loc[0])
# this will change the type of value to object first and then print
# so the result will be
# a      1
# b    2.2
# Name: 0, dtype: object

print(dict(df.astype(object).loc[0]))
# in this way the dictionary is as expected
# {'a': 1, 'b': 2.2}
  1. Lorsque vous ajoutez un dictionnaire à une trame de données, il convertit d'abord le dictionnaire en série , puis ajoute. (Donc, le même problème se reproduit)

https://github.com/pandas-dev/pandas/blob/master/pandas/core/frame.py#L6973

if isinstance(other, dict):
    other = Series(other)

Donc, votre solution de contournement est en fait solide, sinon nous pourrions:

df.append(pd.Series({'a': 5, 'b': 4.4}, dtype=object, name=1))
#    a    b
# 0  1  2.2
# 1  5  4.4
Hongpei
la source
Bonne idée d'utiliser objectdes types de données! Un autre est de créer un objet DataFrame depuis le début:df = pd.DataFrame({'a': [1], 'b': [2.2]}, dtype=object)
Mike T
2

Chaque fois que vous obtenez des données d'une trame de données ou que vous ajoutez des données à une trame de données et que vous devez conserver le même type de données, évitez la conversion vers d'autres structures internes qui ne connaissent pas les types de données nécessaires.

Lorsque vous le df.loc[0]convertissez pd.Series,

>>> type(df.loc[0])
<class 'pandas.core.series.Series'>

Et maintenant, Seriesn'en aura qu'un seul dtype. Ainsi contraignant intà float.

Au lieu de cela, conservez la structure pd.DataFrame,

>>> type(df.loc[[0]])
<class 'pandas.core.frame.DataFrame'>

Sélectionnez la ligne requise comme cadre, puis convertissez-la en dict

>>> df.loc[[0]].to_dict(orient='records')
[{'a': 1, 'b': 2.2}]

De même, pour ajouter une nouvelle ligne, utilisez la pd.DataFrame.appendfonction pandas ,

>>> df = df.append([{'a': 5, 'b': 4.4}]) # NOTE: To append as a row, use []
   a    b
0  1  2.2
0  5  4.4

Ce qui précède ne provoquera pas de conversion de type,

>>> df.dtypes
a      int64
b    float64
dtype: object
Vishnudev
la source
Wow a dû lire ce deuxième bloc de code trois fois pour l'obtenir. C'est très subtil. C'est bien mieux que ce que j'ai fait dans le passé ... parcourez la trame de données finale et réaffectez les valeurs avec le type de données correct (oui, ce que j'ai fait est une horrible solution qui ne va vraiment pas évoluer.).
VanBantam
1
Oh. Heureux que cela ait aidé 😊 @VanBantam
Vishnudev
1

Une approche différente avec de légères manipulations de données:

Supposons que vous ayez une liste de dictionnaires (ou cadres de données)

lod=[{'a': [1], 'b': [2.2]}, {'a': [5], 'b': [4.4]}]

où chaque dictionnaire représente une ligne (notez les listes dans le deuxième dictionnaire). Ensuite, vous pouvez facilement créer une trame de données via:

pd.concat([pd.DataFrame(dct) for dct in lod])
   a    b
0  1  2.2
0  5  4.4

et vous gérez les types de colonnes. Voir concat

Donc, si vous avez une trame de données et une liste de dict, vous pouvez simplement utiliser

pd.concat([df] + [pd.DataFrame(dct) for dct in lod])
Quickbeam2k1
la source
0

Dans le premier cas, vous pouvez travailler avec le type de données entier nullable . La sélection de la série n'est pas contrainte floatet les valeurs sont placées dans un objectconteneur. Le dictionnaire est ensuite correctement créé, avec la valeur sous-jacente stockée en tant que np.int64.

df = pd.DataFrame({'a': [1], 'b': [2.2]})
df['a'] = df['a'].astype('Int64')

d = dict(df.loc[0])
#{'a': 1, 'b': 2.2}

type(d['a'])
#numpy.int64

Avec votre syntaxe, cela fonctionne presque aussi pour le deuxième cas, mais cela se transforme en object, donc pas génial:

df.loc[1] = {'a': 5, 'b': 4.4}
#   a    b
#0  1  2.2
#1  5  4.4

df.dtypes
#a     object
#b    float64
#dtype: object

Cependant, nous pouvons apporter une petite modification à la syntaxe pour ajouter une ligne à la fin (avec un RangeIndex) et maintenant les types sont traités correctement.

df = pd.DataFrame({'a': [1], 'b': [2.2]})
df['a'] = df['a'].astype('Int64')

df.loc[df.shape[0], :] = [5, 4.4]
#   a    b
#0  1  2.2
#1  5  4.4

df.dtypes
#a      Int64
#b    float64
#dtype: object
ALollz
la source