Эффективное чтение данных из CSV в фрейм данных с несколькими разделителями

У меня есть неуклюжий файл CSV, который имеет несколько разделителей: разделитель для нечисловой части это ',', для числовой части ';' , Я хочу построить фрейм данных только из числовой части настолько эффективно, насколько это возможно.

Я сделал 5 попыток: среди них, используя аргумент converters pd.read_csv, используя регулярное выражение с engine='python', используя str.replace. Все они более чем в 2 раза медленнее, чем чтение всего файла CSV без преобразований. Это слишком медленно для моего варианта использования.

Я понимаю, что сравнение не похоже на сравнение, но оно демонстрирует, что общая низкая производительность не связана с вводом/выводом. Есть ли более эффективный способ считывания данных в числовой кадр данных Pandas? Или эквивалентный массив NumPy?

Приведенная ниже строка может использоваться для целей тестирования.

# Python 3.7.0, Pandas 0.23.4

from io import StringIO
import pandas as pd
import csv

# strings in first 3 columns are of arbitrary length
x = '''ABCD,EFGH,IJKL,34.23;562.45;213.5432
MNOP,QRST,UVWX,56.23;63.45;625.234
'''*10**6

def csv_reader_1(x):
    df = pd.read_csv(x, usecols=[3], header=None, delimiter=',',
                     converters={3: lambda x: x.split(';')})
    return df.join(pd.DataFrame(df.pop(3).values.tolist(), dtype=float))

def csv_reader_2(x):
    df = pd.read_csv(x, header=None, delimiter=';',
                     converters={0: lambda x: x.rsplit(',')[-1]}, dtype=float)
    return df.astype(float)

def csv_reader_3(x):
    return pd.read_csv(x, usecols=[3, 4, 5], header=None, sep=',|;', engine='python')

def csv_reader_4(x):
    with x as fin:
        reader = csv.reader(fin, delimiter=',')
        L = [i[-1].split(';') for i in reader]
        return pd.DataFrame(L, dtype=float)

def csv_reader_5(x):
    with x as fin:
        return pd.read_csv(StringIO(fin.getvalue().replace(';',',')),
                           sep=',', header=None, usecols=[3, 4, 5])

Проверки:

res1 = csv_reader_1(StringIO(x))
res2 = csv_reader_2(StringIO(x))
res3 = csv_reader_3(StringIO(x))
res4 = csv_reader_4(StringIO(x))
res5 = csv_reader_5(StringIO(x))

print(res1.head(3))
#        0       1         2
# 0  34.23  562.45  213.5432
# 1  56.23   63.45  625.2340
# 2  34.23  562.45  213.5432

assert all(np.array_equal(res1.values, i.values) for i in (res2, res3, res4, res5))

Результаты сравнительного анализа:

%timeit csv_reader_1(StringIO(x))  # 5.31 s per loop
%timeit csv_reader_2(StringIO(x))  # 6.69 s per loop
%timeit csv_reader_3(StringIO(x))  # 18.6 s per loop
%timeit csv_reader_4(StringIO(x))  # 5.68 s per loop
%timeit csv_reader_5(StringIO(x))  # 7.01 s per loop
%timeit pd.read_csv(StringIO(x))   # 1.65 s per loop

Обновить

Я открыт для использования инструментов командной строки в качестве последнего средства. В той степени, я включил такой ответ. Я надеюсь, что есть решение на чистом Python или Pandas с сопоставимой эффективностью.

Ответы

Ответ 1

Используйте инструмент командной строки

На сегодняшний день наиболее эффективным решением, которое я нашел, является использование специального инструмента командной строки для замены ";" с "," а затем прочитать в панд. Решения Pandas или чистого Python не приближаются с точки зрения эффективности.

По сути, использование CPython или инструмента, написанного на C/C++, вероятно, превзойдет манипуляции на уровне Python.

Например, используя поиск и замену текста:

import os

os.chdir(r'C:\temp')                       # change directory location
os.system('fart.exe -c file.csv ";" ","')  # run FART with character to replace

df = pd.read_csv('file.csv', usecols=[3, 4, 5], header=None)  # read file into Pandas

Ответ 2

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

import io
import pandas as pd

# strings in first 3 columns are of arbitrary length
x = '''ABCD,EFGH,IJKL,34.23;562.45;213.5432
MNOP,QRST,UVWX,56.23;63.45;625.234
'''*10**6

