【问题标题】:Get pandas.read_csv to read empty fields as NaN, and empty strings as empty strings获取 pandas.read_csv 将空字段读取为 NaN,将空字符串读取为空字符串
【发布时间】:2020-03-24 19:09:23
【问题描述】:

这与Get pandas.read_csv to read empty values as empty string instead of nan完全相反

鉴于以下 CSV 文件:

col,val
"hi
there",1
,2
\f\,3
"",4
"""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 将 val 2 和 val 4 都转换为 NaN,或者如果我使用 na_filter=False 两者都保留为空字符串。

我假设这两种表示在 CSV 中意味着不同的东西(空字段与空字符串),所以我假设 pandas 也应该能够区分这一点。

有没有办法让pandas区分这两种情况?还是我的假设是错误的,这两种表示实际上是相同的? (如果是第二种情况,请指出 CSV 标准)

更多信息,我通过将 BigQuery 表(具有预期含义,val 2 为空,val 4 为空字符串)导出到 CSV 来获得 CSV。我想拿回完全相同的桌子。 所以这个例子不仅仅是一个人为的例子,而是 BigQuery 在导出到 CSV 时实际使用的。

编辑:进一步搜索显示 Github issue 4 years ago 讨论了类似的观点(例如,参见 this comment),其中一位评论者提到存在一些强制(我不确定他们指的是什么,但我将其理解为空字段和空字符串之间的强制)。这还在发生吗?

【问题讨论】:

  • 如果你使用 csv 模块,并且打印出每一行,这两者有区别吗?这会告诉你 pandas 是否有机会以不同的方式阅读它们。
  • 也许这相当于我的问题,因为熊猫在后台使用 csv 模块:“如何让 csv 模块以不同的方式读取空字段和空字符串?” (注意,我还没试过)
  • 你用的是什么熊猫版本?这在 0.25 上按预期工作。
  • 我在 Ubuntu 上使用 0.25.1,Python 3。你能澄清一下你所说的按预期工作是什么意思吗?就像我希望它在我的问题中一样工作?
  • 您的问题有一部分不是标准的,即您想混合列数据类型。 NaN 是一个浮点值;所以空字符串不应该是NaN。我完全理解这种行为的许多用例——另一方面,最好以更明确的方式实现这一点(并且库更有可能支持):有另一列告诉你是否不是另一列为空。无论数据类型如何,这都会起作用,并且避免必须使用域的有效值(空字符串)作为 null 的指示符。

标签: python pandas csv


【解决方案1】:

pandas.read_csv 接受一个控制每个字段的引用行为的quoting 参数。该参数接受intcsv.QUOTE_* 类型的值。后者是在 csv 模块中定义的常量。在所有可用选项中,需要注意的是csv.QUOTE_NONE。此常量指示reader 对象不对引号字符执行特殊处理,这意味着双引号中的字段按原样读取,并且在解析时不会向字段添加额外的双引号。 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

【讨论】:

  • 认为如果你传入一个文件对象,而不是像csv 模块文档中推荐的那样使用newlines='' 打开的路径,你将不会有引用字符串中的换行符的问题。 IE。 with open("test.csv", "r+", newlines="") as fpd.read_csv(f, ...).
  • 如果alkasm说的是真的,那将是一个很好的解决方案,只需对所有字段进行后处理,去除首尾引号,然后将连续的双引号替换为一个双引号。
  • 该死!好吧,您可以先使用标准 csv 模块将其读入行列表,然后构建数据框,这可行,但您会丢失所有其他 pd.read_csv() 关键字。
【解决方案2】:

另一个选项是禁用引用以获取存在空字符串且不存在任何内容的字段。这种情况下的问题是测试中包含换行符的条目。我们需要先删除这些字符并合并这些行以创建一个新的数据文件。

读取带引号的新数据文件时,空值是 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。
【解决方案3】:

这是一个有点难看但完整的答案:

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', '')

re.sub() 将空字符串替换为 EMPTY_STR,稍后可以将其替换为实际的空字符串。对于所有三种可能的出现类型(行的开头、中间和和),它必须被调用 3 次。

真正空的单元格被单独留下,实际上被解释为NaN

【讨论】:

  • 使用正则表达式解决方案,我担心它可能不仅会替换预期的解决方案,还会替换其他解决方案,因为字段内的引号也被转义为两个双引号。例如,如果第一个字段是 a,",b,第二个字段是 cd,那么在 CSV 中它将是 "a,"",b",cd,因此您的中间正则表达式匹配这个字段,即使它实际上是字段的一部分。 OTOH,尽管这个答案有其弱点,但我绝对可以检查我的数据是否包含这种结构。 (顺便说一句,我相信应该有一种方法只使用 1 个正则表达式)
【解决方案4】:

在创建 BigQuery csv 导出时,您有什么方法可以用其他内容替换空字符串?喜欢用"EMPTY_STR" 替换""?然后,您可以在使用 .read_csv() 时使用转换器函数将它们替换回空字符串。

【讨论】:

    猜你喜欢
    • 2012-06-07
    • 1970-01-01
    • 2014-03-23
    • 1970-01-01
    • 1970-01-01
    • 2012-06-06
    • 2021-04-24
    • 2021-02-23
    • 1970-01-01
    相关资源
    最近更新 更多