Leyendo un archivo .csv enorme

107

Actualmente estoy tratando de leer datos de archivos .csv en Python 2.7 con hasta 1 millón de filas y 200 columnas (los archivos oscilan entre 100 MB y 1,6 GB). Puedo hacer esto (muy lentamente) para los archivos con menos de 300,000 filas, pero una vez que voy por encima de eso, obtengo errores de memoria. Mi código se ve así:

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

El motivo de la cláusula else en la función getstuff es que todos los elementos que se ajustan al criterio se enumerarán juntos en el archivo csv, así que dejo el bucle cuando los supere para ahorrar tiempo.

Mis preguntas son:

  1. ¿Cómo puedo hacer que esto funcione con archivos más grandes?

  2. ¿Hay alguna forma de hacerlo más rápido?

Mi computadora tiene 8 GB de RAM, ejecuta Windows 7 de 64 bits y el procesador es de 3,40 GHz (no estoy seguro de qué información necesita).

Charles Dillon
fuente
1
Soy consciente de que hay varias preguntas aparentemente similares, pero ninguna de ellas parecía ser lo suficientemente específica para mi problema como para ayudar mucho. Perdón si hay uno que me perdí.
Charles Dillon
2
Debe almacenar los datos leídos en una base de datos (por ejemplo, Sqlite) en lugar de guardarlos en la memoria. Luego puede ejecutar un procesamiento adicional como el filtrado en la base de datos
Michael Butscher

Respuestas:

158

Estás leyendo todas las filas en una lista y luego procesando esa lista. No hagas eso .

Procese sus filas a medida que las produce. Si primero necesita filtrar los datos, use una función de generador:

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

También simplifiqué su prueba de filtro; la lógica es la misma pero más concisa.

Debido a que solo está haciendo coincidir una única secuencia de filas que coinciden con el criterio, también puede 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

Ahora puede recorrer getstuff()directamente. Haz lo mismo en getdata():

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

Ahora repita directamente getdata()su código:

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

Ahora solo tiene una fila en la memoria, en lugar de sus miles de líneas por criterio.

yieldconvierte una función en una función generadora , lo que significa que no funcionará hasta que empiece a recorrerla.

Martijn Pieters
fuente
¿Obtiene la misma eficiencia de memoria cuando usa esta técnica con csv.DictReader? Porque mis pruebas en un archivo .csv de 2.5GB muestran que intentar iterar fila por fila como esta cuando se usa eso en lugar de csv.readerhace que el proceso de Python crezca hasta el uso total de memoria de 2.5GB.
user5359531
@ user5359531 que indicaría que mantiene referencias a los objetos del diccionario en algún lugar. DictReader por sí solo no retiene referencias, por lo que el problema está en otra parte.
Martijn Pieters
39

Aunque la respuesta de Martijin es probablemente la mejor. Aquí hay una forma más intuitiva de procesar archivos csv grandes para principiantes. Esto le permite procesar grupos de filas o fragmentos a la vez.

import pandas as pd
chunksize = 10 ** 8
for chunk in pd.read_csv(filename, chunksize=chunksize):
    process(chunk)
mmann1123
fuente
9
¿Por qué usar pandas lo hace más intuitivo?
Segunda Guerra
25
4 líneas de código siempre son mejores para principiantes como yo.
mmann1123
3
El código Python normal es igualmente corto y le permite procesar por línea. La función de generador solo está ahí para filtrar cosas; ¿Cómo harías el mismo filtrado en Pandas?
Martijn Pieters
1
¡Esto es asombroso! Resolví mi problema de cargar y procesar archivos csv grandes usando pandas. ¡Gracias!
Elsa Li
1
¡Funciona muy bien incluso cuando el contenido de algunas filas abarca varias líneas!
Dielson Sales
19

Hago una buena cantidad de análisis de vibraciones y miro grandes conjuntos de datos (decenas y cientos de millones de puntos). Mis pruebas mostraron que la función pandas.read_csv () es 20 veces más rápida que numpy.genfromtxt (). Y la función genfromtxt () es 3 veces más rápida que numpy.loadtxt (). Parece que necesitas pandas para grandes conjuntos de datos.

Publiqué el código y los conjuntos de datos que usé en esta prueba en un blog en el que se discutía MATLAB vs Python para el análisis de vibraciones .

Steve
fuente
3
El problema principal del OP no era la velocidad, era el agotamiento de la memoria. Usar una función diferente para procesar el archivo en sí no elimina las desventajas de leerlo en una lista en lugar de usar un procesador de flujo.
pydsigner
6

lo que funcionó para mí fue y es superrápido es

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)

Otra solución de trabajo es:

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
Billetera Yury
fuente
¿No df_train=df_train.compute()carga la línea en su primera solución todo el conjunto de datos en la memoria ... que es lo que está tratando de no hacer?
Sam Dillard
3

Para alguien que aterriza en esta pregunta. El uso de pandas con ' chunksize ' y ' usecols ' me ayudó a leer un archivo zip enorme más rápido que las otras opciones propuestas.

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
fuente
1

aquí hay otra solución 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

aquí datareaderhay una función de generador.

Rishabh Agrahari
fuente
Entonces, esto funciona tan eficientemente como la solución que usa el operador de rendimiento. : lo siento, no es así. La llamada a la función de devolución de llamada agrega más gastos generales, especialmente porque debe manejar el estado de forma explícita y por separado.
Martijn Pieters
@MartijnPieters Gracias. Actualizó la respuesta.
Rishabh Agrahari
0

Si está usando pandas y tiene mucha RAM (suficiente para leer todo el archivo en la memoria) intente usar pd.read_csvcon low_memory=False, por ejemplo:

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