“Large data” work flows using pandas
在学习大熊猫的过程中,我花了好几个月的时间试图找到这个问题的答案。我在日常工作中使用SAS,它非常适合核心支持之外的工作。然而,由于许多其他原因,SAS作为软件的一部分是可怕的。
有一天,我希望用python和pandas替换我对SAS的使用,但是我目前缺少一个大数据集的核心外工作流。我说的不是需要分布式网络的"大数据",而是文件太大而无法放入内存,但又太小而无法放入硬盘。
我的第一个想法是使用
完成以下任务的最佳实践工作流有哪些:
在现实世界中,特别是在"大数据"上使用熊猫的任何人,都会非常欣赏这些例子。
edit——我希望它如何工作的示例:
我正在尝试找到执行这些步骤的最佳实践方法。阅读有关熊猫和Pytables的链接时,附加一个新的专栏似乎是个问题。
编辑——具体回答杰夫的问题:
我很少向数据集添加行。我将几乎总是创建新的列(统计/机器学习术语中的变量或特性)。
我经常以这种方式使用数十GB的数据例如,我在磁盘上有通过查询读取的表,创建数据并追加回来。
对于如何存储数据的一些建议,阅读文档是值得的,而且在这个线程的最后阶段也是值得的。
影响数据存储方式的详细信息,如:尽你所能提供更多的细节;我可以帮助你开发一个结构。
解决方案
确保您至少安装了
读取逐块迭代文件和多个表查询。
由于Pytables被优化为按行操作(这是您查询的内容),所以我们将为每组字段创建一个表。这样很容易选择一小组字段(这将与一个大表一起使用,但这样做更有效…我想我可以在将来解决这个限制…无论如何,这更直观):(以下是伪代码。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import numpy as np import pandas as pd # create a store store = pd.HDFStore('mystore.h5') # this is the key to your storage: # this maps your fields to a specific group, and defines # what you want to have as data_columns. # you might want to create a nice class wrapping this # (as you will want to have this map and its inversion) group_map = dict( A = dict(fields = ['field_1','field_2',.....], dc = ['field_1',....,'field_5']), B = dict(fields = ['field_10',...... ], dc = ['field_10']), ..... REPORTING_ONLY = dict(fields = ['field_1000','field_1001',...], dc = []), ) group_map_inverted = dict() for g, v in group_map.items(): group_map_inverted.update(dict([ (f,g) for f in v['fields'] ])) |
读取文件并创建存储(基本上执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | for f in files: # read in the file, additional options hmay be necessary here # the chunksize is not strictly necessary, you may be able to slurp each # file into memory in which case just eliminate this part of the loop # (you can also change chunksize if necessary) for chunk in pd.read_table(f, chunksize=50000): # we are going to append to each table by group # we are not going to create indexes at this time # but we *ARE* going to create (some) data_columns # figure out the field groupings for g, v in group_map.items(): # create the frame for this group frame = chunk.reindex(columns = v['fields'], copy = False) # append it store.append(g, frame, index=False, data_columns = v['dc']) |
现在文件中有了所有的表(实际上,如果您愿意,可以将它们存储在单独的文件中,您可能需要将文件名添加到组映射中,但这可能不是必需的)。
这是获取列和创建新列的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | frame = store.select(group_that_I_want) # you can optionally specify: # columns = a list of the columns IN THAT GROUP (if you wanted to # select only say 3 out of the 20 columns in this sub-table) # and a where clause if you want a subset of the rows # do calculations on this frame new_frame = cool_function_on_frame(frame) # to 'add columns', create a new group (you probably want to # limit the columns in this new_group to be only NEW ones # (e.g. so you don't overlap from the other tables) # add this info to the group_map store.append(new_group, new_frame.reindex(columns = new_columns_created, copy = False), data_columns = new_columns_created) |
准备好后处理时:
1 2 3 | # This may be a bit tricky; and depends what you are actually doing. # I may need to modify this function to be a bit more general: report_data = store.select_as_multiple([groups_1,groups_2,.....], where =['field_1>0', 'field_1000=foo'], selector = group_1) |
关于数据列,您实际上不需要定义任何数据列;它们允许您根据列子选择行。例如:
1 | store.select(group, where = ['field_1000=foo', 'field_1001>0']) |
在最终报告生成阶段,它们可能对您最感兴趣(实际上,数据列与其他列是分离的,如果您定义了很多,这可能会对效率产生一定影响)。
您还可能希望:
- 创建一个函数,它获取一个字段列表,在组映射中查找组,然后选择这些组并连接结果,这样就得到了结果帧(这基本上就是select_as_multiple所做的)。这样结构对您来说就相当透明了。
- 某些数据列上的索引(使行子集设置更快)。
- 启用压缩。
有问题请告诉我!
我认为上面的答案缺少了一个我发现非常有用的简单方法。
当我有一个文件太大而无法在内存中加载时,我将该文件拆分为多个较小的文件(按行或列)。
示例:如果30天的交易数据大小约为30GB,我会将其分解为一个每天大小约为1GB的文件。我随后分别处理每个文件,并在最后汇总结果
最大的优点之一是它允许并行处理文件(多个线程或进程)
另一个优点是,文件操作(如示例中的添加/删除日期)可以通过常规shell命令完成,这在更高级/更复杂的文件格式中是不可能实现的。
这种方法不包括所有的场景,但在很多场景中非常有用
如果您的数据集在1到20GB之间,您应该得到一个具有48GB RAM的工作站。然后熊猫可以在RAM中保存整个数据集。我知道这不是你要找的答案,但是在一个有4GB内存的笔记本上进行科学计算是不合理的。
在这个问题发生两年后,现在有了一只"核心外"的大熊猫:dask。太棒了!虽然它不支持所有的熊猫功能,但您可以真正做到这一点。
我知道这是一条老路,但我认为Blaze图书馆值得一看。它是为这些类型的情况而构建的。
来自文档:
Blaze将numpy和pandas的可用性扩展到了分布式和非核心计算。Blaze提供了一个类似于numpy-nd数组或pandas数据帧的接口,但是将这些熟悉的接口映射到各种其他计算引擎上,比如postgres或spark。
编辑:顺便说一句,它得到了《numpy》的作者Continuumio和TravisOliphant的支持。
这就是蒙古包的情况。我也在python中使用了SQLServer、sqlite、hdf、orm(sqlAlchemy)。首先,pymongo是一个基于文档的数据库,因此每个人都是一个文档(
pd.dateframe->pymongo注:我用
1 | aCollection.insert((a[1].to_dict() for a in df.iterrows())) |
查询:gt=大于…
1 | pd.DataFrame(list(mongoCollection.find({'anAttribute':{'$gt':2887000, '$lt':2889000}}))) |
连接怎么样,因为我通常会得到10个数据源粘贴在一起:
1 | aJoinDF = pandas.DataFrame(list(mongoCollection.find({'anAttribute':{'$in':Att_Keys}}))) |
然后(在我的例子中,有时我必须在
1 | df = pandas.merge(df, aJoinDF, on=aKey, how='left') |
然后您可以通过下面的更新方法将新信息写入主集合。(逻辑集合与物理数据源)。
1 | collection.update({primarykey:foo},{key:change}) |
对于较小的查找,只需取消规范化。例如,您在文档中有代码,只需添加字段代码文本,并在创建文档时执行
现在,您有了一个基于一个人的好数据集,您可以释放每个案例的逻辑,并生成更多的属性。最后,您可以将您的3到内存的最大键指示器读到pandas中,并进行pivots/agg/data exploration。这对我来说适用于300万条记录,包括数字/大文本/类别/代码/浮点数。
您还可以使用MongoDB中内置的两种方法(MapReduce和Aggregate框架)。有关聚合框架的更多信息,请参阅此处,因为它似乎比MapReduce更容易,而且对于快速聚合工作来说很方便。注意,我不需要定义字段或关系,我可以向文档添加项。在快速变化的numpy、pandas、python工具集的当前状态下,mongodb帮助我开始工作:)
我发现这有点晚了,但我也遇到了类似的问题(抵押贷款提前还款模式)。我的解决方案是跳过pandas hdfstore层,使用直接的PyTables。我在最终文件中将每列保存为一个单独的HDF5数组。
我的基本工作流程是首先从数据库中获取一个csv文件。我喜欢,所以没那么大。然后我将其转换为一个面向行的HDF5文件,方法是在python中对其进行迭代,将每一行转换为实际的数据类型,然后将其写入HDF5文件。这需要几十分钟的时间,但它不使用任何内存,因为它只是逐行操作。然后我将面向行的hdf5文件"转置"为面向列的hdf5文件。
表转置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def transpose_table(h_in, table_path, h_out, group_name="data", group_path="/"): # Get a reference to the input data. tb = h_in.getNode(table_path) # Create the output group to hold the columns. grp = h_out.createGroup(group_path, group_name, filters=tables.Filters(complevel=1)) for col_name in tb.colnames: logger.debug("Processing %s", col_name) # Get the data. col_data = tb.col(col_name) # Create the output array. arr = h_out.createCArray(grp, col_name, tables.Atom.from_dtype(col_data.dtype), col_data.shape) # Store the data. arr[:] = col_data h_out.flush() |
重新阅读,然后看起来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | def read_hdf5(hdf5_path, group_path="/data", columns=None): """Read a transposed data set from a HDF5 file.""" if isinstance(hdf5_path, tables.file.File): hf = hdf5_path else: hf = tables.openFile(hdf5_path) grp = hf.getNode(group_path) if columns is None: data = [(child.name, child[:]) for child in grp] else: data = [(child.name, child[:]) for child in grp if child.name in columns] # Convert any float32 columns to float64 for processing. for i in range(len(data)): name, vec = data[i] if vec.dtype == np.float32: data[i] = (name, vec.astype(np.float64)) if not isinstance(hdf5_path, tables.file.File): hf.close() return pd.DataFrame.from_items(data) |
现在,我通常在一台有大量内存的机器上运行这个程序,所以我可能对内存使用不够小心。例如,默认情况下,加载操作读取整个数据集。
这通常对我有用,但有点笨重,我不能使用奇特的Pytables魔法。
编辑:与默认的记录Pytables数组相比,这种方法的真正优点是我可以使用h5r将数据加载到r中,而h5r不能处理表。或者,至少,我无法让它加载异构表。
对于大型数据用例,我发现一个有用的技巧是通过将浮点精度降低到32位来减少数据量。它不适用于所有情况,但在许多应用程序中,64位精度过高,节省2倍内存是值得的。更明显的是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | >>> df = pd.DataFrame(np.random.randn(int(1e8), 5)) >>> df.info() <class 'pandas.core.frame.DataFrame'> RangeIndex: 100000000 entries, 0 to 99999999 Data columns (total 5 columns): ... dtypes: float64(5) memory usage: 3.7 GB >>> df.astype(np.float32).info() <class 'pandas.core.frame.DataFrame'> RangeIndex: 100000000 entries, 0 to 99999999 Data columns (total 5 columns): ... dtypes: float32(5) memory usage: 1.9 GB |
正如其他人所指出的,在几年后,一个"核心外"的大熊猫等价物出现了:dask。尽管dask并不是大熊猫及其所有功能的替代品,但它的突出原因有几个:
DASK是一个灵活的并行计算库,用于分析计算,它针对以下交互计算工作负载的动态任务调度进行了优化:"大数据"集合(如并行数组、数据帧和列表)将通用接口(如numpy、pandas或python迭代器)扩展到大于内存或分布式环境,并从笔记本电脑扩展到集群。
Dask emphasizes the following virtues:
- Familiar: Provides parallelized NumPy array and Pandas DataFrame objects
- Flexible: Provides a task scheduling interface for more custom workloads and integration with other projects.
- Native: Enables distributed computing in Pure Python with access to the PyData stack.
- Fast: Operates with low overhead, low latency, and minimal serialization necessary for fast numerical algorithms
- Scales up: Runs resiliently on clusters with 1000s of cores Scales down: Trivial to set up and run on a laptop in a single process
- Responsive: Designed with interactive computing in mind it provides rapid feedback and diagnostics to aid humans
并添加一个简单的代码示例:
1 2 3 | import dask.dataframe as dd df = dd.read_csv('2015-*-*.csv') df.groupby(df.user_id).value.mean().compute() |
替换如下熊猫代码:
1 2 3 | import pandas as pd df = pd.read_csv('2015-01-01.csv') df.groupby(df.user_id).value.mean() |
特别值得注意的是,通过concurrent.futures接口提供了一个用于提交自定义任务的通用接口:
1 2 3 4 5 6 7 8 9 10 | from dask.distributed import Client client = Client('scheduler:port') futures = [] for fn in filenames: future = client.submit(load, fn) futures.append(future) summary = client.submit(summarize, futures) summary.result() |
再来一个变体
在熊猫中执行的许多操作也可以作为数据库查询(SQL、Mongo)执行。
使用RDBMS或MongoDB可以在数据库查询中执行一些聚合(它针对大数据进行了优化,并有效地使用缓存和索引)。
稍后,您可以使用熊猫执行后处理。
此方法的优点是,您可以获得用于处理大数据的DB优化,同时还可以使用高级声明性语法定义逻辑,而不必处理决定在内存中做什么和在核心外做什么的细节。
尽管查询语言和pandas是不同的,但是将部分逻辑从一个转换到另一个通常并不复杂。
这里值得一提的是,雷,这是一个分布式计算框架,它以分布式的方式为熊猫提供了自己的实现。
只需替换熊猫导入,代码就可以正常工作:
1 2 3 4 | # import pandas as pd import ray.dataframe as pd #use pd as usual |
可以在此处阅读更多详细信息:
https://rise.cs.berkeley.edu/blog/pandas-on-ray网站/
如果您使用创建数据管道的简单路径,将其分解为多个较小的文件,请考虑Ruffus。
我最近遇到了一个类似的问题。我发现简单地将数据分块读取,然后在将数据分块写入同一个csv时将其附加,效果很好。我的问题是根据另一个表中的信息添加一个日期列,使用以下特定列的值。这可能有助于那些被dask和hdf5弄糊涂的人,但对像我这样的熊猫更熟悉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | def addDateColumn(): """Adds time to the daily rainfall data. Reads the csv as chunks of 100k rows at a time and outputs them, appending as needed, to a single csv. Uses the column of the raster names to get the date. """ df = pd.read_csv(pathlist[1]+"CHIRPS_tanz.csv", iterator=True, chunksize=100000) #read csv file as 100k chunks '''Do some stuff''' count = 1 #for indexing item in time list for chunk in df: #for each 100k rows newtime = [] #empty list to append repeating times for different rows toiterate = chunk[chunk.columns[2]] #ID of raster nums to base time while count <= toiterate.max(): for i in toiterate: if i ==count: newtime.append(newyears[count]) count+=1 print"Finished", str(chunknum),"chunks" chunk["time"] = newtime #create new column in dataframe based on time outname ="CHIRPS_tanz_time2.csv" #append each output to same csv, using no header chunk.to_csv(pathlist[2]+outname, mode='a', header=None, index=None) |