在Julia中读取CSV文件相比Python较慢

29

在Julia中读取大文本/CSV文件比Python要慢很多。以下是读取大小为486.6 MB,具有153895行和644列的文件所需的时间。

Python 3.3示例

import pandas as pd
import time
start=time.time()
myData=pd.read_csv("C:\\myFile.txt",sep="|",header=None,low_memory=False)
print(time.time()-start)

Output: 19.90

R 3.0.2 示例

system.time(myData<-read.delim("C:/myFile.txt",sep="|",header=F,
   stringsAsFactors=F,na.strings=""))

Output:
User    System  Elapsed
181.13  1.07    182.32

Julia 0.2.0(Julia Studio 0.4.4)示例 #1

using DataFrames
timing = @time myData = readtable("C:/myFile.txt",separator='|',header=false)

Output:
elapsed time: 80.35 seconds (10319624244 bytes allocated)

Julia 0.2.0(Julia Studio 0.4.4)示例#2

timing = @time myData = readdlm("C:/myFile.txt",'|',header=false)

Output:
elapsed time: 65.96 seconds (9087413564 bytes allocated)
  1. Julia比R更快,但与Python相比则相对较慢。我应该如何改变操作以加速读取大型文本文件?

  2. 另一个问题是在Julia中,内存大小为硬盘文件大小的18倍,而Python只有2.5倍。在Matlab中,我发现它对于大文件来说最节省内存,其内存大小为硬盘文件大小的2倍。为什么Julia中的文件要占用这么大的内存空间呢?


4
顺便说一下,在 R 中,我建议使用 data.table 包中的 fread 函数,它的速度更快。 - baptiste
@baptiste,无法使用fread读取文件。当读取数据时,在第12141行的字段412上,出现了错误Expected sep ('|') but '"' ends field 412 on line 12141 when reading data: ...。字段412中有一个包含两个"的字符串,导致fread出现问题。此外,以它的速度读取到第12141行,总时间理论上只需要36.1秒,因此可能比read.delim有显著的改进。 - uday
7个回答

45

最好的答案可能是,我不如Wes那样是一个好程序员。

总的来说,DataFrames 中的代码比 Pandas 中的代码优化得少得多。我相信我们可以追赶上去,但这需要一些时间,因为我们需要先实现许多基本功能。由于在 Julia 中还有很多需要构建的东西,所以我倾向于分三步进行:(1)构建任何版本,(2)构建正确的版本,(3)构建快速、正确的版本。对于我做的工作,Julia 通常没有提供任何必要功能的版本,所以我的工作重点是 (1) 和 (2)。随着我需要的工具越来越多被构建出来,便更容易专注于性能。

至于内存使用,我认为答案是,在解析表格数据时,我们使用了一组数据结构,它们比 Pandas 中使用的数据结构效率要低得多。如果我更了解 Pandas 的内部机制,我可以列出我们效率较低的地方,但现在我只能猜测一个明显的问题是,我们正在将整个数据集读入内存,而不是从磁盘获取块。这当然是可以避免的,也有相应的问题正在处理。这只是一个时间问题。

顺便说一句,readtable 代码相当容易阅读。让 readtable 更快的最可靠方法是使用 Julia 分析器快速修复性能缺陷。


4
谢谢,约翰。继续保持好工作。我一定会继续监控朱莉娅。 - uday

18

有一个相对较新的Julia包,叫做CSV.jl,由Jacob Quinn开发,提供了一种更快的CSV解析器,在许多情况下与pandas相媲美:https://github.com/JuliaData/CSV.jl