def iterstream(iterable, buffer_size=io.DEFAULT_BUFFER_SIZE):
    """
    http://stackoverflow.com/a/20260030/190597 (Mechanical snail)
    Lets you use an iterable (e.g. a generator) that yields bytestrings as a
    read-only input stream.

    The stream implements Python 3 newer I/O API (available in Python 2 io
    module).

    For efficiency, the stream is buffered.
    """
    class IterStream(io.RawIOBase):
        def __init__(self):
            self.leftover = None
        def readable(self):
            return True
        def readinto(self, b):
            try:
                l = len(b)  # We're supposed to return at most this much
                chunk = self.leftover or next(iterable)
                output, self.leftover = chunk[:l], chunk[l:]
                b[:len(output)] = output
                return len(output)
            except StopIteration:
                return 0    # indicate EOF
    return io.BufferedReader(IterStream(), buffer_size=buffer_size)

def replacementgenerator(haystack, needle, replace):
    for s in haystack:
        if s == needle:
            yield str.encode(replace);
        else:
            yield str.encode(s);

csv = pd.read_csv(iterstream(replacementgenerator(x, ";", ",")), usecols=[3, 4, 5])

Обратите внимание, что мы преобразуем строку (или составляющие ее символы) в байты через str.encode, так как это требуется для использования Pandas.

Этот подход функционально идентичен ответу Даниэле, за исключением того факта, что мы заменяем значения "на лету", так как они запрашиваются вместо всех сразу.

Ответ 3

Очень, очень, очень быстрый результат, 3.51, просто сделайте csv_reader_4 ниже, он просто преобразует StringIO в str, а затем заменяет ; с , и читает фрейм данных с sep=',':

def csv_reader_4(x):
    with x as fin:
        reader = pd.read_csv(StringIO(fin.getvalue().replace(';',',')), sep=',',header=None)
    return reader

Тест:

%timeit csv_reader_4(StringIO(x)) # 3.51 s per loop

Ответ 4

В моей среде (Ubuntu 16.04, 4 ГБ ОЗУ, Python 3.5.2) самым быстрым методом был (прототип 1) csv_reader_5 (взят из ответа U9-Forward), который работал только на 25% медленнее, чем чтение всего файла CSV без преобразований, Я улучшил этот подход, реализовав фильтр/оболочку, которая заменяет char в методе read():

class SingleCharReplacingFilter:

    def __init__(self, reader, oldchar, newchar):
        def proxy(obj, attr):
            a = getattr(obj, attr)
            if attr in ('read'):
                def f(*args):
                    return a(*args).replace(oldchar, newchar)
                return f
            else:
                return a

        for a in dir(reader):
            if not a.startswith("_") or a == '__iter__':
                setattr(self, a, proxy(reader, a))

def csv_reader_6(x):
    with x as fin:
        return pd.read_csv(SingleCharReplacingFilter(fin, ";", ","),
                            sep=',', header=None, usecols=[3, 4, 5])

В результате производительность немного выше по сравнению с чтением всего файла CSV без преобразований:

In [3]: %timeit pd.read_csv(StringIO(x))
605 ms ± 3.24 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [4]: %timeit csv_reader_5(StringIO(x))
733 ms ± 3.49 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [5]: %timeit csv_reader_6(StringIO(x))
568 ms ± 2.98 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

1 Я называю это прототипом, потому что он предполагает, что входной поток имеет тип StringIO (так как он вызывает .getvalue() для него).

Ответ 5

Если это вариант, подстановка символа ; с , в строке быстрее. Я записал строку x в файл test.dat.

def csv_reader_4(x):
    with open(x, 'r') as f:
        a = f.read()
    return pd.read_csv(StringIO(unicode(a.replace(';', ','))), usecols=[3, 4, 5])

Функция unicode() была необходима, чтобы избежать ошибки TypeError в Python 2.

Сравнительный анализ:

%timeit csv_reader_2('test.dat')  # 1.6 s per loop
%timeit csv_reader_4('test.dat')  # 1.2 s per loop

Ответ 6

Используя str.replace(), sample.csv данные в sample.csv и всегда col: 3 содержит ';' ограничитель

from pandas import read_csv
dataset = read_csv('sample.csv', header=None)
dataset[3] = dataset[3].str.replace(';', ',')
print(dataset)


      0     1     2                      3
0  ABCD  EFGH  IJKL  34.23,562.45,213.5432
1  MNOP  QRST  UVWX    56.23,63.45,625.234

Ответ 7

Почему бы не позволить csv_reader_4 использовать pd.concat:

def csv_reader_4(x):
    with x as fin:
        reader = pd.read_csv(fin, sep=',')
        reader = pd.concat([reader,pd.DataFrame(reader[3].str.split(';').tolist(),columns=[4,5])],axis=1)
    return reader

Ответ 8

В Python есть мощные функции для манипулирования данными, но не ожидайте, что производительность с помощью Python. Когда требуется производительность, C и C++ - ваш друг. Любая быстрая библиотека на python написана на C/C++. Использовать код C/C++ в python довольно просто, взгляните на утилиту swig (http://www.swig.org/tutorial.html). Вы можете написать класс C++, который может содержать несколько быстрых утилит, которые вы будете использовать в своем коде Python при необходимости.