Сначала давайте ответим на заголовок вопроса
1- Как эффективно прочитать 15M строк CSV-файла с плавающей запятой
Я предлагаю вам использовать модин :
Генерация данных образца:
import modin.pandas as mpd
import pandas as pd
import numpy as np
frame_data = np.random.randint(0, 10_000_000, size=(15_000_000, 2))
pd.DataFrame(frame_data*0.0001).to_csv('15mil.csv', header=False)
!wc 15mil*.csv ; du -h 15mil*.csv
15000000 15000000 480696661 15mil.csv
459M 15mil.csv
Теперь к тестам:
%%timeit -r 3 -n 1 -t
global df1
df1 = pd.read_csv('15mil.csv', header=None)
9.7 s ± 95.1 ms per loop (mean ± std. dev. of 3 runs, 1 loop each)
%%timeit -r 3 -n 1 -t
global df2
df2 = mpd.read_csv('15mil.csv', header=None)
3.07 s ± 685 ms per loop (mean ± std. dev. of 3 runs, 1 loop each)
(df2.values == df1.values).all()
True
Итак, как мы видим, модин был приблизительно в 3 раза быстрее в моей установке.
Теперь, чтобы ответить на вашу конкретную проблему
2 - Очистка CSV-файла, содержащего нечисловые символы, и затем чтение его
Как уже отмечалось, ваше узкое место, вероятно, является конвертером. Вы называете эти лямбды 30 миллионов раз. Даже затраты на вызов функции в этом масштабе становятся нетривиальными.
Давайте атаковать эту проблему.
Создание грязного набора данных:
!sed 's/.\{4\}/&)/g' 15mil.csv > 15mil_dirty.csv
Подходы
Сначала я попытался использовать модин с аргументом конвертеров. Затем я попробовал другой подход, который вызывает регулярное выражение меньше раз:
Сначала я создам объект в виде файла, который фильтрует все через ваше регулярное выражение:
class FilterFile():
def __init__(self, file):
self.file = file
def read(self, n):
return re.sub(r"[^\d.,\n]", "", self.file.read(n))
def write(self, *a): return self.file.write(*a) # needed to trick pandas
def __iter__(self, *a): return self.file.__iter__(*a) # needed
Затем мы передаем его пандам в качестве первого аргумента в read_csv:
with open('15mil_dirty.csv') as file:
df2 = pd.read_csv(FilterFile(file))
Тесты:
%%timeit -r 1 -n 1 -t
global df1
df1 = pd.read_csv('15mil_dirty.csv', header=None,
converters={0: lambda x: np.float32(re.sub(r"[^\d.]", "", x)),
1: lambda x: np.float32(re.sub(r"[^\d.]", "", x))}
)
2min 28s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
%%timeit -r 1 -n 1 -t
global df2
df2 = mpd.read_csv('15mil_dirty.csv', header=None,
converters={0: lambda x: np.float32(re.sub(r"[^\d.]", "", x)),
1: lambda x: np.float32(re.sub(r"[^\d.]", "", x))}
)
38.8 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
%%timeit -r 1 -n 1 -t
global df3
df3 = pd.read_csv(FilterFile(open('15mil_dirty.csv')), header=None,)
1min ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
Похоже, модин снова побеждает!
К сожалению, в modin еще не реализовано чтение из буферов, поэтому я разработал ULTIMATE APPROACH:
%%timeit -r 1 -n 1 -t
with open('15mil_dirty.csv') as f, open('/dev/shm/tmp_file', 'w') as tmp:
tmp.write(f.read().translate({ord(i):None for i in '()'}))
df4 = mpd.read_csv('/dev/shm/tmp_file', header=None)
5.68 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
Используется translate
, что значительно быстрее, чем re.sub
, а также /dev/shm
- файловая система в памяти, которую обычно предоставляет ubuntu (и другие linux). Любой записанный там файл никогда не попадет на диск, поэтому он быстрый.
Наконец, он использует modin для чтения файла, обойдя ограничение буфера modin.
Этот подход примерно в 30 раз быстрее , чем ваш, и также довольно прост.