2019-05-31 13:41:20 blogtranslator 阅读数 2506
  • Python数据分析实战-Pandas

    深度学习、机器学习和数据分析必须用pandas。pandas是在python直接流行的数据处理框架。可以说,如果不会使用pandas,就谈不上会用python做数据分析。本课程会使用奥林匹克一个真实的数据作为实验数据,从初级到各种pandas的常用操作,到常用的数据可视化,让你在短的时间内掌握好pandas,轻松愉快的玩转数据分析。

    2319 人正在学习 去看看 阿勒拉哈

在这里插入图片描述

原文链接:3 simple ways to handle large data with
Pandas

作者 | George Seif
译者 | jojoa

易上手, 文档丰富的Pandas 已经成为时下最火的数据处理库。此外,Pandas数据处理能力也一流。

其实无论你使用什么库,大量的数据处理起来往往回遇到新的挑战。
数据处理时,往往会遇到没有足够内存(RAM)这个硬件问题。 企业往往需要能够存够数百, 乃至数千 的GB 数据。
即便你的计算机恰好有足够的内存来存储这些数据, 但是读取数据到硬盘依旧非常耗时。
别担心! Pandas 数据库会帮我们摆脱这种困境。 这篇文章包含3种方法来减少数据大小,并且加快数据读取速度。 我用这些方法,把超过100GB 的数据, 压缩到了64GB 甚至32GB 的内存大小。

快来看看这三个妙招吧。

数据分块

csv 格式是一种易储存, 易更改并且用户易读取的格式。 pandas 有read_csv ()方法来上传数据,存储为CSV 格式。当遇到CSV 文件过大,导致内存不足的问题该怎么办呢?试试强大的pandas 工具吧!我们先把整个文件拆分成小块。这里,我们把拆分的小块称为chunk。

一个chunk 就是我们数据的一个小组。 Chunk 的大小主要依据我们内存的大小,自行决定。

过程如下:

1.读取一块数据。
2.分析数据。
3.保存该块数据的分析结果。
4.重复1-3步骤,直到所有chunk 分析完毕。
5.把所有的chunk 合并在一起。

我们可以通过read_csv()方法Chunksize来完成上述步骤。 Chunksize是指pandas 一次能读取到多少行csv文件。这个当然也是建立在RAM 内存容量的基础上。
假如我们认为数据呈现高斯分布时, 我们可以在一个chunk 上, 进行数据处理和视觉化, 这样会提高准确率。

当数据稍微复杂时, 例如呈现泊松分布时, 我们最好能一块块筛选,然后把每一小块整合在一起。 然后再进行分析。很多时候, 我们往往删除太多的不相关列,或者删除有值行。 我们可以在每个chunk 上,删除不相关数据, 然后再把数据整合在一起,最后再进行数据分析。

代码如下:

在这里插入图片描述
删除数据

有时候, 我们一眼就能看到需要分析的列。事实上, 通常名字,账号等列,我们是不做分析的。

读取数据前, 先跳过这些无用的列,可以帮我们节省很多内存。 Pandas 可以允许我们选择想要读取的列。

在这里插入图片描述
把包含无用信息的列删除掉, 往往给我们节省了大量内存。

此外,我们还可以把有缺失值的行,或者是包含“NA” 的行删除掉。 通过dropna()方法可以实现:

在这里插入图片描述
有几个非常有用的参数,可以传给dropna():

  • how: 可选项:“any”(该行的任意一列如果出现”NA”, 删除该行)
  • “all” (只有某行所有数数据全部是”NA” 时才删除)
  • thresh: 设定某行最多包含多少个NA 时,才进行删除
  • subset: 选定某个子集,进行NA 查找

可以通过这些参数, 尤其是thresh 和 subset 两个参数可以决定某行是否被删除掉。

Pandas 在读取信息的时候,无法删除列。但是我们可以在每个chunk 上,进行上述操作。

为列设定不同的数据类型

数据科学家新手往往不会对数据类型考虑太多。 当处理数据越来越多时, 就非常有必要考虑数据类型了。

行业常用的解决方法是从数据文件中,读取数据, 然后一列列设置数据类型。 但当数据量非常大时, 我们往往担心内存空间不够用。

在CSV 文件中,例如某列是浮点数, 它往往会占据更多的存储空间。 例如, 当我们下载数据来预测股票信息时, 价格往往以32位浮点数形式存储。

但是,我们真的需要32位浮点数码? 大多数情况下, 股票价格以小数点后保留两位数据进行交易。 即便我们想看到更精确的数据, 16位浮点数已经足够了。

我们往往会在读取数据的时候, 设置数据类型,而不是保留数据原类型。 那样的话,会浪费掉部分内存。

通过read_csv() 中设置dtype参数来完成数据类型设置。还可以设置字典类型,设置该列是键, 设置某列是字典的值。

请看下面的pandas 例子:

在这里插入图片描述
文章到这里结束了! 希望上述三个方法可以帮你节省时间和内存。

