将大型 CSV 文件按组平均拆分为较小的 CSV 文件的更快方法?

Faster way to split a large CSV file evenly by Groups into smaller CSV files?

提问人:GreenGodot 提问时间:7/27/2023 最后编辑:GreenGodot 更新时间:7/28/2023 访问量:239

问:

我相信有更好的方法,但我画的是空白。我有一个这种格式的 CSV 文件。ID 列已排序,因此所有内容至少都组合在一起:

Text                 ID
this is sample text, AAAA
this is sample text, AAAA
this is sample text, AAAA
this is sample text, AAAA
this is sample text, AAAA
this is sample text2, BBBB
this is sample text2, BBBB
this is sample text2, BBBB
this is sample text3, CCCC
this is sample text4, DDDD
this is sample text4, DDDD
this is sample text5, EEEE
this is sample text5, EEEE
this is sample text6, FFFF
this is sample text6, FFFF

我想做的是快速将 CSV 快速拆分为 X 数量的较小 CSV 文件。因此,如果 X==3,那么 AAAA 将进入“1.csv”,BBBB 将进入“2.csv”,CCCC 将进入“3.csv”,下一组将循环返回并进入“1.csv”。

这些组的大小各不相同,因此按数字进行硬编码的拆分在这里不起作用。

有没有比我目前的方法更可靠的方法来可靠地拆分它们,该方法仅在 Python 中使用 Pandas groupby 来编写它们?

    file_ = 0
    num_files = 3

    for name, group in df.groupby(by=['ID'], sort=False):

        file_+=1
        group['File Num'] = file_

        group.to_csv(file_+'.csv',index=False, header=False, mode='a')

        if file_ == num_files:

            file_ = 0

这是一个基于 python 的解决方案,但如果它完成工作,我对使用或 bash 持开放态度。awk

编辑:

为了澄清起见,我希望将组拆分为我可以设置的固定数量的文件。

在本例中,3.(所以 x = 3)。第一组 (AAAA) 将进入 1.csv,第二组进入 2.csv,第三组进入 3.csv,然后对于第四组,它将循环返回并将其插入到 1.csv 中。等。

示例输出 1.csv:

Text                 ID
this is sample text, AAAA
this is sample text, AAAA
this is sample text, AAAA
this is sample text, AAAA
this is sample text, AAAA
this is sample text4, DDDD
this is sample text4, DDDD

示例输出 2.csv:

Text                 ID
this is sample text2, BBBB
this is sample text2, BBBB
this is sample text2, BBBB
this is sample text5, EEEE
this is sample text5, EEEE

示例输出 3.csv:

Text                 ID
this is sample text3, CCCC
this is sample text6, FFFF
this is sample text6, FFFF
Python Pandas CSV 文件 AWK

评论

0赞 RavinderSingh13 7/27/2023
您的列是否始终是排序值?如图所示样本。ID
1赞 GreenGodot 7/27/2023
是的,ID 列已排序。忘了澄清一下。
0赞 mozway 7/27/2023
您的真实数据集中有多少行?对所有提供的答案进行快速测试后,我发现 python 的速度相当快。你能在你的数据集上测试吗?
0赞 Ed Morton 7/27/2023
@mozway我更新了答案中的第一个脚本,以避免持续打开/关闭文件以避免“打开文件过多”错误(我怀疑 python 脚本更快,因为它也没有这样做),因为我认为这个应用程序不需要,请再次尝试速度测试,因为它应该明显快于每次更改时打开/关闭每个文件时的速度最后一个字段。

答:

2赞 RavinderSingh13 7/27/2023 #1

使用您显示的示例,请尝试以下代码。如前所述,考虑到最后一列是根据所示样本排序的。

awk -v x="3" '
BEGIN{
  count=1
  outFile=count".csv"
}
FNR==1{
  print
  next
}
prev!=$NF && prev{
  close(outFile)
  count++
  outFile=count".csv"
}
{
  print >> (outFile)
  prev=$NF
}
x==count{ count=1 }
' Input_file

评论

