Экспорт ints с отсутствующими значениями в csv в Pandas

При сохранении Pandas DataFrame в csv некоторые целые числа преобразуются в поплавки. Это происходит, когда столбец с поплавками имеет отсутствующие значения (np.nan).

Есть ли простой способ избежать этого? (Особенно в автоматическом режиме - я часто имею дело со многими столбцами различных типов данных.)

Например

import pandas as pd
import numpy as np
df = pd.DataFrame([[1,2],[3,np.nan],[5,6]],
                  columns=["a","b"],
                  index=["i_1","i_2","i_3"])
df.to_csv("file.csv")

дает

,a,b
i_1,1,2.0
i_2,3,
i_3,5,6.0

То, что я хотел бы получить,

,a,b
i_1,1,2
i_2,3,
i_3,5,6

EDIT: я полностью осведомлен о поддержке целых NA - Pandas Предостережений и Gotchas. Вопрос в том, что является хорошим обходным решением (особенно в случае, если имеется много других столбцов разных типов, и я заранее не знаю, какие столбцы "целочисленные" имеют отсутствующие значения).

Ответы

Ответ 1

Использование float_format = '%.12g' внутри функции to_csv разрешило аналогичную проблему для меня. Он хранит десятичные знаки для законных поплавков с 12 значащими цифрами, но бросает их для того, чтобы ints был вынужден плавать при наличии NaN's:

In [4]: df
Out[4]: 
     a    b
i_1  1    2.0
i_2  3    NaN
i_3  5.9  6.0

In [5]: df.to_csv('file.csv', float_format = '%.12g')

Выход:

   , a,  b
i_1, 1,  2
i_2, 3, 
i_3, 5.9, 6

Ответ 2

Этот фрагмент делает то, что вы хотите, и должен быть относительно эффективным при этом.

import numpy as np
import pandas as pd

EPSILON = 1e-9

def _lost_precision(s):
    """
    The total amount of precision lost over Series `s`
    during conversion to int64 dtype
    """
    try:
        return (s - s.fillna(0).astype(np.int64)).sum()
    except ValueError:
        return np.nan

def _nansafe_integer_convert(s):
    """
    Convert Series `s` to an object type with `np.nan`
    represented as an empty string ""
    """
    if _lost_precision(s) < EPSILON:
        # Here where the magic happens
        as_object = s.fillna(0).astype(np.int64).astype(np.object)
        as_object[s.isnull()] = ""
        return as_object
    else:
        return s


def nansafe_to_csv(df, *args, **kwargs):
    """
    Write `df` to a csv file, allowing for missing values
    in integer columns

    Uses `_lost_precision` to test whether a column can be
    converted to an integer data type without losing precision.
    Missing values in integer columns are represented as empty
    fields in the resulting csv.
    """
    df.apply(_nansafe_integer_convert).to_csv(*args, **kwargs)

Мы можем проверить это с помощью простого DataFrame, который должен охватывать все базы:

In [75]: df = pd.DataFrame([[1,2, 3.1, "i"],[3,np.nan, 4.0, "j"],[5,6, 7.1, "k"]]
                  columns=["a","b", "c", "d"],
                  index=["i_1","i_2","i_3"])
In [76]: df
Out[76]: 
     a   b    c  d
i_1  1   2  3.1  i
i_2  3 NaN  4.0  j
i_3  5   6  7.1  k

In [77]: nansafe_to_csv(df, 'deleteme.csv', index=False)

Создает следующий csv файл:

a,b,c,d
1,2,3.1,i
3,,4.0,j
5,6,7.1,k

Ответ 3

Я расширяю образцы данных здесь, чтобы надеяться убедиться, что это обрабатывает ситуации, с которыми вы имеете дело:

df = pd.DataFrame([[1.1,2,9.9,44,1.0],
                   [3.3,np.nan,4.4,22,3.0],
                   [5.5,8,np.nan,66,4.0]],
                  columns=list('abcde'),
                  index=["i_1","i_2","i_3"])

       a   b    c   d  e
i_1  1.1   2  9.9  44  1
i_2  3.3 NaN  4.4  22  3
i_3  5.5   8  NaN  66  4

df.dtypes

a    float64
b    float64
c    float64
d      int64
e    float64

Я думаю, что если вы хотите общее решение, оно должно быть явно закодировано из-за pandas, не позволяя NaNs в столбцах int. То, что я делаю ниже, - это проверка значений целых чисел (поскольку мы не можем действительно проверить тип, поскольку они будут перепрограммированы для float, если они содержат NaN), и если это целочисленное значение, то преобразование в строковый формат, а также преобразование 'NAN' до '' (пустой). Конечно, это не то, как вы хотите хранить целые числа, за исключением последнего шага перед выводом.

for col in df.columns:
    if any( df[col].isnull() ):
        tmp = df[col][ df[col].notnull() ]
        if all( tmp.astype(int).astype(float) == tmp.astype(float) ):
            df[col] = df[col].map('{:.0F}'.format).replace('NAN','')

df.to_csv('x.csv')

Вот выходной файл, а также то, на что он похож, если вы прочитали его обратно в pandas, хотя цель этого, по-видимому, читать его в другие числовые пакеты.

%more x.csv

,a,b,c,d,e
i_1,1.1,2,9.9,44,1.0
i_2,3.3,,4.4,22,3.0
i_3,5.5,8,,66,4.0

pd.read_csv('x.csv')

  Unnamed: 0    a   b    c   d  e
0        i_1  1.1   2  9.9  44  1
1        i_2  3.3 NaN  4.4  22  3
2        i_3  5.5   8  NaN  66  4

Ответ 4

@EdChum предложение - комментарий хороший, вы также можете использовать аргумент float_format (см. docs)

In [28]: a
Out[28]: 
   a   b
0  0   1
1  1 NaN
2  2   3
In [31]: a.to_csv(r'c:\x.csv', float_format = '%.0f')

Выдает:

,a,b
0,0,1
1,1,
2,2,3