Условие
Скрипт обрабатывает 50 млн строк за 25 минут. Менеджер просит ускорить. Как методично найти узкое место?
Решение
План
- Сначала измерить, не угадывать.
- Локализовать самую медленную строку.
- Понять, чем именно — CPU или памятью.
- Оптимизировать только её.
Инструменты
# 1. Сколько занимает каждый шаг — %time / %timeit в Jupyter
%time result = step1(df)
# 2. Построчно — line_profiler
%load_ext line_profiler
%lprun -f my_func my_func(df)
# 3. Память — memory_profiler / df.memory_usage(deep=True)
df.memory_usage(deep=True).sum() / 1e9, 'GB'
# 4. Cprofile + snakeviz для общей картины
import cProfile, pstats
cProfile.run('main()', 'out.prof')
pstats.Stats('out.prof').sort_stats('cumulative').print_stats(20)Типичные находки
apply(axis=1)в горячем цикле →np.where/векторизация.mergeбез индекса/sort → выставитьindex, использоватьvalidate=.- Чтение CSV без
dtype→ читать сdtype=/usecols=. - Большие object-колонки → перевод в
category(df['c'] = df['c'].astype('category')). - Цикл по
df.iterrows()→ переписать в groupby.
Оптимизация типа
def reduce_mem(df):
for c in df.select_dtypes('integer'):
df[c] = pd.to_numeric(df[c], downcast='integer')
for c in df.select_dtypes('float'):
df[c] = pd.to_numeric(df[c], downcast='float')
for c in df.select_dtypes('object'):
if df[c].nunique() / len(df) < 0.5:
df[c] = df[c].astype('category')
return dfПодводные камни
- Без измерения 80% «оптимизаций» бесполезны. Сначала
%lprun, потом код. df.memory_usage()безdeep=Trueврёт по object-колонкам (показывает только указатели).categoryускоряет groupby и экономит память, но ломается при наивном merge с другим dtype.- Конвейерные операции (
a.b.c.d) создают копии — иногда быстрее одна большая операция илиinplace=True. - Skewed groupby (один ключ — 90% данных) даже Dask делает медленным; решение — pre-split.
Эталонный ответ
Сначала измерить %lprun и memory_usage(deep=True), найти ту самую медленную строку, и переписать конкретно её — обычно apply→np.where, downcast типов, перевод в category.