让pandas.read_csv把空字段读成NaN,把空字符串读成空字符串

7 人关注

This is quite the opposite of 让pandas.read_csv把空值读成空字符串而不是nan

Given the following CSV file:

col,val
there",1
\f\,3
"""hi""",5

我希望它能被解读为。

         col  val
0  hi\nthere    1
1        NaN    2
2        \f\    3
3               4
4       "hi"    5

也就是说,将空字段(val 2)读为NaN,而将空字符串(val 4)保持为空字符串。

目前,pd.read_csv将值2和值4都转换为NaN,或者如果我使用na_filter=False,两者都被保留为空字符串。

我想这两种表示方法在CSV中的含义是不同的(空字段与空字符串),所以我想pandas也应该能够区分这一点。

有没有办法让pandas区分这两种情况?还是说我的假设是错误的,这两种表现形式实际上是一样的?(如果是第二种情况,请指给我一个CSV标准)。

更多信息,我通过导出BigQuery表(具有预期的含义,值2为空,值4为空字符串)到CSV中得到CSV。而我想得到完全相同的表。 因此,这个例子不仅仅是一个伪造的例子,而是BigQuery在导出到CSV时实际使用的。

编辑:进一步搜索发现了一个Github问题 4年之前讨论了类似的观点(见this comment例如),其中一位评论者提到有一些强迫现象(我不确定他们指的是什么,但我理解为空字段和空字符串之间的强迫现象)。这种情况还在发生吗?

5 个评论
如果你使用csv模块,并打印出每一行,两者之间是否有区别?这将告诉你pandas是否有机会以不同的方式读取它们。
也许这相当于我的问题,因为pandas在引擎下使用csv模块:"如何让csv模块以不同的方式读取空字段和空字符串?"(注意,我还没有试过)
cs95
你使用的是什么版本的pandas?这在0.25版本上可以正常工作。
我在 Ubuntu 上使用 0.25.1, Python 3。你能解释一下你说的按预期工作是什么意思吗?就像在我的问题中所说的,它能按我的要求工作?
你的问题有一部分是不标准的,那就是你想混合列数据类型。 NaN 是一个浮点值;所以空字符串不应该是 NaN 。我完全理解这种行为的许多用处--另一方面,最好是以一种更明确的方式来实现它(而且图书馆更有可能支持):有另一列告诉你另一列是否为空。这将适用于任何数据类型,并避免使用你的领域的有效值(空字符串)作为null的指标。
python
pandas
csv
justhalf
justhalf
发布于 2019-11-29
4 个回答
mjspier
mjspier
发布于 2019-12-09
0 人赞同

另一个选择是禁用引号,以获得存在空字符串的字段和不存在空字符串的字段。在这种情况下,问题在于测试中包含新行字符的条目。我们需要首先删除这些字符,然后合并这些行来创建一个新的数据文件。

当读取新的数据文件时,在关闭引号的情况下,空值为NaN,空字符串为双引号。然后这个数据框可以用来设置原始数据框中的NaN,以设置真正的NaN。

import numpy as np
import pandas as pd
with open('./data.csv') as f:
    lines = f.readlines()
# merge lines where the comma is missing
it = iter(lines)
lines2 = [x if ',' in x else x + next(it) for x in it]
# replace \n which are not at the end of the line
lines3 = [l.replace('\n','') + '\n' for l in lines2]
# write new file with merged lines
with open('./data_merged.csv', 'w+') as f:
    f.writelines(lines3)
# read original data
df = pd.read_csv('./data.csv', na_filter=False)
# read merged lines data with quoting off
df_merged = pd.read_csv('./data_merged.csv', quoting=3)
# in df_merged dataframe if is NaN it is a real NaN
# set lines in original df to NaN when in df_merged is NaN
df.loc[df_merged.col.isna(), 'col'] = np.NaN
    