0赞 GreenGodot 7/27/2023
这看起来像我想要的,唯一缺少的是“所以如果 X==3,那么 AAAA 将进入”1.csv“,BBBB 将进入”2.csv“,CCCC 将进入”3.csv“,下一组将循环返回并进入”1.csv”。
0赞 RavinderSingh13 7/27/2023
@GreenGodot,你能告诉我这里有什么吗?谢谢x==3
0赞 GreenGodot 7/27/2023
我们将组拆分为我可以设置的固定数量的文件。在本例中,3.(所以 x = 3)。第一组进入 1.csv,第二组进入 2.csv,thirs 进入 3.csv,然后对于第四组,它将循环返回并将其插入到 1.csv 中。如果这有意义?
0赞 RavinderSingh13 7/27/2023
@GreenGodot,所以你的意思是你根本不想在你的问题中考虑最后一列?
0赞 RavinderSingh13 7/27/2023
@GreenGodot,您能否检查一下我更新的代码,让我知道它是怎么回事。
3赞 anubhava 7/27/2023 #2

您可以使用以下解决方案:awk

awk -v X=3 '
FNR == 1 {   # save 1st record as header 
   hdr = $0
   next
}
p != $NF {   # ID field changes, move to new output csv file 
   close(fn)
   fn = ((n++ % X) + 1)".csv" # construct new file name
}
!seen[fn]++ {                 # do we need to print header
   print hdr > fn 
}
{
   print >> fn                # append each record to output
   p = $NF                    # save last field in variable p
}' file
0赞 mozway 7/27/2023 #3

使用 groupbyfactorize 模数 ():N

N = 3

for i, g in df.groupby(pd.factorize(df['ID'])[0]%N):
    g.to_csv(f'chunk{i+1}.csv', index=False)

输出文件:

# chunk1.csv
Text,ID
this is sample text,AAAA
this is sample text,AAAA
this is sample text,AAAA
this is sample text,AAAA
this is sample text,AAAA
this is sample text4,DDDD
this is sample text4,DDDD

# chunk2.csv
Text,ID
this is sample text2,BBBB
this is sample text2,BBBB
this is sample text2,BBBB
this is sample text5,EEEE
this is sample text5,EEEE

# chunk3.csv
Text,ID
this is sample text3,CCCC
this is sample text6,FFFF
this is sample text6,FFFF

计时

在 1400 万行上进行了测试:

15.8 s ± 687 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

其中 ~14 秒是由于 I/O

与其他答案的比较(在 shell 中使用):time

# @mozway as a python script including imports and reading the file
real    0m20,834s

# @RavinderSingh13
real    1m22,952s

# @anubhava
real    1m23,790s

# @Ed Morton (updated code, original solution was 2m58,171s)
real    0m8,599s

作为功能:

import pandas as pd

def split_csv(filename, N=3, id_col='ID', out_basename='chunk'):
    df = pd.read_csv(filename)
    for i, g in df.groupby(pd.factorize(df[id_col])[0]%N):
        g.to_csv(f'{out_basename}{i+1}.csv', index=False)

split_csv('my_file.csv', N=3)

评论

0赞 Ed Morton 7/27/2023
anubhavas 脚本的运行速度不可能是我的原始脚本的两倍,因为它们都在执行相同的打开/关闭/写入,但我对每个输入行执行的附加指令更少。它们的性能应该非常接近,而我的性能略快。我修改后的脚本不会在每次输出文件更改时打开/关闭输出文件,这应该会更快。
0赞 Ed Morton 7/27/2023
当我将底部脚本保存在您的答案中并尝试运行它时,我得到以下语法错误:向上箭头指向 的末尾。我没有看到任何明显的问题,并且对 python 的了解不够多,无法对其进行调试。我正在运行,所以也许这是一个 python 2 对 3 的问题?如果是这样,我无法安装 python 3,因为我的计算机由我们的 IT 部门控制。mozway.pypython mozway.pyFile "mozway.py", line 6g.to_csv(f'{out_basename}{i+1}.csv', index=False)'.csv'SyntaxError: invalid syntaxPython 2.7.15
0赞 mozway 7/27/2023
@EdMorton python 2 已被弃用多年(您收到的错误是由于自 python3.6 以来仅存在的 f 字符串),我使用了 python 3.11 和 pandas 2.0.3。关于你的时间与anubhavas的时间,请随意自己比较,但无论如何,两者都比我手中的python慢得多
1赞 mozway 7/27/2023
是的,这是计划好的,我只是在重新检查旧脚本,尽管我很确定我在时间上没有犯错。但这是有道理的,如果你一遍又一遍地重新打开文件,行为是二次的,因为你需要在每次迭代中再次到达末尾。编辑:再次测试,确实是 >2 分钟。
1赞 mozway 7/27/2023
@EdMorton我的意思是你的文件是 AAA...BBB...CCC...DDD的...电子电气设备...FFF...您应该在 AA 上进行测试...B...C...D...E...F...G...H...[...]Z123...只有 6 个不同的 ID,您很少循环访问文件。尝试许多排序的 ID,而不仅仅是 6 个。
4赞 Ed Morton 7/27/2023 #4

