Evitar a coerção dos quadros de dados do pandas ao indexar e inserir linhas

16

Estou trabalhando com linhas individuais de quadros de dados de pandas, mas tropeço em questões de coerção ao indexar e inserir linhas. Pandas parece sempre querer coagir de um tipo misto de int / float para all-float, e não consigo ver nenhum controle óbvio sobre esse comportamento.

Por exemplo, aqui está um quadro de dados simples com aas inte 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

Aqui está um problema de coerção ao indexar uma linha:

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}

E aqui está um problema de coerção ao inserir uma linha:

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

Nos dois casos, quero que a acoluna permaneça como um tipo inteiro, em vez de ser coagida a um tipo de flutuação.

Mike T
fonte
Encontrei isso , mas não consegui encontrar se efetivamente o problema foi resolvido. Enquanto isso, acho que você poderia fazer:df.loc[[0], df.columns]
Dani Mesejo 23/10/19
Parece que o pd.DataFrame não suporta mixagem de tipos na instanciação? pandas.pydata.org/pandas-docs/stable/reference/api/… dtype param suporta apenas um único tipo. .read_[type]embora suporte a vários tipos ...
Quentin

Respostas:

4

Após algumas pesquisas, aqui estão algumas soluções terrivelmente feias. (Uma resposta melhor será aceita.)

Uma peculiaridade encontrada aqui é que as colunas não numéricas interrompem a coerção, e aqui está como indexar uma linha para dict:

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

E a inserção de uma linha pode ser feita criando um novo quadro de dados com uma linha:

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 os truques não são otimizados para grandes quadros de dados, então eu apreciaria muito uma resposta melhor!

Mike T
fonte
Você sempre pode coagir o post append df['a'] = df.a.astype(mytype)... Ainda está sujo e provavelmente não é eficiente.
Quentin
.astype()é perigoso para float -> inteiro; não há nenhum problema 1.1para mudar para 1, então você realmente precisa ter certeza de que todos os seus valores são semelhantes a números inteiros antes de fazê-lo. Provavelmente melhor para usar pd.to_numericcomdowncast='integer'
ALollz
2

A raiz do problema é que

  1. A indexação do dataframe do pandas retorna uma série de pandas

Nós podemos ver isso:

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

E uma série pode ter apenas um tipo, no seu caso, int64 ou float64.

Há duas soluções alternativas que vêm à minha cabeça:

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. Quando você anexa um dicionário a um quadro de dados, ele converte o dicionário em uma Série primeiro e depois anexa. (Então o mesmo problema acontece novamente)

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

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

Portanto, sua solução é realmente sólida, caso contrário, poderí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
fonte
Boa idéia para usar objecttipos de dados! Outra é criar um objeto DataFrame desde o início:df = pd.DataFrame({'a': [1], 'b': [2.2]}, dtype=object)
Mike T
2

Sempre que você estiver obtendo dados do dataframe ou anexando dados a um dataframe e precisar manter o mesmo tipo de dados, evite a conversão para outras estruturas internas que não estão cientes dos tipos de dados necessários.

Quando você faz df.loc[0]a conversão para pd.Series,

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

E agora, Seriessó terá um single dtype. Coagindo assim inta float.

Em vez disso, mantenha a estrutura como pd.DataFrame,

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

Selecione a linha necessária como um quadro e depois converta para dict

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

Da mesma forma, para adicionar uma nova linha, use a pd.DataFrame.appendfunção 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

O acima não causará conversão de tipo,

>>> df.dtypes
a      int64
b    float64
dtype: object
Vishnudev
fonte
Wow teve que ler o segundo bloco de código três vezes para obtê-lo. Isso é muito sutil. Isso é muito melhor do que o que eu fiz no passado ... percorre o dataframe final e reatribui os valores com o tipo de dados correto (sim, o que eu fiz é uma solução horrível que realmente não será dimensionada.).
VanBantam
11
Oh. Ainda bem que ajudou 😊 @VanBantam
Vishnudev
1

Uma abordagem diferente com pequenas manipulações de dados:

Suponha que você tenha uma lista de dicionários (ou quadros de dados)

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

onde cada dicionário representa uma linha (observe as listas no segundo dicionário). Em seguida, você pode criar um quadro de dados facilmente através de:

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

e você mantém os tipos das colunas. Veja concat

Então, se você tem um quadro de dados e uma lista de dictos, você pode simplesmente usar

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

No primeiro caso, você pode trabalhar com o tipo de dados inteiro nulo . A seleção de séries não é coerente floate os valores são colocados em um objectcontêiner. O dicionário é criado corretamente, com o valor subjacente armazenado 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

Com sua sintaxe, isso quase funciona também para o segundo caso, mas é compatível com object, então não é ótimo:

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

No entanto, podemos fazer uma pequena alteração na sintaxe para adicionar uma linha no final (com um RangeIndex) e agora os tipos são tratados adequadamente.

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
fonte