Lendo um arquivo .csv enorme

107

Atualmente, estou tentando ler dados de arquivos .csv no Python 2.7 com até 1 milhão de linhas e 200 colunas (os arquivos variam de 100 MB a 1,6 GB). Posso fazer isso (muito lentamente) para os arquivos com menos de 300.000 linhas, mas quando vou além disso, recebo erros de memória. Meu código é parecido com este:

def getdata(filename, criteria):
    data=[]
    for criterion in criteria:
        data.append(getstuff(filename, criteron))
    return data

def getstuff(filename, criterion):
    import csv
    data=[]
    with open(filename, "rb") as csvfile:
        datareader=csv.reader(csvfile)
        for row in datareader: 
            if row[3]=="column header":
                data.append(row)
            elif len(data)<2 and row[3]!=criterion:
                pass
            elif row[3]==criterion:
                data.append(row)
            else:
                return data

O motivo da cláusula else na função getstuff é que todos os elementos que se enquadram no critério serão listados juntos no arquivo csv, portanto, deixo o loop quando passar por eles para economizar tempo.

Minhas perguntas são:

  1. Como posso fazer isso funcionar com os arquivos maiores?

  2. Existe alguma maneira de tornar isso mais rápido?

Meu computador tem 8 GB de RAM, executando o Windows 7 de 64 bits, e o processador tem 3,40 GHz (não tenho certeza de quais informações você precisa).

Charles Dillon
fonte
1
Estou ciente de que há várias perguntas semelhantes, mas nenhuma delas parecia ser específica o suficiente para ajudar muito. Desculpe se perdi um.
Charles Dillon
2
Você deve armazenar os dados lidos em um banco de dados (por exemplo, Sqlite) em vez de mantê-los na memória. Você pode então executar processamento adicional, como filtragem no banco de dados
Michael Butscher

Respostas:

159

Você está lendo todas as linhas em uma lista e, em seguida, processando essa lista. Não faça isso .

Processe suas linhas conforme você as produz. Se você precisar filtrar os dados primeiro, use uma função geradora:

import csv

def getstuff(filename, criterion):
    with open(filename, "rb") as csvfile:
        datareader = csv.reader(csvfile)
        yield next(datareader)  # yield the header row
        count = 0
        for row in datareader:
            if row[3] == criterion:
                yield row
                count += 1
            elif count:
                # done when having read a consecutive series of rows 
                return

Também simplifiquei seu teste de filtro; a lógica é a mesma, mas mais concisa.

Como você está correspondendo apenas a uma única sequência de linhas que correspondem ao critério, você também pode usar:

import csv
from itertools import dropwhile, takewhile

def getstuff(filename, criterion):
    with open(filename, "rb") as csvfile:
        datareader = csv.reader(csvfile)
        yield next(datareader)  # yield the header row
        # first row, plus any subsequent rows that match, then stop
        # reading altogether
        # Python 2: use `for row in takewhile(...): yield row` instead
        # instead of `yield from takewhile(...)`.
        yield from takewhile(
            lambda r: r[3] == criterion,
            dropwhile(lambda r: r[3] != criterion, datareader))
        return

Agora você pode fazer um loop getstuff()diretamente. Faça o mesmo em getdata():

def getdata(filename, criteria):
    for criterion in criteria:
        for row in getstuff(filename, criterion):
            yield row

Agora faça um loop diretamente getdata()em seu código:

for row in getdata(somefilename, sequence_of_criteria):
    # process row

Você agora mantém apenas uma linha na memória, em vez de seus milhares de linhas por critério.

yieldtorna uma função uma função geradora , o que significa que ela não fará nenhum trabalho até que você comece a fazer um loop sobre ela.

Martijn Pieters
fonte
você obtém a mesma eficiência de memória ao usar essa técnica com csv.DictReader? Porque meus testes em um arquivo .csv de 2,5 GB mostram que tentar iterar linha por linha dessa forma ao usar isso, em vez de fazer csv.readercom que o processo Python cresça até o uso total de memória de 2,5 GB.
user5359531
@ user5359531 que indica que você mantém referências aos objetos de dicionário em algum lugar. O DictReader por si só não retém referências, então o problema está em outro lugar.
Martijn Pieters
39

Embora a resposta de Martijin seja provavelmente a melhor. Esta é uma maneira mais intuitiva de processar arquivos csv grandes para iniciantes. Isso permite que você processe grupos de linhas, ou pedaços, de uma vez.

import pandas as pd
chunksize = 10 ** 8
for chunk in pd.read_csv(filename, chunksize=chunksize):
    process(chunk)