11
我找到了一些可以部分帮助这种情况的方法。
  1. using the readdlm() function in Julia seems to work considerably faster (e.g. 3x on a recent trial) than readtable(). Of course, if you want the DataFrame object type, you'll then need to convert to it, which may eat up most or all of the speed improvement.

  2. Specifying dimensions of your file can make a BIG difference, both in speed and in memory allocations. I ran this trial reading in a file that is 258.7 MB on disk:

    julia> @time Data = readdlm("MyFile.txt", '\t', Float32, skipstart = 1);
    19.072266 seconds (221.60 M allocations: 6.573 GB, 3.34% gc time)
    
    julia> @time Data = readdlm("MyFile.txt", '\t', Float32, skipstart = 1, dims = (File_Lengths[1], 62));
    10.309866 seconds (87 allocations: 528.331 MB, 0.03% gc time)
    
  3. The type specification for your object matters a lot. For instance, if your data has strings in it, then the data of the array that you read in will be of type Any, which is expensive memory wise. If memory is really an issue, you may want to consider preprocessing your data by first converting the strings to integers, doing your computations, and then converting back. Also, if you don't need a ton of precision, using Float32 type instead of Float64 can save a LOT of space. You can specify this when reading the file in, e.g.:

    Data = readdlm("file.csv", ',', Float32)

  4. Regarding memory usage, I've found in particular that the PooledDataArray type (from the DataArrays package) can be helpful in cutting down memory usage if your data has a lot of repeated values. The time to convert to this type is relatively large, so this isn't a time saver per se, but at least helps reduce the memory usage somewhat. E.g. when loading a data set with 19 million rows and 36 columns, 8 of which represented categorical variables for statistical analysis, this reduced the memory allocation of the object from 5x its size on disk to 4x its size. If there are even more repeated values, the memory reduction can be even more significant (I've had situations where the PooledDataArray cuts memory allocation in half).

  5. It can also sometimes help to run the gc() (garbage collector) function after loading and formatting data to clear out any unneeded ram allocation, though generally Julia will do this automatically pretty well.

尽管如此,我仍然期待Julia的进一步发展,以实现更快速的加载和更高效的内存使用,用于处理大型数据集。


11
请注意,@time输出的“分配的n字节”是所有已分配对象的总大小,不考虑其中有多少被释放了。这个数字通常比内存中活跃对象的最终大小要高得多。我不知道您的内存大小估计是否基于此,但我想指出这一点。

11

首先,让我们创建一个你所说的文件以提供可重复性:

open("myFile.txt", "w") do io
    foreach(i -> println(io, join(i+1:i+644, '|')), 1:153895)
end

现在我正在Julia 1.4.2和CSV.jl 0.7.1中读取此文件。

单线程:

julia> @time CSV.File("myFile.txt", delim='|', header=false);
  4.747160 seconds (1.55 M allocations: 1.281 GiB, 4.29% gc time)

julia> @time CSV.File("myFile.txt", delim='|', header=false);
  2.780213 seconds (13.72 k allocations: 1.206 GiB, 5.80% gc time)

并使用例如4个线程:

julia> @time CSV.File("myFile.txt", delim='|', header=false);
  4.546945 seconds (6.02 M allocations: 1.499 GiB, 5.05% gc time)

julia> @time CSV.File("myFile.txt", delim='|', header=false);
  0.812742 seconds (47.28 k allocations: 1.208 GiB)

在 R 中,它是这样的:
> system.time(myData<-read.delim("myFile.txt",sep="|",header=F,
+                                stringsAsFactors=F,na.strings=""))
   user  system elapsed 
 28.615   0.436  29.048 

在Python(Pandas)中,它是这样的:

>>> import pandas as pd
>>> import time
>>> start=time.time()
>>> myData=pd.read_csv("myFile.txt",sep="|",header=None,low_memory=False)
>>> print(time.time()-start)
25.95710587501526

现在,如果我们从R中测试fread(非常快速的函数),我们得到:

> system.time(fread("myFile.txt", sep="|", header=F,
                    stringsAsFactors=F, na.strings="", nThread=1))
   user  system elapsed 
  1.043   0.036   1.082 
> system.time(fread("myFile.txt", sep="|", header=F,
                    stringsAsFactors=F, na.strings="", nThread=4))
   user  system elapsed 
  1.361   0.028   0.416 

因此,在这种情况下,总结如下:

  • 尽管在Julia中编译CSV.File的成本很高,但是当您第一次运行它时,它比基本的R或Python快得多。
  • 它的速度与R中的fread相当(在这种情况下稍微慢一些,但其他的基准测试 (在这里)也显示了它更快的情况)。

编辑:根据要求,我添加了一个小文件的基准测试:10列,100,000行的Julia和Pandas。

数据准备步骤:

open("myFile.txt", "w") do io
    foreach(i -> println(io, join(i+1:i+10, '|')), 1:100_000)
end

CSV.jl,单线程:

julia> @time CSV.File("myFile.txt", delim='|', header=false);
  1.898649 seconds (1.54 M allocations: 93.848 MiB, 1.48% gc time)

julia> @time CSV.File("myFile.txt", delim='|', header=false);
  0.029965 seconds (248 allocations: 17.037 MiB)

Pandas:

>>> import pandas as pd
>>> import time
>>> start=time.time()
>>> myData=pd.read_csv("myFile.txt",sep="|",header=None,low_memory=False)
>>> print(time.time()-start)
0.07587623596191406

结论:

  • 编译成本是一次性的成本,必须付出并且它是恒定的(粗略地说,它不取决于要读取的文件有多大)
  • 对于小文件,如果我们排除编译成本,CSV.jl比Pandas更快

现在,如果您想避免在每个新的Julia会话中支付编译成本,可以使用https://github.com/JuliaLang/PackageCompiler.jl

根据我的经验,如果您正在进行数据科学工作,例如读入数千个CSV文件,我不介意等待2秒钟进行编译,以后可以节省几个小时。编写读取文件的代码花费的时间超过了2秒钟。

当然 - 如果您编写一个仅执行少量工作并在完成后终止的脚本,则编译时间实际上将是计算成本的主要部分。在这种情况下,我使用PackageCompiler.jl策略。


1
可能值得展示一下较小文件的成本情况,其中编译开销占主导地位。 - Oscar Smith
1
我已经添加了这个比较,并注释了如果想避免支付编译成本应该怎么做。 - Bogumił Kamiński
@BogumiłKamiński:自v1.9版本以来,PackageCompiler.jl已经变得无用,因为默认情况下,安装包时它们会被编译,我说的对吗? - undefined
部分是的。但不完全,因为PackageCompiler.jl提供了更多的功能。 - undefined

6

根据我的经验,处理大型文本文件的最佳方式不是将它们加载到Julia中,而是进行流式处理。这种方法有一些额外的固定成本,但通常运行非常快。以下是一些伪代码:

function streamdat()
    mycsv=open("/path/to/text.csv", "r")   # <-- opens a path to your text file

    sumvec = [0.0]                # <-- store a sum  here
    i = 1
    while(!eof(mycsv))            # <-- loop through each line of the file
       row = readline(mycsv) 
       vector=split(row, "|")     # <-- split each line by |
       sumvec+=parse(Float64, vector[i]) 
       i+=1
    end
end

streamdat()

上面的代码只是一个简单的求和,但这种逻辑可以扩展到更复杂的问题。


Julia 应该被构建为能够透明地为用户使用数据库,并能够从中流式处理所有操作。 - skan
你指的是什么? - Oscar Smith

0
using CSV
@time df=CSV.read("C:/Users/hafez/personal/r/tutorial for students/Book2.csv")

最近我在尝试使用Julia 1.4.2。我发现它的响应与以前不同,一开始我并不理解Julia。后来我在Julia讨论论坛上发布了相同的内容。然后我明白了这段代码只会提供编译时间。在这里你可以找到基准测试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接