2019-02-11 17:21:57 yiluohan0307 阅读数 746
  • Python数据分析实战-Pandas

    深度学习、机器学习和数据分析必须用pandas。pandas是在python直接流行的数据处理框架。可以说,如果不会使用pandas,就谈不上会用python做数据分析。本课程会使用奥林匹克一个真实的数据作为实验数据,从初级到各种pandas的常用操作,到常用的数据可视化,让你在短的时间内掌握好pandas,轻松愉快的玩转数据分析。

    2319 人正在学习 去看看 阿勒拉哈

python pandas处理大数据节省内存的方法

  • 数值类型的列进行降级处理
  • 字符串类型的列转化为类别类型(category)
  • 字符串类型的列的类别数超过总行数的一半时,建议使用object类型
'''
减少内存的使用
'''
def reduce_mem_usage(df, verbose=True):
    numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64', 'object']
    start_mem = df.memory_usage().sum() / 1024**2    
    if verbose:print "Memory usage of the dataframe before converted is :", start_mem, "MB"
    # print dataset.isnull().any()
    for col in df.columns:
        col_type = df[col].dtypes
        if col_type in numerics:
            c_min = df[col].min()
            c_max = df[col].max()
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)  
            elif str(col_type)[:5] == 'float':
                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                    df[col] = df[col].astype(np.float16)
                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                    df[col] = df[col].astype(np.float32)
                else:
                    df[col] = df[col].astype(np.float64)
            else:
                num_unique_values = len(df[col].unique())
                num_total_values = len(df[col])
                rate = num_unique_values/num_total_values
                #rate = df[col].value_counts(normalize=True, dropna=False).values[0]
                if rate <0.5:
                    df[col] = df[col].astype('category')
    end_mem = df.memory_usage().sum() / 1024**2
    if verbose:print "Memory usage of the dataframe after converted is :", end_mem, "MB"
    if verbose: print('Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction)'.format(end_mem, 100 * (start_mem - end_mem) / start_mem))
    return df, ditc(df.types)

参考

https://blog.csdn.net/weiyongle1996/article/details/78498603

https://blog.csdn.net/qq_34739497/article/details/79600479

2019-06-16 13:28:17 kclax 阅读数 236
  • Python数据分析实战-Pandas

    深度学习、机器学习和数据分析必须用pandas。pandas是在python直接流行的数据处理框架。可以说,如果不会使用pandas,就谈不上会用python做数据分析。本课程会使用奥林匹克一个真实的数据作为实验数据,从初级到各种pandas的常用操作,到常用的数据可视化,让你在短的时间内掌握好pandas,轻松愉快的玩转数据分析。

    2319 人正在学习 去看看 阿勒拉哈

在这里插入图片描述

在数据分析领域,最热门的莫过于Python和R语言,此前有一篇文章《别老扯什么Hadoop了,你的数据根本不够大》指出:只有在超过5TB数据量的规模下,Hadoop才是一个合理的技术选择。这次拿到近亿条日志数据,千万级数据已经是关系型数据库的查询分析瓶颈,之前使用过Hadoop对大量文本进行分类,这次决定采用Python来处理数据:

硬件环境
CPU:3.5 GHz Intel Core i7
内存:32 GB HDDR 3 1600 MHz
硬盘:3 TB Fusion Drive
数据分析工具
Python:2.7.6
Pandas:0.15.0
IPython notebook:2.0.0
源数据如下表所示:

Table	Size	Desc
ServiceLogs	98,706,832 rows x 14 columns	8.77 GB	交易日志数据,
每个交易会话可以有多条交易

ServiceCodes	286 rows × 8 columns	20 KB	交易分类的字典表
数据读取
启动IPython notebook,加载pylab环境:

ipython notebook --pylab=inline

Pandas提供了IO工具可以将大文件分块读取,测试了一下性能,完整加载9800万条数据也只需要263秒左右,还是相当不错了。

import pandas as pd
reader = pd.read_csv('data/servicelogs', iterator=True)
try:
    df = reader.get_chunk(100000000)
except StopIteration:
    print "Iteration is stopped."
1百万条	1千万条	1亿条
ServiceLogs	1 s	17 s	263 s
使用不同分块大小来读取再调用 pandas.concat 连接DataFrame,chunkSize设置在1000万条左右速度优化比较明显。
loop = True
chunkSize = 100000
chunks = []
while loop:
    try:
        chunk = reader.get_chunk(chunkSize)
        chunks.append(chunk)
    except StopIteration:
        loop = False
        print "Iteration is stopped."
df = pd.concat(chunks, ignore_index=True)
下面是统计数据,Read Time是数据读取时间,Total Time是读取和Pandas进行concat操作的时间,根据数据总量来看,对5~50个DataFrame对象进行合并,性能表现比较好。

Chunk Size	Read Time (s)	Total Time (s)	Performance
100,000	224.418173	261.358521	
200,000	232.076794	256.674154	
1,000,000	213.128481	234.934142	√ √
2,000,000	208.410618	230.006299	√ √ √
5,000,000	209.460829	230.939319	√ √ √
10,000,000	207.082081	228.135672	√ √ √ √
20,000,000	209.628596	230.775713	√ √ √
50,000,000	222.910643	242.405967	
100,000,000	263.574246	263.574246


如果使用Spark提供的Python Shell,同样编写Pandas加载数据,时间会短25秒左右,看来Spark对Python的内存使用都有优化。

