python - Evite la coerción de marcos de datos de pandas mientras indexa e inserta filas
coercion (5)
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
a
como
int
y
b
como
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 columna permanezca como un tipo entero, en lugar de ser forzada a un tipo flotante.
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, así que aquí está cómo indexar una fila a un
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.
En el primer caso, puede trabajar con el
tipo de datos entero anulable
.
La selección de la serie no obliga a
float
y los valores se colocan en un contenedor de
object
.
El diccionario se crea correctamente, con el valor subyacente almacenado como
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 se convierte en una
object
, por lo 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
La raíz del problema es que
- 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}
- 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
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 haces
df.loc[0]
se convierte a
pd.Series
,
>>> type(df.loc[0])
<class ''pandas.core.series.Series''>
Y ahora, la
Series
solo tendrá un tipo único.
Por lo tanto, obligar a
int
a
float
.
En su lugar, mantenga la estructura como pandas.pydata.org/pandas-docs/stable/reference/api/… ,
>>> 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}]
De manera similar, para agregar una nueva fila, use la función pandas
pd.DataFrame.append
,
>>> 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
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])