Numpy чтение файла с фильтрацией строк на лету

у меня есть большой массив чисел, написанных в CSV-файле, и мне нужно загрузить только фрагмент этого массива. Концептуально я хочу позвонить np.genfromtxt() а затем строка-срез результирующего массива, но

  1. файл настолько велик, что может не поместиться в памяти
  2. количество соответствующих строк может быть небольшим, поэтому нет необходимости анализировать каждую строку.

MATLAB имеет функцию textscan() это может взять дескриптор файла и прочитать только часть файла. Есть есть что-нибудь подобное в NumPy?

на данный момент я определил следующую функцию, которая читает только строки, удовлетворяющие данному условию:

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)

есть несколько проблем с этим решением:

  • не общие: поддерживает только тип поплавка, в то время как genfromtxt обнаруживает типы,которые могут варьироваться от столбца к столбцу; также отсутствуют значения, преобразователи, пропуск и т. д.;
  • не эффективный: когда условие трудно, каждая строка может быть в два раза разбирается, и используемые структуры данных и чтение bufferization может быть неоптимальной;
  • требует написания кода.

есть ли стандартная функция, реализующая фильтрацию, или какой-то аналог MATLAB's textscan?

3 ответов


я могу придумать два подхода, которые обеспечивают некоторые из функций, которые вы просите:

  1. для чтения файла либо в кусках / или шагами n-строк / etc.:
    Вы можете пройти generator to и NumPy.genfromtxt а так же и NumPy.loadtxt. Таким образом, вы можете загрузить большой набор данных из памяти текстового файла-эффективно, сохраняя все удобные функции разбора двух функции.

  2. для чтения данных только из строк, соответствующих критерию, который может быть выражен как регулярное выражение:
    Вы можете использовать и NumPy.fromregex и использовать regular expression to точно определите, какие токены из данной строки во входном файле должны быть загружены. Строки, не соответствующие шаблону будут игнорироваться.

чтобы проиллюстрировать два подхода, я собираюсь использовать пример из моего исследования контекст.
Мне часто нужно загружать файлы со следующей структурой:

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

эти файлы могут быть огромными (GBs), и меня интересуют только числовые данные. Все блоки данных имеют одинаковый размер -- 6 в этом примере-и они всегда отделены двумя линиями. Так что stride блоков 8.

используя первый подход:

сначала я собираюсь определить генератор, который отфильтровывает нежелательные строки:

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

затем я открываю файл, создать filter_lines-генератор (здесь мне нужно знать stride), и передайте этот генератор genfromtxt:

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

это работает, как ветер. Обратите внимание, что я могу использовать usecols чтобы избавиться от первого столбца данных. Таким же образом, вы можете использовать все другие возможности genfromtxt -- обнаруживать типы, меняя типы от столбца к столбцу, отсутствующие значения, конвертеры, etc.

в этом примере data.shape был (204000, 3) в то время как исходный файл состоял из 272000 строки.

на generator используется для фильтрации однородно полосатых линий, но можно также представить, что он фильтрует неоднородные блоки линий на основе (простых) критериев.

используя второй подход:

здесь regexp Я собираюсь использовать:

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

группы -- т. е. внутри () -- определите токены, которые будут извлечены из данной строки. Следующий, fromregex выполняет задание и игнорирует строки, не соответствующие шаблону:

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

результат точно такой же, как в первом варианте.


если вы передадите список типов (условие формата), используйте блок try и используйте yield для использования genfromtxt в качестве генератора, мы сможем реплицировать 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

Edit:я забыл блок except. Теперь он работает нормально, и вы можете использовать genfromtext в качестве генератора (используя случайный журнал CSV, который у меня есть):

>>> 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]

Я, вероятно, должен отметить, что я использую zip для того чтобы застегнуть совместно линию разделения запятой и formatSpec которая tuplify два списка (остановка, когда в одном из списков заканчиваются элементы), поэтому мы можем перебирать их вместе, избегая цикла, зависящего от len(line) или что-то подобное.


попытка продемонстрировать комментарий к 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

выход

[[ 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]]