数据清洗
Pandas提供了 DataFrame.describe 方法查看数据摘要,包括数据查看(默认共输出首尾60行数据)和行列统计。由于源数据通常包含一些空值甚至空列,会影响数据分析的时间和效率,在预览了数据摘要后,需要对这些无效数据进行处理。

首先调用 DataFrame.isnull() 方法查看数据表中哪些为空值,与它相反的方法是 DataFrame.notnull() ,Pandas会将表中所有数据进行null计算,以True/False作为结果进行填充,如下图所示:
在这里插入图片描述
Pandas的非空计算速度很快,9800万数据也只需要28.7秒。得到初步信息之后,可以对表中空列进行移除操作。尝试了按列名依次计算获取非空列,和 DataFrame.dropna() 两种方式,时间分别为367.0秒和345.3秒,但检查时发现 dropna() 之后所有的行都没有了,查了Pandas手册,原来不加参数的情况下, dropna() 会移除所有包含空值的行。如果只想移除全部为空值的列,需要加上 axis 和 how 两个参数:

df.dropna(axis=1, how='all')

共移除了14列中的6列,时间也只消耗了85.9秒。

接下来是处理剩余行中的空值,经过测试,在 DataFrame.replace() 中使用空字符串,要比默认的空值NaN节省一些空间;但对整个CSV文件来说,空列只是多存了一个“,”,所以移除的9800万 x 6列也只省下了200M的空间。进一步的数据清洗还是在移除无用数据和合并上。

对数据列的丢弃,除无效值和需求规定之外,一些表自身的冗余列也需要在这个环节清理,比如说表中的流水号是某两个字段拼接、类型描述等,通过对这些数据的丢弃,新的数据文件大小为4.73GB,足足减少了4.04G!

数据处理
使用 DataFrame.dtypes 可以查看每列的数据类型,Pandas默认可以读出int和float64,其它的都处理为object,需要转换格式的一般为日期时间。DataFrame.astype() 方法可对整个DataFrame或某一列进行数据格式转换,支持Python和NumPy的数据类型。

df['Name'] = df['Name'].astype(np.datetime64)

对数据聚合,我测试了 DataFrame.groupby 和 DataFrame.pivot_table 以及 pandas.merge ,groupby 9800万行 x 3列的时间为99秒,连接表为26秒,生成透视表的速度更快,仅需5秒。

df.groupby(['NO','TIME','SVID']).count() # 分组
fullData = pd.merge(df, trancodeData)[['NO','SVID','TIME','CLASS','TYPE']] # 连接
actions = fullData.pivot_table('SVID', columns='TYPE', aggfunc='count') # 透视表

根据透视表生成的交易/查询比例饼图:
在这里插入图片描述
将日志时间加入透视表并输出每天的交易/查询比例图:

total_actions = fullData.pivot_table('SVID', index='TIME', columns='TYPE', aggfunc='count')
total_actions.plot(subplots=False, figsize=(18,6), kind='area')

在这里插入图片描述
除此之外,Pandas提供的DataFrame查询统计功能速度表现也非常优秀,7秒以内就可以查询生成所有类型为交易的数据子表:

tranData = fullData[fullData['Type'] == 'Transaction']

该子表的大小为 [10250666 rows x 5 columns]。在此已经完成了数据处理的一些基本场景。实验结果足以说明,在非“>5TB”数据的情况下,Python的表现已经能让擅长使用统计分析语言的数据分析师游刃有余。

2019-03-06 22:33:00 u011984148 阅读数 79
  • Python数据分析实战-Pandas

    深度学习、机器学习和数据分析必须用pandas。pandas是在python直接流行的数据处理框架。可以说,如果不会使用pandas,就谈不上会用python做数据分析。本课程会使用奥林匹克一个真实的数据作为实验数据,从初级到各种pandas的常用操作,到常用的数据可视化,让你在短的时间内掌握好pandas,轻松愉快的玩转数据分析。

    2319 人正在学习 去看看 阿勒拉哈

点击上方“AI公园”,关注公众号,选择加“星标“或“置顶”


者:Admond Lee

编译:ronghuaiyang

前戏

我们用Pandas来处理大量数据,而不是大数据,为什么呢?一起来看看吧。

640?wx_fmt=jpeg

pandas是Python编程语言中用于数据整理和分析的最流行和最受欢迎的数据科学工具之一。

在现实世界中,数据的混乱是不可避免。在清理、转换、操作和分析数据方面,pandas是一个“真正的”游戏改变者。简单地说,pandas帮助清理脏乱差。

我的NumPy和Pandas的故事

当我第一次开始学习Python时,我很自然地接触了NumPy(Numerical Python)。它是使用Python进行科学计算的基本包,提供了Python中对n-array和矩阵进行操作的大量有用特性。

此外,该库还提供了对NumPy数组类型的数学操作的向量化,极大地优化了计算,提高了执行速度和性能。

NumPy很酷。

但是仍然存在着对更高层次数据分析工具的一些潜在需求。这就是pandas拯救我的地方。