mmann1123
fonte
9
Por que usar o pandas o torna mais intuitivo?
wwii
25
4 linhas de código é sempre melhor para iniciantes como eu.
mmann1123
3
O código Python regular é igualmente curto e permite que você processe por linha. A função do gerador existe apenas para filtrar coisas; como você faria para fazer a mesma filtragem no Pandas?
Martijn Pieters
1
Isso é incrível! Resolvi meu problema de carregar e processar grandes arquivos csv usando o pandas. Obrigado!
Elsa Li
1
Funciona muito bem mesmo quando o conteúdo de algumas linhas abrange várias linhas!
Dielson Sales
19

Eu faço uma boa análise de vibração e vejo grandes conjuntos de dados (dezenas e centenas de milhões de pontos). Meus testes mostraram que a função pandas.read_csv () é 20 vezes mais rápida do que numpy.genfromtxt (). E a função genfromtxt () é 3 vezes mais rápida do que numpy.loadtxt (). Parece que você precisa de pandas para grandes conjuntos de dados.

Publiquei o código e os conjuntos de dados que usei neste teste em um blog discutindo MATLAB vs Python para análise de vibração .

Steve
fonte
3
O principal problema do OP não era a velocidade, era o esgotamento da memória. Usar uma função diferente para processar o próprio arquivo não remove as desvantagens de lê-lo em uma lista em vez de usar um processador de fluxo.
pydsigner
6

o que funcionou para mim foi e é super rápido é

import pandas as pd
import dask.dataframe as dd
import time
t=time.clock()
df_train = dd.read_csv('../data/train.csv', usecols=[col1, col2])
df_train=df_train.compute()
print("load train: " , time.clock()-t)

Outra solução de trabalho é:

import pandas as pd 
from tqdm import tqdm

PATH = '../data/train.csv'
chunksize = 500000 
traintypes = {
'col1':'category',
'col2':'str'}

cols = list(traintypes.keys())

df_list = [] # list to hold the batch dataframe

for df_chunk in tqdm(pd.read_csv(PATH, usecols=cols, dtype=traintypes, chunksize=chunksize)):
    # Can process each chunk of dataframe here
    # clean_data(), feature_engineer(),fit()

    # Alternatively, append the chunk to list and merge all
    df_list.append(df_chunk) 

# Merge all dataframes into one dataframe
X = pd.concat(df_list)

# Delete the dataframe list to release memory
del df_list
del df_chunk
Carteira Yury
fonte
a df_train=df_train.compute()linha em sua primeira solução não carrega o conjunto de dados inteiro na memória ... que é o que ele está tentando não fazer?
Sam Dillard
3

Para quem chega a esta questão. Usar pandas com ' chunksize ' e ' usecols ' me ajudou a ler um arquivo zip enorme mais rápido do que as outras opções propostas.

import pandas as pd

sample_cols_to_keep =['col_1', 'col_2', 'col_3', 'col_4','col_5']

# First setup dataframe iterator, ‘usecols’ parameter filters the columns, and 'chunksize' sets the number of rows per chunk in the csv. (you can change these parameters as you wish)
df_iter = pd.read_csv('../data/huge_csv_file.csv.gz', compression='gzip', chunksize=20000, usecols=sample_cols_to_keep) 

# this list will store the filtered dataframes for later concatenation 
df_lst = [] 

# Iterate over the file based on the criteria and append to the list
for df_ in df_iter: 
        tmp_df = (df_.rename(columns={col: col.lower() for col in df_.columns}) # filter eg. rows where 'col_1' value grater than one
                                  .pipe(lambda x:  x[x.col_1 > 0] ))
        df_lst += [tmp_df.copy()] 

# And finally combine filtered df_lst into the final lareger output say 'df_final' dataframe 
df_final = pd.concat(df_lst)
Ewalel
fonte
1

aqui está outra solução para Python3:

import csv
with open(filename, "r") as csvfile:
    datareader = csv.reader(csvfile)
    count = 0
    for row in datareader:
        if row[3] in ("column header", criterion):
            doSomething(row)
            count += 1
        elif count > 2:
            break

aqui datareaderestá uma função geradora.

Rishabh Agrahari
fonte
Então, isso funciona tão eficientemente quanto a solução que usa o operador de rendimento. : desculpe, não. A chamada da função de retorno de chamada adiciona mais sobrecarga, especialmente porque você precisa lidar com o estado explicitamente e separadamente.
Martijn Pieters
@MartijnPieters Obrigado. Atualizou a resposta.
Rishabh Agrahari
0

Se você estiver usando o pandas e tiver muita memória RAM (o suficiente para ler todo o arquivo na memória), tente usar pd.read_csvcom low_memory=False, por exemplo:

import pandas as pd
data = pd.read_csv('file.csv', low_memory=False)
Mike T
fonte