很酷的技巧。这似乎确实解决了问题,但代价是两倍的文件大小和两倍的读取时间。我仍然喜欢一个不需要写入数据副本的解决方案,但这似乎是迄今为止最好的竞争者谢谢mjspier。
这似乎是到目前为止最好的答案(保留换行而不是替换它们),尽管我仍在寻找更好的解决方案。
事实上,我认为这个解决方案可能有一个大问题。当文本中出现逗号时,行与行之间的合并可能不正确。我认为在一行的开头替换空字符串 "" 可能是一个更稳定的解决方案。
在这方面你是对的,但我认为试图正确处理引号CSV中的换行符本身已经是一个CSV分析器了,所以在这一点上,最好的答案仍然是 "使用QUOTE_NONE,对换行符进行一些处理"。由于你的答案是第一个,所以我把+50给了你。
amanb
amanb
发布于 2019-12-09
0 人赞同

pandas.read_csv 接受一个 quoting 参数,用于控制每个字段的引用行为。该参数接受类型为 int csv.QUOTE_* 的值。后者是csv模块中定义的常数。在所有可用的选项中,需要注意的是 csv.QUOTE_NONE .这个常数指示了 读者 对象不对引号字符进行特殊处理,这意味着双引号中的字段会被原样读取,解析时不会在字段中添加额外的双引号。pandas设置的默认值是 csv.QUOTE_MINIMAL .

In [237]: import csv
In [238]: import pandas as pd
In [239]: df = pd.read_csv("test.csv", quoting=csv.QUOTE_NONE)
In [240]: df
Out[240]: 
        col  val
0       "hi  NaN
1    there"  1.0
2       NaN  2.0
3       \f\  3.0
4        ""  4.0
5  """hi"""  5.0

在没有特殊引号的情况下,空值被解析为NaN,带有双引号的空字符串则保持原样。

但是这种方法有一个问题:如果任何字段包含双引号中的换行符,它们会被当作独立的字符串。这在csv文件的第一行很明显,"hi\nthere "被pandas解析为独立的行。为了解决这个问题,我首先用re模块进行了一些预处理。这需要将双引号字符串中的任何换行符替换为白边。然后我写回同一个文件,并像上面一样在read_csv中再次使用。由于我不知道你的数据的完整格式,可能会有更多的必要的铰链要求。然而,对于给定的问题,我得到了想要的输出。

In [314]: with open("test.csv", 'r+') as f:
     ...:     data = f.read()
     ...:     import re
     ...:     pattern = re.compile(r'".*?"', re.DOTALL)
     ...:     data = pattern.sub(lambda x: x.group().replace('\n', ' '), data)
     ...:     f.seek(0)
     ...:     f.write(data)
In [315]: df = pd.read_csv("test.csv", quoting=csv.QUOTE_NONE)
In [316]: df
Out[316]: 
          col  val
0  "hi there"    1
1         NaN    2
2         \f\    3
3          ""    4
4    """hi"""    5
    
I think 如果你按照 csv 模块文档中的建议,传入一个文件对象,而不是用 newlines='' 打开的路径,你就不会有引号字符串中的换行问题了。即: with open("test.csv", "r+", newlines="") as f pd.read_csv(f, ...)
如果alkasm说的是真的,这将是一个很好的解决方案,只需要对所有字段做后处理,去掉首尾引号,然后用一个双引号替换连续的双引号。
糟了!好吧,你可以先用标准的 csv 模块把它读成一个行的列表,然后再构建数据框架,这样做可以,但你会失去所有其他的 pd.read_csv() 的关键字。
gosuto
gosuto
发布于 2019-12-09
0 人赞同

这里有一个有点丑陋但完整的答案。

import io
import re
import pandas as pd
with open('overflow.csv', 'r') as f:
    with io.StringIO(re.sub(r'(^"",)', "EMPTY_STR,", f.read(), flags=re.MULTILINE)) as ff:
        with io.StringIO(re.sub(r'(,"",)', ",EMPTY_STR,", ff.read(), flags=re.MULTILINE)) as fff:
            with io.StringIO(re.sub(r'(,""$)', ",EMPTY_STR", fff.read(), flags=re.MULTILINE)) as ffff:
                df = pd.read_csv(ffff)
df= df.replace('EMPTY_STR', '')