本质上,pandas的功能构建在NumPy之上,两个库都属于SciPy。这意味着pandas在很大程度上依赖于NumPy数组来实现它的对象来进行操作和计算—但是使用起来更加方便。

在实践中,NumPy和panda仍然可以互换使用。高级的特点和方便的使用决定了我对pandas的偏爱。

为什么要用pandas来处理大量数据——而不是大数据?

大量数据和大数据之间有着明显的区别。随着围绕大数据的炒作,我们很容易将所有事情都视为大数据,顺其自然。

Dan Ariely教授有一个著名的笑话:

640?wx_fmt=png

大量和大这两个词本身就是“相对的”,在我看来,大量就是小于100GB的数据集。

panda对于小数据(通常从100MB到1GB)非常有效,性能很少受到关注。

然而,如果你从事数据科学或大数据领域,那么在处理大型数据集时,你迟早会遇到一个常见的问题—性能低下和运行时间长,最终导致内存使用不足。

事实上,由于算法和本地内存的限制,pandas在大数据方面有自己的局限性。因此,大数据通常存储在计算集群中,具有更高的可伸缩性和容错性。并且经常可以通过大数据生态系统(AWS EC2、Hadoop等)使用Spark和许多其他工具来访问。

最后,一种在本地机器(具有一定的内存限制)上,用pandas来处理大量数据的方法是减少数据的内存使用。

如何在大量数据上使用Pandas?

640?wx_fmt=jpeg

所以问题是:如何使用pandas减少数据的内存使用?

下面的解释基于我的经验和一个匿名的大数据集(40 - 50gb),它要求我减少内存使用以适应本地内存进行分析(甚至在将数据集读入dataframe之前)。

1. 按块大小读取CSV文件

说实话,当时我遇到一个错误,我无法从CSV文件中读取数据,我感到很困惑,但时我发现我的本地机器的16GB RAM内存对于数据来说太小了。

然后好消息就来了:我意识到pandas.read_csv有一个名为chunksize的参数!

该参数本质上是指在任何时间为适应本地内存而读入dataframe的行数。由于数据由7000多万行组成,因此我将chunksize指定为每次100万行,每次100万行将大型数据集分解成许多小块。

# read the large csv file with specified chunksize 	
df_chunk = pd.read_csv(r'../input/data.csv', chunksize=1000000)
按块大小读取CSV文件

上面的操作产生了一个用于迭代的TextFileReader对象。严格地说,df_chunk不是一个dataframe,而是一个用于下一步操作的对象。

一旦我准备好了对象,基本的工作流就是对每个块执行操作,并将它们连接起来,最后形成一个dataframe(如下所示)。通过迭代每个块,在将每个块添加到列表之前,我使用一个函数- chunk_preprocessing执行数据过滤/预处理。最后,我将这个列表连接到最终的dataframe中,以适应本地内存。

chunk_list = []  # append each chunk df here 	
# Each chunk is in df format	
for chunk in df_chunk:  	
    # perform data filtering 	
    chunk_filter = chunk_preprocessing(chunk)	
    # Once the data filtering is done, append the chunk to list	
    chunk_list.append(chunk_filter)	
# concat the list into dataframe 	
df_concat = pd.concat(chunk_list)
对每个块执行操作的工作流

2. 过滤掉不重要的列以节约内存

太好了。在这个阶段,我已经有了一个dataframe来执行所需的各种分析。

为了节省数据操作和计算的时间,我进一步过滤掉了一些不重要的列,以节省更多的内存。

# Filter out unimportant columns	
df = df[['col_1','col_2', 'col_3', 'col_4', 'col_5', 'col_6','col_7', 'col_8', 'col_9', 'col_10']]
过滤掉不重要的列

3. 更改列的类型

将pandas数据列转换为另一种类型的最简单方法是使用 astype()

我可以说,更改pandas中的数据类型对于节省内存非常有帮助,特别是如果你有大量数据用于高强度的分析或计算(例如,将数据输入你的机器学习模型中进行训练)。

通过减少存储数据所需的比特位,我将数据的总体内存使用量减少了50% !

试试吧。我相信你也会发现这很有用!让我知道进展如何。?

# Change the dtypes (int64 -> int32)	
df[['col_1','col_2', 	
    'col_3', 'col_4', 'col_5']] = df[['col_1','col_2', 	
                                      'col_3', 'col_4', 'col_5']].astype('int32')	
# Change the dtypes (float64 -> float32)	
df[['col_6', 'col_7',	
    'col_8', 'col_9', 'col_10']] = df[['col_6', 'col_7',	
                                       'col_8', 'col_9', 'col_10']].astype('float32')
更改数据类型节省内存

最后的想法

640?wx_fmt=jpeg

好了。感谢你的阅读。

我希望通过分享我在使用大数据时使用panda的经验,可以帮助你通过减少内存使用并最终提高计算效率来探索pandas处理大量数据的另一个有用特性。

通常,pandas具有我们需要进行数据处理和分析的大多数特性。我强烈建议你去看看它们,因为它们下次会对你有用的。

此外,如果你认真学习如何用Python进行数据分析,那么这本书是为你准备的—Python for Data Analysis。里面有在Python中使用pandas操作、处理、清理和处理数据集的完整说明,本书提供了一个全面和逐步的指南,有效地指导你在数据分析中使用pandas。