在每个 Unix 机器上的任何 shell 中使用任何 awk:

$ cat tst.awk
NR==1 {
    hdr = $0
    next
}
$NF != prev {
    out = (((blockCnt++) % X) + 1) ".csv"
    if ( blockCnt <= X ) {
        print hdr > out
    }
    prev = $NF
}
{ print > out }

$ awk -v X=3 -f tst.awk input.csv

$ head [0-9]*.csv
==> 1.csv <==
Text                 ID
this is sample text, AAAA
this is sample text, AAAA
this is sample text, AAAA
this is sample text, AAAA
this is sample text, AAAA
this is sample text4, DDDD
this is sample text4, DDDD

==> 2.csv <==
Text                 ID
this is sample text2, BBBB
this is sample text2, BBBB
this is sample text2, BBBB
this is sample text5, EEEE
this is sample text5, EEEE

==> 3.csv <==
Text                 ID
this is sample text3, CCCC
this is sample text6, FFFF
this is sample text6, FFFF

如果数量足够大,以至于您超出了并发打开文件的系统限制,并且您开始收到“打开的文件太多”错误,那么您需要使用 GNU awk,因为它会在内部处理该错误,或者将代码更改为一次只打开 1 个文件:X

NR==1 {
    hdr = $0
    next
}
$NF != prev {
    close(out)
    out = (((blockCnt++) % X) + 1) ".csv"
    if ( blockCnt <= X ) {
        print hdr > out
    }
    prev = $NF
}
{ print >> out }

或者实现您自己的方式来管理同时打开的文件数。


编辑:这是@PaulHodges在评论中提出的建议,将产生如下脚本:

NR == 1 {
    for ( i=1; i <= X; i++ ) {
        print > (i ".csv")
    }
    next
}
$NF != prev {
    out = (((NR-1) % X) + 1) ".csv"
    prev = $NF
}
{ print > out }

评论

1赞 Paul Hodges 7/28/2023
将测试移动到一个块以写入已知输出文件集的标头是否会对大型输入文件的速度产生相关影响?blockCntBEGIN
1赞 Ed Morton 7/28/2023
@PaulHodges是的,这会让它稍微快一点,但你;d 需要添加对 getline 的调用以读取标头,但将其输入重定向为来自文件,因为 awk 尚未打开输入,然后文件将重新打开,该标题行仍然存在于主循环中,因此您必须添加或类似。鉴于所有这些,最好在一节中完成。我只是在答案的末尾添加了一个这样的脚本 - 唯一潜在的缺点是,即使输入中的 ID 少于 X 个,它也会始终创建 X 个输出文件。NR==1 { next}NR==1
0赞 Daweo 7/27/2023 #5

这里

group.to_csv(file_+'.csv',index=False, header=False, mode='a')

您提供字符串作为第一个参数,但是to_csv方法允许您提供类似文件的对象作为第一个参数,在这种情况下,您可以避免多次执行与文件打开相关的事情,请考虑以下简单比较

import os
import time
import pandas as pd
REPEAT = 1000
df = pd.DataFrame({'col1':range(100)})
t1 = time.time()
for _ in range(REPEAT):
    df.to_csv('file.csv',index=False,header=False,mode='a')
t2 = time.time()
os.remove('file.csv')
t3 = time.time()
with open('file.csv','a') as f:
    for _ in range(REPEAT):
        df.to_csv(f,index=False,header=False)
t4 = time.time()
print('Using filename',t2-t1)
print('Using filehandle',t4-t3)

给出输出

Using filename 0.35850977897644043
Using filehandle 0.2669696807861328

请注意,第二种方式大约需要第 1 种方式的 75% 时间,因此虽然它更快,但它仍然是相同的数量级。