关于python:”大数据”工作流使用pandas

“Large data” work flows using pandas

在学习大熊猫的过程中,我花了好几个月的时间试图找到这个问题的答案。我在日常工作中使用SAS,它非常适合核心支持之外的工作。然而,由于许多其他原因,SAS作为软件的一部分是可怕的。

有一天,我希望用python和pandas替换我对SAS的使用,但是我目前缺少一个大数据集的核心外工作流。我说的不是需要分布式网络的"大数据",而是文件太大而无法放入内存,但又太小而无法放入硬盘。

我的第一个想法是使用HDFStore在磁盘上保存大型数据集,并且只将我需要的部分放入数据帧中进行分析。其他人提到MongoDB是一种更容易使用的替代方案。我的问题是:

完成以下任务的最佳实践工作流有哪些:

  • 将平面文件加载到永久的磁盘数据库结构中
  • 查询该数据库以检索要馈送到熊猫数据结构中的数据
  • 在处理熊猫的片段后更新数据库
  • 在现实世界中,特别是在"大数据"上使用熊猫的任何人,都会非常欣赏这些例子。

    edit——我希望它如何工作的示例:

  • 迭代导入一个大的平面文件,并将其存储在永久的磁盘数据库结构中。这些文件通常太大,内存不足。
  • 为了使用panda,我想读取这些数据的子集(通常一次只读取几列),这些数据可以放在内存中。
  • 我将通过对所选列执行各种操作来创建新列。
  • 然后我必须将这些新列附加到数据库结构中。
  • 我正在尝试找到执行这些步骤的最佳实践方法。阅读有关熊猫和Pytables的链接时,附加一个新的专栏似乎是个问题。

    编辑——具体回答杰夫的问题:

  • 我正在建立消费者信贷风险模型。数据类型包括电话、SSN和地址特征;财产价值;犯罪记录、破产等贬损信息。我每天使用的数据集平均有1000到2000个混合数据类型的字段:数字和字符数据的连续变量、名义变量和序数变量。我很少追加行,但我确实执行了许多创建新列的操作。
  • 典型的操作包括使用条件逻辑将几个列组合成一个新的复合列。例如,if var1 > 2 then newvar = 'A' elif var2 = 4 then newvar = 'B'。这些操作的结果是我的数据集中每个记录的一个新列。
  • 最后,我想将这些新列附加到磁盘上的数据结构中。我将重复步骤2,用交叉表和描述性统计来探索数据,试图找到有趣的、直观的模型关系。
  • 典型的项目文件通常约为1GB。文件被组织成这样一种方式:一行由消费数据记录组成。每一行的每一条记录的列数相同。情况总是这样。
  • 在创建新列时,我很少按行进行子集。但是,在创建报表或生成描述性统计信息时,我通常在行上进行子集。例如,我可能想为特定的业务线创建一个简单的频率,比如说零售信用卡。为此,除了要报告的列之外,我只选择业务线=零售的记录。但是,在创建新列时,我将提取所有数据行,并且只提取操作所需的列。
  • 建模过程要求我分析每一列,查找与某个结果变量的有趣关系,并创建描述这些关系的新复合列。我研究的列通常是小的集合。例如,我将集中讨论一组比如20列,只处理属性值,并观察它们与贷款违约的关系。一旦对这些内容进行了探索并创建了新的专栏,我就转到另一组专栏,比如说大学教育,然后重复这个过程。我要做的是创建候选变量,解释我的数据和某些结果之间的关系。在这个过程的最后,我应用了一些学习技巧,从这些复合列中创建一个方程。
  • 我很少向数据集添加行。我将几乎总是创建新的列(统计/机器学习术语中的变量或特性)。


    我经常以这种方式使用数十GB的数据例如,我在磁盘上有通过查询读取的表,创建数据并追加回来。

    对于如何存储数据的一些建议,阅读文档是值得的,而且在这个线程的最后阶段也是值得的。

    影响数据存储方式的详细信息,如:尽你所能提供更多的细节;我可以帮助你开发一个结构。

  • 数据大小,行、列、列类型;是否附加行,还是只是列?
  • 典型的操作会是什么样子。例如,对列进行查询以选择一组行和特定列,然后执行操作(在内存中),创建新列,保存这些列。(给出一个玩具的例子可以使我们提供更具体的建议。)
  • 处理完之后,你会怎么做?步骤2是临时的还是可重复的?
  • 输入平面文件:多少,粗略的总大小以GB为单位。这些是如何组织的,例如通过记录?每个文件包含不同的字段,还是每个文件都有一些记录,其中包含每个文件中的所有字段?
  • 是否根据条件选择行(记录)的子集(例如,选择字段A>5的行)?然后做点什么,或者您只是选择包含所有记录的字段A、B、C(然后做点什么)?
  • 您是否"处理"了所有列(分组),或者是否有一个很好的比例只能用于报表(例如,您希望保留数据,但在最终结果时间之前不需要明确拉入该列)?
  • 解决方案

    确保您至少安装了0.10.1

    读取逐块迭代文件和多个表查询。

    由于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'] ]))

    读取文件并创建存储(基本上执行append_to_multiple所做的操作):

    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是一个基于文档的数据库,因此每个人都是一个文档(dict个属性)。许多人组成一个集合,你可以有许多集合(人,股票市场,收入)。

    pd.dateframe->pymongo注:我用read_csv中的chunksize来保存它到5到10k条记录(如果较大的话,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}})))

    .find()返回一个迭代器,因此我通常使用ichunked将其拆分为较小的迭代器。

    连接怎么样,因为我通常会得到10个数据源粘贴在一起:

    1
    aJoinDF = pandas.DataFrame(list(mongoCollection.find({'anAttribute':{'$in':Att_Keys}})))

    然后(在我的例子中,有时我必须在aJoinDF的"可合并"之前先对它进行聚合)。

    1
    df = pandas.merge(df, aJoinDF, on=aKey, how='left')

    然后您可以通过下面的更新方法将新信息写入主集合。(逻辑集合与物理数据源)。

    1
    collection.update({primarykey:foo},{key:change})

    对于较小的查找,只需取消规范化。例如,您在文档中有代码,只需添加字段代码文本,并在创建文档时执行dict查找。

    现在,您有了一个基于一个人的好数据集,您可以释放每个案例的逻辑,并生成更多的属性。最后,您可以将您的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)