希望可以对你帮助!

640?wx_fmt=pngEND

英文原文:https://towardsdatascience.com/why-and-how-to-use-pandas-with-large-data-9594dda2ea4c

640?wx_fmt=jpeg

请长按或扫描二维码关注本公众号

喜欢的话,请给我个好看吧640?wx_fmt=gif

2017-08-29 21:54:51 wally21st 阅读数 4753
  • Python数据分析实战-Pandas

    深度学习、机器学习和数据分析必须用pandas。pandas是在python直接流行的数据处理框架。可以说,如果不会使用pandas,就谈不上会用python做数据分析。本课程会使用奥林匹克一个真实的数据作为实验数据,从初级到各种pandas的常用操作,到常用的数据可视化,让你在短的时间内掌握好pandas,轻松愉快的玩转数据分析。

    2319 人正在学习 去看看 阿勒拉哈

原文地址https://www.dataquest.io/blog/pandas-big-data/

一般来说,用pandas处理小于100兆的数据,性能不是问题。当用pandas来处理100兆至几个G的数据时,将会比较耗时,同时会导致程序因内存不足而运行失败。

当然,像Spark这类的工具能够胜任处理100G至几个T的大数据集,但要想充分发挥这些工具的优势,通常需要比较贵的硬件设备。而且,这些工具不像pandas那样具有丰富的进行高质量数据清洗、探索和分析的特性。对于中等规模的数据,我们的愿望是尽量让pandas继续发挥其优势,而不是换用其他工具。

本文我们讨论pandas的内存使用,展示怎样简单地为数据列选择合适的数据类型,就能够减少dataframe近90%的内存占用。

处理棒球比赛记录数据

我们将处理130年的棒球甲级联赛的数据,数据源于Retrosheet

原始数据放在127个csv文件中,我们已经用csvkit将其合并,并添加了表头。如果你想下载我们版本的数据用来运行本文的程序,我们提供了下载地址

我们从导入数据,并输出前5行开始:

import pandas as pd

gl = pd.read_csv('game_logs.csv')
gl.head()
  date number_of_game day_of_week v_name v_league v_game_number h_name
0 18710504 0 Thu CL1 na 1 FW1
1 18710505 0 Fri BS1 na 1 WS3
2 18710506 0 Sat CL1 na 2 RC1
3 18710508 0 Mon CL1 na 3 CH1
4 18710509 0 Tue BS1 na 2 TRO

我们将一些重要的字段列在下面:

  • date - 比赛日期
  • v_name - 客队名
  • v_league - 客队联赛
  • h_name - 主队名
  • h_league - 主队联赛
  • v_score - 客队得分
  • h_score - 主队得分
  • v_line_score - 客队线得分, 如010000(10)00.
  • h_line_score- 主队线得分, 如010000(10)0X.
  • park_id - 主办场地的ID
  • attendance- 比赛出席人数

我们可以用Dataframe.info()方法来获得我们dataframe的一些高level信息,譬如数据量、数据类型和内存使用量。

这个方法默认情况下返回一个近似的内存使用量,现在我们设置参数memory_usage'deep'来获得准确的内存使用量:

gl.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 171907 entries, 0 to 171906
Columns: 161 entries, date to acquisition_info
dtypes: float64(77), int64(6), object(78)
memory usage: 861.6 MB

我们可以看到它有171907行和161列。pandas已经为我们自动检测了数据类型,其中包括83列数值型数据和78列对象型数据。对象型数据列用于字符串或包含混合数据类型的列。

由此我们可以进一步了解我们应该如何减少内存占用,下面我们来看一看pandas如何在内存中存储数据。

Dataframe对象的内部表示

在底层,pandas会按照数据类型将列分组形成数据块(blocks)。下图所示为pandas如何存储我们数据表的前十二列:

可以注意到,这些数据块没有保持对列名的引用,这是由于为了存储dataframe中的真实数据,这些数据块都经过了优化。有个BlockManager类会用于保持行列索引与真实数据块的映射关系。他扮演一个API,提供对底层数据的访问。每当我们查询、编辑或删除数据时,dataframe类会利用BlockManager类接口将我们的请求转换为函数和方法的调用。

每种数据类型在pandas.core.internals模块中都有一个特定的类。pandas使用ObjectBlock类来表示包含字符串列的数据块,用FloatBlock类来表示包含浮点型列的数据块。对于包含数值型数据(比如整型和浮点型)的数据块,pandas会合并这些列,并把它们存储为一个Numpy数组(ndarray)。Numpy数组是在C数组的基础上创建的,其值在内存中是连续存储的。基于这种存储机制,对其切片的访问是相当快的。

由于不同类型的数据是分开存放的,我们将检查不同数据类型的内存使用情况,我们先看看各数据类型的平均内存使用量:

for dtype in ['float','int','object']:
    selected_dtype = gl.select_dtypes(include=[dtype])
    mean_usage_b = selected_dtype.memory_usage(deep=True).mean()
    mean_usage_mb = mean_usage_b / 1024 ** 2
    print("Average memory usage for {} columns: {:03.2f} MB".format(dtype,mean_usage_mb))
