Evite la coerción de marcos de datos de pandas mientras indexa e inserta filas

16

Estoy trabajando con filas individuales de marcos de datos de pandas, pero me encuentro con problemas de coerción al indexar e insertar filas. Parece que Pandas siempre quiere coaccionar de un tipo mixto int / float a todo tipo float, y no puedo ver ningún control obvio sobre este comportamiento.

Por ejemplo, aquí hay un marco de datos simple con aas inty bcomo 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

Aquí hay un problema de coerción al indexar una fila:

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}

Y aquí hay un problema de coerción al insertar una fila:

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

En ambos casos, quiero que la acolumna permanezca como un tipo entero, en lugar de ser forzada a un tipo flotante.

Mike T
fuente
Encontré esto , pero no pude encontrar si efectivamente se resolvió el problema. Mientras tanto, supongo que podrías hacer:df.loc[[0], df.columns]
Dani Mesejo el
Suena como pd.DataFrame no admite la mezcla de tipos en la instanciación? pandas.pydata.org/pandas-docs/stable/reference/api/… dtype param solo admite un solo tipo. .read_[type]aunque admite varios tipos de dty ...
Quentin

Respuestas:

4

Después de excavar un poco, aquí hay algunas soluciones terriblemente feas. (Se aceptará una mejor respuesta).

Una peculiaridad que se encuentra aquí es que las columnas no numéricas detienen la coerción, por lo que aquí se explica cómo indexar una fila a dict:

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

E insertar una fila se puede hacer creando un nuevo marco de datos con una fila:

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

Ambos trucos no están optimizados para grandes marcos de datos, por lo que agradecería mucho una mejor respuesta.

Mike T
fuente
Siempre puedes coaccionar la publicación anexar df['a'] = df.a.astype(mytype)... Sin embargo, todavía está sucio y probablemente no sea eficiente.
Quentin
.astype()es peligroso para flotar -> entero; no tiene ningún problema 1.1para cambiar 1, por lo que realmente debe asegurarse de que todos sus valores sean 'enteros' antes de hacerlo. Probablemente la mejor manera de usar pd.to_numericcondowncast='integer'
ALollz
2

La raíz del problema es que

  1. La indexación del marco de datos de pandas devuelve una serie de pandas

Podemos ver eso:

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

Y una serie solo puede tener un tipo de letra, en su caso int64 o float64.

Se me ocurren dos soluciones:

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}

o

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. Cuando agrega un diccionario a un marco de datos, primero convertirá el diccionario a una Serie y luego lo agregará. (Entonces el mismo problema vuelve a ocurrir)

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

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

Entonces, su recorrido es realmente sólido, o de lo contrario podríamos:

df.append(pd.Series({'a': 5, 'b': 4.4}, dtype=object, name=1))
#    a    b
# 0  1  2.2
# 1  5  4.4
Hongpei
fuente
¡Buena idea usar objecttipos de datos! Otra es crear un objeto DataFrame desde el principio:df = pd.DataFrame({'a': [1], 'b': [2.2]}, dtype=object)
Mike T
2

Siempre que obtenga datos de un marco de datos o anexe datos a un marco de datos y necesite mantener el mismo tipo de datos, evite la conversión a otras estructuras internas que no conozcan los tipos de datos necesarios.

Cuando lo haces df.loc[0]se convierte a pd.Series,

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

Y ahora, Seriessolo tendrá uno dtype. Por lo tanto coaccionar inta float.

En cambio, mantenga la estructura como pd.DataFrame,

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

Seleccione la fila necesaria como marco y luego convierta a dict

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

Del mismo modo, para agregar una nueva fila, use la pd.DataFrame.appendfunción 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

Lo anterior no causará conversión de tipo,

>>> df.dtypes
a      int64
b    float64
dtype: object
Vishnudev
fuente
Wow tuvo que leer ese segundo bloque de código tres veces para obtenerlo. Eso es muy sutil. Esto es mucho mejor que lo que he hecho en el pasado ... recorrer el marco de datos final y reasignar los valores con el tipo de datos correcto (sí, lo que hice es una solución horrible que realmente no escalará).
VanBantam
1
Oh. Me alegra que haya ayudado 😊 @VanBantam
Vishnudev
1

Un enfoque diferente con ligeras manipulaciones de datos:

Suponga que tiene una lista de diccionarios (o marcos de datos)

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

donde cada diccionario representa una fila (observe las listas en el segundo diccionario). Entonces puede crear un marco de datos fácilmente a través de:

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

y mantienes los tipos de las columnas. Ver concat

Entonces, si tiene un marco de datos y una lista de dictos, simplemente puede usar

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

En el primer caso, puede trabajar con el tipo de datos entero anulable . La selección de Series no coacciona floaty los valores se colocan en un objectcontenedor. El diccionario se crea correctamente, con el valor subyacente almacenado como a 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

Con su sintaxis, esto casi funciona también para el segundo caso, pero esto aumenta object, así que no es genial:

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

Sin embargo, podemos hacer un pequeño cambio en la sintaxis para agregar una fila al final (con un RangeIndex) y ahora los tipos se tratan correctamente.

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
fuente