Как мыслить в столбик, писать более быстрый код и, наконец, использовать Pandas как профессионал.
Делиться

Ладно, признаюсь честно: когда я только начинал использовать Pandas, я постоянно писал циклы вот такого типа:
for i in range(len(df)): if df.loc[i, "sales"] > 1000: df.loc[i, "tier"] = "high" else: df.loc[i, "tier"] = "low" Это сработало. И я подумал: «Ну, это же здорово, правда?»
Оказалось… не совсем так.
В тот момент я этого не понимал, но подобные циклы — классическая ловушка для начинающих. Они заставляют Pandas выполнять гораздо больше работы, чем необходимо, и незаметно внедряют в ментальную модель мышление, заставляющее думать построчно, а не по столбцам.
Как только я начал мыслить в столбчатом формате , всё изменилось. Код стал короче. Выполнение ускорилось. И внезапно я почувствовал, что Pandas создан именно для того, чтобы помогать мне, а не замедлять меня.
Чтобы это продемонстрировать, воспользуемся небольшим набором данных, на который будем ссылаться на протяжении всего текста:
import pandas as pd df = pd.DataFrame({ "product": ["A", "B", "C", "D", "E"], "sales": [500, 1200, 800, 2000, 300] })Выход:
product sales 0 A 500 1 B 1200 2 C 800 3 D 2000 4 E 300 Наша цель проста: пометить каждую строку как high , если объем продаж превышает 1000, и как low в противном случае.
Позвольте мне показать вам, как я это делал вначале, и почему есть лучший способ.
Метод циклов, с которого я начал
Вот цикл, который я использовал, когда учился:
for i in range(len(df)): if df.loc[i, "sales"] > 1000: df.loc[i, "tier"] = "high" else: df.loc[i, "tier"] = "low" print(df)В результате получается следующее:
product sales tier 0 A 500 low 1 B 1200 high 2 C 800 low 3 D 2000 high 4 E 300 low И да, это работает. Но вот что я узнал на собственном горьком опыте:
Pandas выполняет крошечную операцию для каждой строки , вместо того чтобы эффективно обрабатывать весь столбец сразу.
Такой подход не масштабируется — то, что работает нормально при 5 строках, замедляется при 50 000 строках.
Что еще более важно, это заставляет вас мыслить как новичок — строка за строкой — а не как профессиональный пользователь Pandas.
Замеры времени зацикливания (момент, когда я понял, что оно медленное)
Когда я впервые запустил свой цикл на этом крошечном наборе данных, я подумал: «Ничего страшного, он достаточно быстрый». Но потом я задумался… а что, если бы у меня был набор данных большего размера?
Поэтому я попробовал:
import pandas as pd import time # Make a bigger dataset df_big = pd.DataFrame({ "product": ["A", "B", "C", "D", "E"] * 100_000, "sales": [500, 1200, 800, 2000, 300] * 100_000 }) # Time the loop start = time.time() for i in range(len(df_big)): if df_big.loc[i, "sales"] > 1000: df_big.loc[i, "tier"] = "high" else: df_big.loc[i, "tier"] = "low" end = time.time() print("Loop time:", end - start)Вот что у меня получилось:
Loop time: 129.27328729629517Это 129 секунд .
Более двух минут уходит только на то, чтобы пометить строки как "high" или "low" .
В тот момент до меня дошло. Код был не просто «немного неэффективным». Он принципиально неправильно использовал Pandas.
А теперь представьте, что это работает внутри конвейера обработки данных, при обновлении панели мониторинга, обрабатывая миллионы строк каждый день.
Почему так медленно?
Цикл заставляет Pandas:
- Получите доступ к каждой строке по отдельности.
- Выполнять логику на уровне Python для каждой итерации
- Обновляйте DataFrame по одной ячейке за раз.
Иными словами, это превращает высокооптимизированный столбцовый механизм в усовершенствованный обработчик списков на Python.
А библиотека Pandas создана не для этого.
Решение в одну строчку (и момент, когда всё стало ясно)
После просмотра 129 секунд я понял, что должен быть способ получше.
Поэтому вместо перебора строк я попробовал выразить правило на уровне столбцов :
«Если объем продаж превышает 1000, указывайте высокую маркировку. В противном случае — низкую».
Вот и всё. Таково правило.
Вот векторизованная версия:
import numpy as np import time start = time.time() df_big["tier"] = np.where(df_big["sales"] > 1000, "high", "low") end = time.time() print("Vectorized time:", end - start)И каков результат?
Vectorized time: 0.08Дайте этому осмыслиться.
Зацикленная версия: 129 секунд
Векторизованная версия: 0,08 секунды
Это более чем в 1600 раз быстрее .
Что только что произошло?
Ключевое различие заключается в следующем:
Цикл обрабатывал DataFrame построчно . Векторизованная версия обрабатывала весь столбец sales за одну оптимизированную операцию .
Когда вы пишете:
df_big["sales"] > 1000В Python библиотека Pandas не проверяет значения по одному. Сравнение выполняется на более низком уровне (с помощью NumPy), в скомпилированном коде, по всему массиву.
Затем np.where() применяет метки за один эффективный проход.
Вот незаметное, но существенное изменение:
Вместо того чтобы спрашивать:
«Что мне делать с этим спором?»
Вы спрашиваете:
«Какое правило применяется к этой колонке?»
Вот где проходит грань между начинающими и профессиональными пандами.
В этот момент я подумал, что «перешёл на новый уровень». Но потом обнаружил, что могу сделать это ещё проще.
А потом я открыл для себя булево индексирование.
После замера времени векторизованной версии я почувствовал себя довольно гордым. Но затем меня осенило еще одно.
Для этого мне даже не нужен np.where() .
Вернемся к нашему небольшому набору данных:
df = pd.DataFrame({ "product": ["A", "B", "C", "D", "E"], "sales": [500, 1200, 800, 2000, 300] })Наша цель остается прежней:
Если объем продаж превышает 1000, пометьте каждую строку high , в противном случае — low .
С помощью np.where() мы написали:
df["tier"] = np.where(df["sales"] > 1000, "high", "low")Это чище и быстрее. Гораздо лучше, чем зацикливание.
Но вот что действительно изменило мое представление о Pandas:
Вот эта строчка…
df["sales"] > 1000…уже возвращает нечто невероятно полезное.
Давайте посмотрим:
Выход:
0 False 1 True 2 False 3 True 4 False Name: sales, dtype: boolЭто логический ряд.
Pandas оценила состояние всего столбца сразу.
Нет циклов. Нет условий if . Нет построчной логики.
Программа сгенерировала полную маску значений True/False за один раз.
Булево индексирование ощущается как сверхспособность.
А вот тут начинается самое интересное.
Вы можете использовать эту логическую маску напрямую для фильтрации строк:
df[df["sales"] > 1000]И Pandas мгновенно предоставляет вам:

Мы даже можем построить столбец tier , используя непосредственно булеву индексацию:
df["tier"] = "low" df.loc[df["sales"] > 1000, "tier"] = "high"По сути, я говорю:
- Предположим, что все показатели
"low". - Изменять значения следует только в тех строках, где объем продаж превышает 1000.
Вот и все.
И вдруг я перестаю думать:
«Для каждой строки проверьте значение…»
Я думаю:
«Начните с значения по умолчанию. Затем примените правило к подмножеству».
Этот сдвиг едва заметен, но он меняет всё.
Освоившись с булевыми масками, я начал задумываться:
Что произойдет, если логика не будет такой простой, как «больше 1000»? Что, если мне понадобятся пользовательские правила?
Именно там я открыл для себя apply() . И поначалу мне показалось, что это идеальное сочетание преимуществ обеих функций.
Разве apply() недостаточно хорош?
Честно говоря, после того, как я перестал писать циклы, я думал, что во всем разобрался. Потому что существовала одна волшебная функция, которая, казалось, решала все проблемы:
apply() .
Это казалось идеальным компромиссом между запутанными циклами и пугающей векторизацией.
Естественно, я начал писать что-то подобное:
df["tier"] = df["sales"].apply( lambda x: "high" if x > 1000 else "low" )А что на первый взгляд?
Выглядит отлично.
- Нет цикла
for - Ручная индексация отсутствует
- Легко читается
Создается впечатление, что это профессиональное решение.
Но вот чего я тогда не понимал:
apply() по-прежнему выполняет код Python для каждой отдельной строки.
Это просто скрывает цикл.
При использовании:
df["sales"].apply(lambda x: ...)
Pandas по-прежнему:
- Взяв каждое значение
- Передача его в функцию Python
- Возвращаем результат
- Повторяйте это для каждой строки.
Да, это чище, чем цикл for . Но с точки зрения производительности? Это гораздо ближе к циклу, чем к настоящей векторизации.
Это стало для меня своего рода откровением. Я понял, что заменяю видимые циклы невидимыми.
Итак, когда следует использовать apply() ?
- Если логику можно выразить с помощью векторизованных операций, то следует использовать именно их.
- Если это можно выразить с помощью булевых масок, сделайте это.
- Если для этого абсолютно необходима собственная логика на Python, используйте
apply().
Другими словами:
Сначала векторизуйте. Используйте apply()only в случае крайней необходимости.
Не потому, что apply() плохая функция. А потому что Pandas работает быстрее и чище, когда вы мыслите столбцами, а не построчными функциями.
Заключение
Оглядываясь назад, я понимаю, что моей самой большой ошибкой было не написание циклов. Я полагал, что если код работает, то он достаточно хорош.
Pandas не наказывает вас сразу за мышление в строках. Но по мере роста ваших наборов данных, масштабирования конвейеров и попадания вашего кода в панели мониторинга и рабочие процессы производственной среды, разница становится очевидной.
- Построчный подход не масштабируется.
- Скрытые циклы в Python не масштабируются.
- Правила на уровне столбцов это делают.
Вот где проходит настоящая грань между использованием Pandas на уровне новичка и профессионала.
Итак, вкратце:
Прекратите спрашивать, что делать с каждой строкой. Начните спрашивать, какое правило применяется ко всему столбцу.
После такого перехода ваш код станет быстрее, чище, его будет легче проверять и поддерживать. И вы начнете мгновенно выявлять неэффективные шаблоны, в том числе и свои собственные.
Ибрагим Салами. Все материалы от Ибрагима Салами.
Источник: towardsdatascience.com




