Average memory usage for float columns: 1.29 MB
Average memory usage for int columns: 1.12 MB
Average memory usage for object columns: 9.53 MB

我们可以看到内存使用最多的是78个object列,我们待会再来看它们,我们先来看看我们能否提高数值型列的内存使用效率。

理解子类型(Subtypes)

刚才我们提到,pandas在底层将数值型数据表示成Numpy数组,并在内存中连续存储。这种存储方式消耗较少的空间,并允许我们较快速地访问数据。由于pandas使用相同数量的字节来表示同一类型的每一个值,并且numpy数组存储了这些值的数量,所以pandas能够快速准确地返回数值型列所消耗的字节量。

pandas中的许多数据类型具有多个子类型,它们可以使用较少的字节去表示不同数据,比如,float型就有float16float32float64这些子类型。这些类型名称的数字部分表明了这种类型使用了多少比特来表示数据,比如刚才列出的子类型分别使用了2、4、8个字节。下面这张表列出了pandas中常用类型的子类型:

memory usage float int uint datetime bool object
1 bytes int8 uint8 bool
2 bytes float16 int16 uint16
4 bytes float32 int32 uint32
8 bytes float64 int64 uint64 datetime64
variable object

一个int8类型的数据使用1个字节(8位比特)存储一个值,可以表示256(2^8)个二进制数值。这意味着我们可以用这种子类型去表示从-128到127(包括0)的数值。

我们可以用numpy.iinfo类来确认每一个整型子类型的最小和最大值,如下:

import numpy as np
int_types = ["uint8", "int8", "int16"]
for it in int_types:
    print(np.iinfo(it))
Machine parameters for uint8
-----------------------------------------------------
min = 0
max = 255
-----------------------------------------------------

Machine parameters for int8
-----------------------------------------------------
min = -128
max = 127
-----------------------------------------------------

Machine parameters for int16
-----------------------------------------------------
min = -32768
max = 32767
-----------------------------------------------------

这里我们还可以看到uint(无符号整型)和int(有符号整型)的区别。两者都占用相同的内存存储量,但无符号整型由于只存正数,所以可以更高效的存储只含正数的列。

用子类型优化数值型列

我们可以用函数pd.to_numeric()来对数值型进行向下类型转换。我们用DataFrame.select_dtypes来只选择整型列,然后我们优化这种类型,并比较内存使用量。

# We're going to be calculating memory usage a lot,
# so we'll create a function to save us some time!

def mem_usage(pandas_obj):
    if isinstance(pandas_obj,pd.DataFrame):
        usage_b = pandas_obj.memory_usage(deep=True).sum()
    else: # we assume if not a df it's a series
        usage_b = pandas_obj.memory_usage(deep=True)
    usage_mb = usage_b / 1024 ** 2 # convert bytes to megabytes
    return "{:03.2f} MB".format(usage_mb)

gl_int = gl.select_dtypes(include=['int'])
converted_int = gl_int.apply(pd.to_numeric,downcast='unsigned')

print(mem_usage(gl_int))
print(mem_usage(converted_int))

compare_ints = pd.concat([gl_int.dtypes,converted_int.dtypes],axis=1)
compare_ints.columns = ['before','after']
compare_ints.apply(pd.Series.value_counts)
7.87 MB
1.48 MB
  before after
uint8 NaN 5.0
uint32 NaN 1.0
int64 6.0 NaN

我们看到内存用量从7.9兆下降到1.5兆,降幅达80%。这对我们原始dataframe的影响有限,这是由于它只包含很少的整型列。

同理,我们再对浮点型列进行相应处理:

gl_float = gl.select_dtypes(include=['float'])
converted_float = gl_float.apply(pd.to_numeric,downcast='float')

print(mem_usage(gl_float))
print(mem_usage(converted_float))

compare_floats = pd.concat([gl_float.dtypes,converted_float.dtypes],axis=1)
compare_floats.columns = ['before','after']
compare_floats.apply(pd.Series.value_counts)
100.99 MB
50.49 MB
  before after
float32 NaN 77.0
float64 77.0 NaN

我们可以看到所有的浮点型列都从float64转换为float32,内存用量减少50%。

我们再创建一个原始dataframe的副本,将其数值列赋值为优化后的类型,再看看内存用量的整体优化效果。

optimized_gl = gl.copy()

optimized_gl[converted_int.columns] = converted_int
optimized_gl[converted_float.columns] = converted_float

print(mem_usage(gl))
print(mem_usage(optimized_gl))
861.57 MB
804.69 MB

可以看到通过我们显著缩减数值型列的内存用量,我们的dataframe的整体内存用量减少了7%。余下的大部分优化将针对object类型进行。

在这之前,我们先来研究下与数值型相比,pandas如何存储字符串。

对比数值与字符的存储

object类型用来表示用到了Python字符串对象的值,有一部分原因是Numpy缺少对缺失字符串值的支持。因为Python是一种高层、解析型语言,它没有提供很好的对内存中数据如何存储的细粒度控制。

这一限制导致了字符串以一种碎片化方式进行存储,消耗更多的内存,并且访问速度低下。在object列中的每一个元素实际上都是存放内存中真实数据位置的指针。

