問題
- 設(shè)計(jì)的代碼能hold住小規(guī)模數(shù)據(jù)
- 你準(zhǔn)備將該代碼用來處理真實(shí)場(chǎng)景的數(shù)據(jù)
- 但驚喜的是你的代碼崩潰了
- 問題: 你的電腦只有16G內(nèi)存戳表,但現(xiàn)在卻要應(yīng)付50G大小的數(shù)據(jù)草丧。
硬件解決辦法
- 換裝備惠奸,比如64G內(nèi)存的電腦
- 租用云服務(wù)器冰肴,64核432G內(nèi)存壹置,每小時(shí)幾十元
軟件解決辦法
- 壓縮你的數(shù)據(jù)
- 分塊讀取屯仗,一次只讀一塊。
- 對(duì)數(shù)據(jù)進(jìn)行索引標(biāo)注娘荡,只在需要的時(shí)候?qū)雰?nèi)存
本教程涉及
numpy和pandas的三種思維來處理內(nèi)存占用和性能問題
- 壓縮
- 分塊
- 索引
一干旁、 壓縮
- 指的是同樣的信息量數(shù)據(jù),使用更少的內(nèi)存炮沐。
- 在內(nèi)存上壓縮争群,而非在硬盤里壓縮
1.1 壓縮:Numpy dtype
numpy類型 | 介紹 | 數(shù)值范圍 |
---|---|---|
np.int8 | 字節(jié) | (-128 to 127) |
np.int16 | 整數(shù) | (-32768 to 32767) |
np.int32 | 整數(shù) | (-2147483648 to 2147483647) |
np.int64 | 整數(shù) | (-9223372036854775808 to 9223372036854775807) |
np.uint8 | 無符號(hào)整數(shù) | (0 to 255) |
np.uint16 | 無符號(hào)整數(shù) | (0 to 65535) |
np.uint32 | 無符號(hào)整數(shù) | (0 to 4294967295) |
np.uint64 | 無符號(hào)整數(shù) | (0 to 18446744073709551615) |
np.float16 | 半精度浮點(diǎn)數(shù),包括:1 個(gè)符號(hào)位大年,5 個(gè)指數(shù)位换薄,10 個(gè)尾數(shù)位 | |
np.float32 | 單精度浮點(diǎn)數(shù),包括:1 個(gè)符號(hào)位翔试,8 個(gè)指數(shù)位专控,23 個(gè)尾數(shù)位 | |
np.float64 | 雙精度浮點(diǎn)數(shù),包括:1 個(gè)符號(hào)位遏餐,11 個(gè)指數(shù)位,52 個(gè)尾數(shù)位 |
同樣的整數(shù)赢底,用np.int64占用的內(nèi)存是np.int16的4倍
import numpy as np
int64arr = np.ones((1024, 1024), dtype=np.int64)
int16arr = np.ones((1024, 1024), dtype=np.int16)
#占用(內(nèi)存)的字節(jié)數(shù)
print(int64arr.nbytes)
print(int16arr.nbytes)
8388608
2097152
1.2 壓縮: 稀疏的數(shù)組
- 數(shù)組中有大量的0
- 內(nèi)存浪費(fèi)在很多0身上
- 稀疏數(shù)據(jù)只存儲(chǔ)非0數(shù)據(jù)
- 用numpy數(shù)組對(duì)數(shù)據(jù)進(jìn)行插值
- 不同的表達(dá)數(shù)據(jù)的方式
sparse可以壓縮數(shù)據(jù)內(nèi)存占用量失都,看一個(gè)例子
import numpy as np
arr = np.random.random((1024, 1024))
arr[arr < 0.9] = 0
print(arr)
[[0. 0. 0. ... 0. 0.94559922 0. ]
[0. 0. 0. ... 0. 0. 0. ]
[0. 0. 0. ... 0. 0. 0. ]
...
[0.94589484 0. 0. ... 0.96746948 0. 0. ]
[0. 0. 0. ... 0.96236294 0. 0. ]
[0. 0. 0. ... 0. 0. 0. ]]
import sparse #需要安裝sparse
sparse_arr = sparse.COO(arr)
print(sparse_arr)
<COO: shape=(1024, 1024), dtype=float64, nnz=104998, fill_value=0.0>
print(arr.nbytes)
print(sparse_arr.nbytes)
8388608
2519952
1.3 壓縮: Pandas dtype
如果知道數(shù)據(jù)的字段,可以在pandas導(dǎo)入數(shù)據(jù)時(shí)就設(shè)定字段的dtype參數(shù)幸冻,減少不必要的內(nèi)存開支粹庞。例如
import pandas as pd
import numpy as np
#不設(shè)定dtype
df1 = pd.read_csv('data.csv')
df1
trip_id是整數(shù),默認(rèn)pandas用的是np.int64, 我們可以將其設(shè)定為np.int32
#設(shè)定dtype參數(shù)
df2 = pd.read_csv('data.csv', dtype={"trip_id": np.int32})
df2
print(df1['trip_id'].nbytes)
print(df2['trip_id'].nbytes)
40
20
我們可以看到通過指定dtype洽损,trip_id字段占用的內(nèi)存少了一半庞溜。
二、 分塊
2.1 分塊處理全部的數(shù)據(jù)
也可以分塊處理全部的數(shù)據(jù)碑定,最后將結(jié)果再匯總流码,減少電腦的內(nèi)存壓力。比如我們想求長(zhǎng)度為1024的數(shù)組arr中的最大值
import numpy as np
#長(zhǎng)度1024的數(shù)組arr
arr = np.random.random(1024)
arr
array([0.37143228, 0.14093017, 0.67051473, ..., 0.42278493, 0.38588344,
0.11637298])
#一次性求最大
max(arr)
0.9994997367530419
#分塊延刘,匯總求最大
max(max(arr[:500]), max(arr[500:]))
0.9994997367530419
2.2 分塊:Pandas也能分塊
分塊依次讀取漫试,這樣可以對(duì)比電腦內(nèi)存還大的數(shù)據(jù)進(jìn)行運(yùn)算操作。
import pandas as pd
max_record = 0
#分塊依次讀取碘赖,專業(yè)
for chunk in pd.read_csv('my.csv',
chunksize=100):#塊的記錄數(shù)為100條
max_record = max(
max_record,
max(chunk['某個(gè)需要求最大值的字段名'])
)
print(max_record)
598000
2.3 并行: 對(duì)很多塊并行處理
- 如果數(shù)據(jù)塊之間彼此獨(dú)立
- 且對(duì)數(shù)據(jù)塊的計(jì)算也是獨(dú)立的
- 我們可以利用電腦多核進(jìn)行并行運(yùn)算
- 并不會(huì)降低內(nèi)存占用驾荣,但是會(huì)提高運(yùn)行速度
塊的大小外构,需要滿足
- 64G內(nèi)存, 并行數(shù)為1時(shí)播掷,處理的塊數(shù)據(jù)大小不超過60G
- 64G內(nèi)存审编, 并行數(shù)為4時(shí),處理的塊數(shù)據(jù)大小不超過15G
三歧匈、索引
3.1 索引:需要的時(shí)候再調(diào)用
- 索引是對(duì)數(shù)據(jù)的準(zhǔn)確描述
- 索引對(duì)應(yīng)的數(shù)據(jù)一定比內(nèi)存小很多
- 索引能告訴程序數(shù)據(jù)的子集在哪里
3.2 索引 vs 分塊
分塊
需要導(dǎo)入所有的數(shù)據(jù)垒酬, "What is the longest word in this book?"需要研究這本書的每一頁(yè)。索引
只導(dǎo)入數(shù)據(jù)的子集, "How much money did we spend in July?"眯亦,只需要在意July伤溉,其他月份不用考慮。兩者經(jīng)常搭配使用
3.3 索引:Pandas不支持索引
所以需要自定義,實(shí)現(xiàn)索引功能
def get_subset(csvf, field, conditon):
"""
從csv數(shù)據(jù)中抽取出field值為condition的所有數(shù)據(jù)妻率。
csvf: csv文件的路徑
field: 需要的字段
conditon: 字段field需要滿足的條件
"""
return pd.concat(
df[df.field==conditon]
for df in pd.read_csv(csvf, chunksize=1000)
)
3.4 索引: SQLite&pandas
如何讓sqlite數(shù)據(jù)庫(kù)也能分塊
import sqlite
def create_index(csvf, dbname, field):
"""
將csv中的數(shù)據(jù)轉(zhuǎn)移至sqlite數(shù)據(jù)庫(kù)乱顾,并給field創(chuàng)建索引
dbname: sqlite數(shù)據(jù)庫(kù)庫(kù)名
field: 需要?jiǎng)?chuàng)建索引的字段名
"""
db=sqlite.connect("{}.sqlite".format(dbname))
for chunk in pd.read_csv(csvf, chunksize=1000):
chunk.to_sql(dbname, db, if_exists='append')
db.execute("CRESTE INDEX {field} ON {dbname}({field})".format(field=field, dbname=dbname, field=field))
db.close()
def get_subset(dbname, field, conditon):
"""
從dbname中抽取出field值為condition的所有數(shù)據(jù)。
dbname: sqlite數(shù)據(jù)庫(kù)庫(kù)名
field: 需要的字段
condition: 字段field需要滿足的條件
"""
conn = sqlite3.connect("{}.sqlite".format(dbname))
q = ("SELECT * FROM {db} WHERE {field} = {condtion}".format(db=dbname, field=field, condition=conditon))
return pd.read_sql_query(q, conn)
3.5 索引:SQLite vs csv
使用70k voters數(shù)據(jù)對(duì)比
- Cambridge,MA : 70k voters
類型 | 操作 | 內(nèi)存占用情況 |
---|---|---|
CSV | 分塊依次讀取10000行 + 按條件找出需要的數(shù)據(jù) | 574ms |
SQLite | 索引找出需要的數(shù)據(jù) | 10ms |
總結(jié)
- 同樣的問題
- 內(nèi)存快但貴
- 硬盤便宜但慢
- 解決辦法:壓縮宫静、分塊(有條件的并行)走净、索引
- 對(duì)了,如果不差錢孤里,事情會(huì)好辦不少伏伯。。捌袜。