web-dev-qa-db-fra.com

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

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. Pandas semble 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 un bloc de données simple avec a comme int et b comme 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 colonne a reste en tant que type entier, plutôt que d'être contrainte à un type flottant.

16
Mike T

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 un 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, donc j'apprécierais grandement une meilleure réponse!

4
Mike T

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 faites df.loc[0] il se transforme en pd.Series ,

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

Et maintenant, Series n'aura qu'un seul dtype. Ainsi, contraindre int à float.

Conservez plutôt la structure sous la forme 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 pandas pd.DataFrame.append fonction,

>>> 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
2
Vishnudev

La racine du problème est que

  1. L'indexation de pandas dataframe renvoie une série 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 une série , puis ajoute. (Donc, le même problème se reproduit)

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

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

Votre solution de contournement est donc 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
2
Hongpei

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 un dataframe et une liste de dicts, vous pouvez simplement utiliser

pd.concat([df] + [pd.DataFrame(dct) for dct in lod])
1
Quickbeam2k1

Dans le premier cas, vous pouvez travailler avec le type de données entier nullable . La sélection Series ne contraint pas à float et les valeurs sont placées dans un conteneur object. Le dictionnaire est ensuite correctement créé, avec la valeur sous-jacente stockée sous la forme d'un 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 presque fonctionne 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
0
ALollz