下图对比展示了数值型数据怎样以Numpy数据类型存储,和字符串怎样以Python内置类型进行存储的。


图示来源并改编自Why Python Is Slow

你可能注意到上文表中提到object类型数据使用可变(variable)大小的内存。由于一个指针占用1字节,因此每一个字符串占用的内存量与它在Python中单独存储所占用的内存量相等。我们用sys.getsizeof()来证明这一点,先来看看在Python单独存储字符串,再来看看使用pandas的series的情况。

from sys import getsizeof

s1 = 'working out'
s2 = 'memory usage for'
s3 = 'strings in python is fun!'
s4 = 'strings in python is fun!'

for s in [s1, s2, s3, s4]:
    print(getsizeof(s))
60
65
74
74
obj_series = pd.Series(['working out',
                          'memory usage for',
                          'strings in python is fun!',
                          'strings in python is fun!'])
obj_series.apply(getsizeof)
0    60
1    65
2    74
3    74
dtype: int64

你可以看到这些字符串的大小在pandas的series中与在Python的单独字符串中是一样的。

用类别(categoricals)类型优化object类型

Pandas在0.15版本中引入类别类型。category类型在底层使用整型数值来表示该列的值,而不是用原值。Pandas用一个字典来构建这些整型数据到原数据的映射关系。当一列只包含有限种值时,这种设计是很不错的。当我们把一列转换成category类型时,pandas会用一种最省空间的int子类型去表示这一列中所有的唯一值。

为了介绍我们何处会用到这种类型去减少内存消耗,让我们来看看我们数据中每一个object类型列中的唯一值个数。

gl_obj = gl.select_dtypes(include=['object']).copy()
gl_obj.describe()
  day_of_week v_name v_league h_name h_league day_night
count 171907 171907 171907 171907 171907 140150
unique 7 148 7 148 7 2
top Sat CHN NL CHN NL D
freq 28891 8870 88866 9024 88867 82724

可以看到在我们包含了近172000场比赛的数据集中,很多列只包含了少数几个唯一值。

我们先选择其中一个object列,开看看将其转换成类别类型会发生什么。这里我们选用第二列:day_of_week

我们从上表中可以看到,它只包含了7个唯一值。我们用.astype()方法将其转换为类别类型。

dow = gl_obj.day_of_week
print(dow.head())

dow_cat = dow.astype('category')
print(dow_cat.head())
0    Thu
1    Fri
2    Sat
3    Mon
4    Tue
Name: day_of_week, dtype: object
0    Thu
1    Fri
2    Sat
3    Mon
4    Tue
Name: day_of_week, dtype: category
Categories (7, object): [Fri, Mon, Sat, Sun, Thu, Tue, Wed]

可以看到,虽然列的类型改变了,但数据看上去好像没什么变化。我们来看看底层发生了什么。

下面的代码中,我们用Series.cat.codes属性来返回category类型用以表示每个值的整型数字。

dow_cat.head().cat.codes
0    4
1    0
2    2
3    1
4    5
dtype: int8

可以看到,每一个值都被赋值为一个整数,而且这一列在底层是int8类型。这一列没有任何缺失数据,但是如果有,category子类型会将缺失数据设为-1。

最后,我们来看看这一列在转换为category类型前后的内存使用量。

print(mem_usage(dow))
print(mem_usage(dow_cat))
9.84 MB
0.16 MB

内存用量从9.8兆降到0.16兆,近乎98%的降幅!注意这一特殊列可能代表了我们一个极好的例子——一个包含近172000个数据的列只有7个唯一值。

这样的话,我们把所有这种类型的列都转换成类别类型应该会很不错,但这里面也要权衡利弊。首要问题是转变为类别类型会丧失数值计算能力,在将类别类型转换成真实的数值类型前,我们不能对category列做算术运算,也不能使用诸如Series.min()Series.max()等方法。

对于唯一值数量少于50%的object列,我们应该坚持首先使用category类型。如果某一列全都是唯一值,category类型将会占用更多内存。这是因为这样做不仅要存储全部的原始字符串数据,还要存储整型类别标识。有关category类型的更多限制,参看pandas文档

下面我们写一个循环,对每一个object列进行迭代,检查其唯一值是否少于50%,如果是,则转换成类别类型。

converted_obj = pd.DataFrame()

for col in gl_obj.columns:
    num_unique_values = len(gl_obj[col].unique())
    num_total_values = len(gl_obj[col])
    if num_unique_values / num_total_values < 0.5:
        converted_obj.loc[:,col] = gl_obj[col].astype('category')
    else:
        converted_obj.loc[:,col] = gl_obj[col]

更之前一样进行比较:

print(mem_usage(gl_obj))
print(mem_usage(converted_obj))

compare_obj = pd.concat([gl_obj.dtypes,converted_obj.dtypes],axis=1)
compare_obj.columns = ['before','after']
compare_obj.apply(pd.Series.value_counts)
752.72 MB
51.67 MB
  before after
object 78.0 NaN
category NaN 78.0

这本例中,所有的object列都被转换成了category类型,但其他数据集就不一定了,所以你最好还是得使用刚才的检查过程。

