NumPy archivo de lectura con líneas de filtrado sobre la marcha

Tengo una gran variedad de números escritos en un archivo CSV y necesito cargar solo una porción de esa matriz. Conceptualmente, quiero llamar a np.genfromtxt() y luego cortar en fila la matriz resultante, pero

  1. El archivo es tan grande que puede que no encaje en la memoria RAM.
  2. el número de filas relevantes puede ser pequeño, por lo que no es necesario analizar cada línea.

MATLAB tiene la función textscan() que puede tomar un descriptor de archivo y leer solo una parte del archivo. ¿Hay algo así en NumPy?

Por ahora, definí la siguiente función que lee solo las líneas que satisfacen la condición dada:

 def genfromtxt_cond(fname, cond=(lambda str: True)): res = [] with open(fname) as file: for line in file: if cond(line): res.append([float(s) for s in line.split()]) return np.array(res, dtype=np.float64) 

Hay varios problemas con esta solución:

  • no general: admite solo el tipo flotante, mientras que genfromtxt detecta los tipos, que pueden variar de una columna a otra; también valores perdidos, convertidores, saltos, etc .;
  • no eficiente: cuando la condición es difícil, cada línea puede analizarse dos veces, también la estructura de datos utilizada y la búfer de lectura pueden ser subóptimas;
  • requiere escribir código.

¿Existe una función estándar que implemente el filtrado, o alguna contraparte de los textscan de MATLAB?

Puedo pensar en dos enfoques que brindan algunas de las funciones que solicita:

  1. Para leer un archivo en trozos / o en pasos de n-líneas / etc .:
    Puede pasar un generator a numpy.genfromtxt así como a numpy.loadtxt . De esta manera, puede cargar un gran conjunto de datos desde un archivo de texto de manera eficiente en la memoria, a la vez que conserva todas las funciones de análisis convenientes de las dos funciones.

  2. Para leer datos solo de líneas que coincidan con un criterio que se puede express como una expresión regular:
    Puede usar numpy.fromregex y usar una regular expression para definir con precisión qué tokens de una línea dada en el archivo de entrada deben cargarse. Las líneas que no coincidan con el patrón serán ignoradas.

Para ilustrar los dos enfoques, voy a usar un ejemplo de mi contexto de investigación.
A menudo necesito cargar archivos con la siguiente estructura:

 6 generated by VMD CM 5.420501 3.880814 6.988216 HM1 5.645992 2.839786 7.044024 HM2 5.707437 4.336298 7.926170 HM3 4.279596 4.059821 7.029471 OD1 3.587806 6.069084 8.018103 OD2 4.504519 4.977242 9.709150 6 generated by VMD CM 5.421396 3.878586 6.989128 HM1 5.639769 2.841884 7.045364 HM2 5.707584 4.343513 7.928119 HM3 4.277448 4.057222 7.022429 OD1 3.588119 6.069086 8.017814 

Estos archivos pueden ser enormes (GB) y solo me interesan los datos numéricos. Todos los bloques de datos tienen el mismo tamaño ( 6 en este ejemplo) y siempre están separados por dos líneas. Así que el stride de los bloques es 8 .

Usando el primer enfoque:

Primero voy a definir un generador que filtra las líneas no deseadas:

 def filter_lines(f, stride): for i, line in enumerate(f): if i%stride and (i-1)%stride: yield line 

Luego abro el archivo, creo un generador de filter_lines (aquí necesito saber la stride ) y paso ese generador a genfromtxt :

 with open(fname) as f: data = np.genfromtxt(filter_lines(f, 8), dtype='f', usecols=(1, 2, 3)) 

Esto funciona como una brisa. Tenga en cuenta que puedo usar los usecols de usecols para eliminar la primera columna de los datos. De la misma manera, puede usar todas las demás funciones de genfromtxt : detección de tipos, tipos variables de columna a columna, valores perdidos, convertidores, etc.

En este ejemplo, data.shape era (204000, 3) mientras que el archivo original consistía en 272000 líneas.

En este caso, el generator se utiliza para filtrar líneas de paso homogéneas, pero también se puede imaginar que filtra bloques de líneas no homogéneos según criterios (simples).

Usando el segundo enfoque:

Aquí está la regexp que voy a usar:

 regexp = r'\s+\w+' + r'\s+([-.0-9]+)' * 3 + r'\s*\n' 

Grupos, es decir, dentro de () , definen los tokens que se extraerán de una línea dada. A continuación, fromregex hace el trabajo e ignora las líneas que no coinciden con el patrón:

 data = np.fromregex(fname, regexp, dtype='f') 

El resultado es exactamente el mismo que en el primer enfoque.

Si pasa una lista de tipos (la condición de formato), use un bloque try y use el rendimiento para usar genfromtxt como generador, deberíamos poder replicar textscan() .

 def genfromtext(fname, formatTypes): with open(fname, 'r') as file: for line in file: try: line = line.split(',') # Do you care about line anymore? r = [] for type, cell in zip(formatTypes, line): r.append(type(cell)) except: pass # Fail silently on this line since we hit an error yield r 

Edición: olvidé el bloque excepto. Funciona bien ahora y puedes usar genfromtext como un generador así (usando un registro CSV aleatorio que tengo a mi alrededor):

 >>> a = genfromtext('log.txt', [str, str, str, int]) >>> a.next() ['10.10.9.45', ' 2013/01/17 16:29:26', '00:00:36', 0] >>> a.next() ['10.10.9.45', ' 2013/01/17 16:22:20', '00:08:14', 0] >>> a.next() ['10.10.9.45', ' 2013/01/17 16:31:05', '00:00:11', 3] 

Probablemente debería tener en cuenta que estoy usando zip para comprimir la línea dividida por comas y la especificación de formato que multiplicará las dos listas (deteniéndose cuando una de las listas se quede sin elementos) para que podamos recorrerlas juntas, evitando un bucle dependiente en len(line) o algo así.

Tratando de demostrar comentario a OP.

 def fread(name, cond): with open(name) as file: for line in file: if cond(line): yield line.split() def a_genfromtxt_cond(fname, cond=(lambda str: True)): """Seems to work without need to convert to float.""" return np.array(list(fread(fname, cond)), dtype=np.float64) def b_genfromtxt_cond(fname, cond=(lambda str: True)): r = [[int(float(i)) for i in l] for l in fread(fname, cond)] return np.array(r, dtype=np.integer) a = a_genfromtxt_cond("tar.data") print a aa = b_genfromtxt_cond("tar.data") print aa 

Salida

 [[ 1. 2.3 4.5] [ 4.7 9.2 6.7] [ 4.7 1.8 4.3]] [[1 2 4] [4 9 6] [4 1 4]]