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.
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!
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
La racine du problème est que
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}
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
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])
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