本例的亮点是内存用量从752.72兆降为51.667兆,降幅达93%。我们将其与我们dataframe的剩下部分合并,看看初始的861兆数据降到了多少。

optimized_gl[converted_obj.columns] = converted_obj

mem_usage(optimized_gl)
'103.64 MB'

耶,看来我们的进展还不错!我们还有一招可以做优化,如果你记得我们刚才那张类型表,会发现我们数据集第一列还可以用datetime类型来表示。

date = optimized_gl.date
print(mem_usage(date))
date.head()
0.66 MB
0    18710504
1    18710505
2    18710506
3    18710508
4    18710509
Name: date, dtype: uint32

你可能还记得这一列之前是作为整型读入的,并优化成了uint32。因此,将其转换成datetime会占用原来两倍的内存,因为datetime类型是64位比特的。将其转换为datetime的意义在于它可以便于我们进行时间序列分析。

转换使用pandas.to_datetime()函数,并使用format参数告之日期数据存储为YYYY-MM-DD格式。

optimized_gl['date'] = pd.to_datetime(date,format='%Y%m%d')

print(mem_usage(optimized_gl))
optimized_gl.date.head()
104.29 MB
0   1871-05-04
1   1871-05-05
2   1871-05-06
3   1871-05-08
4   1871-05-09
Name: date, dtype: datetime64[ns]

在数据读入的时候设定数据类型

目前为止,我们探索了一些方法,用来减少现有dataframe的内存占用。通过首先读入dataframe,再对其一步步进行内存优化,我们可以更好地了解这些优化方法能节省多少内存。然而,正如我们之前谈到,我们通常没有足够的内存去表达数据集中的所有数据。如果不能在一开始就创建dataframe,我们怎样才能应用内存节省技术呢?

幸运的是,我们可以在读入数据集的时候指定列的最优数据类型。pandas.read_csv()函数有一些参数可以做到这一点。dtype参数接受一个以列名(string型)为键字典、以Numpy类型对象为值的字典。

首先,我们将每一列的目标类型存储在以列名为键的字典中,开始前先删除日期列,因为它需要分开单独处理。

dtypes = optimized_gl.drop('date',axis=1).dtypes

dtypes_col = dtypes.index
dtypes_type = [i.name for i in dtypes.values]

column_types = dict(zip(dtypes_col, dtypes_type))

# rather than print all 161 items, we'll
# sample 10 key/value pairs from the dict
# and print it nicely using prettyprint

preview = first2pairs = {key:value for key,value in list(column_types.items())[:10]}
import pprint
pp = pp = pprint.PrettyPrinter(indent=4)
pp.pprint(preview)
{   'acquisition_info': 'category',
    'h_caught_stealing': 'float32',
    'h_player_1_name': 'category',
    'h_player_9_name': 'category',
    'v_assists': 'float32',
    'v_first_catcher_interference': 'float32',
    'v_grounded_into_double': 'float32',
    'v_player_1_id': 'category',
    'v_player_3_id': 'category',
    'v_player_5_id': 'category'}

现在我们使用这个字典,同时传入一些处理日期的参数,让日期以正确的格式读入。

read_and_optimized = pd.read_csv('game_logs.csv',dtype=column_types,parse_dates=['date'],infer_datetime_format=True)

print(mem_usage(read_and_optimized))
read_and_optimized.head()
104.28 MB
  date number_of_game day_of_week v_name v_league v_game_number h_name
0 1871-05-04 0 Thu CL1 na 1 FW1
1 1871-05-05 0 Fri BS1 na 1 WS3
2 1871-05-06 0 Sat CL1 na 2 RC1
3 1871-05-08 0 Mon CL1 na 3 CH1
4 1871-05-09 0 Tue BS1 na 2 TRO

通过对列的优化,我们是pandas的内存用量从861.6兆降到104.28兆,有效降低88%。

分析棒球比赛

现在我们有了优化后的数据,可以进行一些分析。我们先看看比赛日的分布情况。

optimized_gl['year'] = optimized_gl.date.dt.year
games_per_day = optimized_gl.pivot_table(index='year',columns='day_of_week',values='date',aggfunc=len)
games_per_day = games_per_day.divide(games_per_day.sum(axis=1),axis=0)

ax = games_per_day.plot(kind='area',stacked='true')
ax.legend(loc='upper right')
ax.set_ylim(0,1)
plt.show()

我们可以看到,1920年代之前,周日棒球赛很少是在周日的,随后半个世纪才逐渐增多。

我们也看到最后50年的比赛日分布变化相对比较平稳。

我们来看看比赛时长的逐年变化。

game_lengths = optimized_gl.pivot_table(index='year', values='length_minutes')
game_lengths.reset_index().plot.scatter('year','length_minutes')
plt.show()

看来棒球比赛时长从1940年代之后逐渐变长。

总结

我们学习了pandas如何存储不同的数据类型,并利用学到的知识将我们的pandas dataframe的内存用量降低了近90%,仅仅只用了一点简单的技巧:

  • 将数值型列降级到更高效的类型
  • 将字符串列转换为类别类型
没有更多推荐了,返回首页