Прекратите гадать и начните диагностировать проблемы с производительностью с помощью Py-Spy.
Делиться

Одни из самых сложных проблем при отладке кода в области анализа данных — это не синтаксические ошибки или логические неточности. Скорее, они возникают из-за кода, который делает именно то, что должен, но делает это очень медленно.
Функциональный, но неэффективный код может стать серьезным препятствием в рабочем процессе анализа данных. В этой статье я кратко расскажу о py-spy, мощном инструменте, предназначенном для профилирования кода на Python. Он позволяет точно определить, где ваша программа тратит больше всего времени, чтобы выявить и исправить неэффективность.
Пример задачи
Давайте сформулируем простой исследовательский вопрос, для решения которого напишем код:
«Какой из аэропортов вылета имеет в среднем самую длительную продолжительность полета среди всех рейсов между штатами и территориями США?»
Ниже приведён простой скрипт на Python, который отвечает на этот исследовательский вопрос, используя данные, полученные из Бюро транспортной статистики (BTS). Набор данных состоит из информации о каждом рейсе внутри штатов и территорий США в период с января по июнь 2025 года, включая информацию об аэропортах отправления и назначения. Он содержит приблизительно 3,5 миллиона строк.
Программа вычисляет расстояние Хаверсина — кратчайшее расстояние между двумя точками на сфере — для каждого рейса. Затем она группирует результаты по аэропортам вылета, чтобы найти среднее расстояние, и сообщает о пяти лучших значениях.
import pandas as pd import math import time def haversine(lat_1, lon_1, lat_2, lon_2): «»»Вычисляет расстояние Хаверсина между двумя точками с координатами широты и долготы»»» lat_1_rad = math.radians(lat_1) lon_1_rad = math.radians(lon_1) lat_2_rad = math.radians(lat_2) lon_2_rad = math.radians(lon_2) delta_lat = lat_2_rad — lat_1_rad delta_lon = lon_2_rad — lon_1_rad R = 6371 # Радиус Земли в км return 2*R*math.asin(math.sqrt(math.sin(delta_lat/2)**2 + math.cos(lat_1_rad)*math.cos(lat_2_rad)*(math.sin(delta_lon/2))**2)) if __name__ == '__main__': # Загрузка данных о рейсах в датафрейм flight_data_file = «r»./data/2025_flight_data.csv» flights_df = pd.read_csv(flight_data_file) # Запуск таймера для определения времени анализа start = time.time() # Вычисление расстояния Хаверсина между аэропортами начала и окончания каждого рейса haversine_dists = [] for i, row in flights_df.iterrows(): haversine_dists.append(haversine(lat_1=row[«LATITUDE_ORIGIN»], lon_1=row[«LONGITUDE_ORIGIN»], lat_2=row[«LATITUDE_DEST»], lon_2=row[«LONGITUDE_DEST»])) flights_df[«Distance»] = haversine_dists # Получаем результат, группируя по аэропорту отправления, вычисляя среднее расстояние полета и выводя 5 лучших результатов = ( flights_df .groupby('DISPLAY_AIRPORT_NAME_ORIGIN').agg(avg_dist=('Distance', 'mean')) .sort_values('avg_dist', ascending=False) ) print(result.head(5)) # Заканчиваем таймер и выводим время анализа end = time.time() print(f»Заняло {end — start} с»)
При выполнении этого кода получается следующий результат:
avg_dist DISPLAY_AIRPORT_NAME_ORIGIN Международный аэропорт Паго-Паго 4202.493567 Международный аэропорт Гуам 3142.363005 Международный аэропорт Луиса Муньоса Марина 2386.141780 Международный аэропорт Теда Стивенса Анкоридж 2246.530036 Международный аэропорт Даниэля К. Иноуэ 2211.857407 Took 169.8935534954071 s
Эти результаты вполне логичны, поскольку указанные аэропорты расположены в Американском Самоа, Гуаме, Пуэрто-Рико, на Аляске и Гавайях. Все они находятся за пределами континентальной части Соединенных Штатов, где, как правило, ожидаются большие средние расстояния полетов.
Проблема здесь не в результатах — которые, безусловно, достоверны — а во времени выполнения: почти три минуты ! Хотя три минуты могут быть терпимы для разового запуска, на этапе разработки это становится губительным для производительности. Представьте это как часть более длинного конвейера обработки данных. Каждый раз, когда изменяется параметр, исправляется ошибка или повторно запускается ячейка, вы вынуждены простаивать, пока работает программа. Это трение нарушает ваш рабочий процесс и превращает быстрый анализ в дело, занимающее весь день.
Теперь давайте посмотрим, как py-spy может помочь нам точно определить, какие именно строки кода занимают так много времени.
Что такое Py-Spy?
Чтобы понять, что делает py-spy и какие преимущества он дает, полезно сравнить его со встроенным в Python профилировщиком cProfile.
- cProfile: Это трассировочный профилировщик , работающий аналогично секундомеру при каждом вызове функции. Измеряется и отображается время между каждым вызовом функции и возвратом результата. Несмотря на высокую точность, это значительно увеличивает накладные расходы, поскольку профилировщику приходится постоянно приостанавливать работу и записывать данные, что может существенно замедлить выполнение скрипта.
- py-spy: Это профилировщик выборки , работающий подобно высокоскоростной камере, которая одновременно наблюдает за всей программой. py-spy полностью находится вне работающего скрипта Python и делает высокочастотные снимки состояния программы. Он анализирует весь «стек вызовов», чтобы точно определить, какая строка кода выполняется и какая функция ее вызвала, вплоть до верхнего уровня.
Запуск Py-spy
Для запуска py-spy на скрипте Python необходимо установить библиотеку py-spy в среду выполнения Python.
pip install py-spy
После установки библиотеки py-spy, наш скрипт можно профилировать, выполнив следующую команду в терминале:
py-spy record -o profile.svg -r 100 — python main.py
Вот что на самом деле делает каждая часть этой команды:
- py-spy: Вызывает инструмент.
- record: Эта команда указывает py-spy использовать режим «record», который будет непрерывно отслеживать выполнение программы и сохранять данные.
- -o profile.svg: Эта опция задает имя и формат выходного файла, указывая на необходимость вывода результатов в виде SVG-файла с именем profile.svg.
- -r 100: Эта опция задает частоту дискретизации, устанавливая ее на 100 раз в секунду. Это означает, что py-spy будет проверять, что делает программа, 100 раз в секунду.
- —: Этот флаг отделяет команду py-spy от команды скрипта Python. Он указывает py-spy, что всё, что следует за этим флагом, — это команда для выполнения, а не аргументы для самого py-spy.
- python main.py: Это команда для запуска скрипта Python, который будет профилироваться с помощью py-spy, в данном случае — main.py.
Примечание : При работе в Linux для запуска py-spy часто требуются права суперпользователя (sudo) по соображениям безопасности.
После завершения выполнения этой команды появится выходной файл profile.svg, который позволит нам более детально изучить, какие участки кода занимают больше всего времени.
Вывод py-spy

Открыв файл profile.svg, вы увидите визуализацию, созданную py-spy, показывающую, сколько времени наша программа потратила в разных частях кода. Это называется графиком «сосульки» (или иногда графиком «пламя», если ось Y инвертирована) и интерпретируется следующим образом:
- Полосы : Каждая цветная полоса представляет собой конкретную функцию, которая была вызвана во время выполнения программы.
- Ось X (Популяция) : Горизонтальная ось представляет собой совокупность всех образцов, взятых в ходе профилирования. Они сгруппированы таким образом, что ширина определенного столбца отражает долю от общего числа образцов, в течение которых программа находилась в функции, представленной этим столбцом. Примечание: Это не временная шкала; порядок не показывает, когда была вызвана функция, а только общий объем затраченного времени.
- Ось Y (глубина стека) : Вертикальная ось представляет собой стек вызовов. Верхняя полоса с надписью «все» представляет собой всю программу, а полосы ниже — функции, вызываемые из «всех». Далее стек рекурсивно спускается вниз, при этом каждая полоса разбивается на функции, которые были вызваны во время ее выполнения. Самая нижняя полоса показывает функцию, которая фактически выполнялась на ЦП в момент взятия выборки.
Взаимодействие с графом
Хотя изображение выше статическое, сам файл .svg, сгенерированный py-spy, полностью интерактивен. Открыв его в веб-браузере, вы сможете:
- Поиск (Ctrl+F) : Выделите определенные функции, чтобы увидеть, где они находятся в стеке.
- Масштабирование : Щелкните по любой полосе, чтобы увеличить масштаб конкретной функции и ее дочерних элементов, что позволит вам выделить сложные части стека вызовов.
- При наведении курсора : при наведении курсора на любой индикатор отображается название конкретной функции, путь к файлу, номер строки и точный процент времени, затраченного на её выполнение.
Самое важное правило для чтения графика типа «сосулька» простое: чем шире полоса, тем чаще встречается функция . Если полоса функции занимает 50% ширины графика, это означает, что программа выполняла эту функцию в течение 50% от общего времени выполнения.
Диагноз
На графике, представленном выше, видно, что полоса, отображающая функцию Pandas iterrows(), заметно широка. При наведении курсора на эту полосу в файле profile.svg становится ясно, что истинная доля времени, затраченного на эту функцию, составляет 68,36% . Таким образом, более 2/3 времени выполнения было потрачено на функцию iterrows(). Интуитивно понятно, почему это узкое место является причиной проблемы, поскольку функция iterrows() создает объект Pandas Series для каждой строки в цикле, что приводит к огромным накладным расходам. Это указывает на четкую цель — попытаться оптимизировать время выполнения скрипта.
Оптимизация скрипта
Наиболее очевидный способ оптимизировать этот скрипт, основываясь на знаниях, полученных с помощью py-spy, — это отказаться от использования функции iterrows() для перебора каждой строки с целью вычисления расстояния Хаверсина. Вместо этого её следует заменить векторизованным вычислением с использованием NumPy, которое будет выполнять вычисление для каждой строки всего одним вызовом функции. Таким образом, необходимые изменения следующие:
- Перепишите функцию haversine(), чтобы она использовала векторизованные и эффективные операции NumPy на уровне C, позволяющие передавать целые массивы, а не только один набор координат за раз.
- Замените цикл iterrows() одним вызовом новой векторизованной функции haversine().
import pandas as pd import numpy as np import time def haversine(lat_1, lon_1, lat_2, lon_2): «»»Вычисляет расстояние Хаверсина между двумя точками с координатами широты и долготы»»» lat_1_rad = np.radians(lat_1) lon_1_rad = np.radians(lon_1) lat_2_rad = np.radians(lat_2) lon_2_rad = np.radians(lon_2) delta_lat = lat_2_rad — lat_1_rad delta_lon = lon_2_rad — lon_1_rad R = 6371 # Радиус Земли в км return 2*R*np.asin(np.sqrt(np.sin(delta_lat/2)**2 + np.cos(lat_1_rad)*np.cos(lat_2_rad)*(np.sin(delta_lon/2))**2)) if __name__ == '__main__': # Загрузка данных о рейсах в датафрейм flight_data_file = «r»./data/2025_flight_data.csv» flights_df = pd.read_csv(flight_data_file) # Запуск таймера для определения времени анализа start = time.time() # Вычисление расстояния Хаверсина между аэропортами начала и окончания каждого рейса flights_df[«Distance»] = haversine(lat_1=flights_df[«LATITUDE_ORIGIN»], lon_1=flights_df[«LONGITUDE_ORIGIN»], lat_2=flights_df[«LATITUDE_DEST»], lon_2=flights_df[«LONGITUDE_DEST»]) # Получаем результат, группируя по аэропорту отправления, вычисляя среднее расстояние полета и выводя 5 лучших результатов. result = ( flights_df .groupby('DISPLAY_AIRPORT_NAME_ORIGIN').agg(avg_dist=('Distance', 'mean')) .sort_values('avg_dist', ascending=False) ) print(result.head(5)) # Завершаем таймер и выводим время анализа. end = time.time() print(f»Заняло {end — start} с»)
При выполнении этого кода получается следующий результат:
avg_dist DISPLAY_AIRPORT_NAME_ORIGIN Международный аэропорт Паго-Паго 4202.493567 Международный аэропорт Гуам 3142.363005 Международный аэропорт Луиса Муньоса Марина 2386.141780 Международный аэропорт Анкориджа Теда Стивенса 2246.530036 Международный аэропорт Даниэля К. Иноуэ 2211.857407 Took 0.5649983882904053 s
Эти результаты идентичны результатам до оптимизации кода, но вместо почти трех минут обработки потребовалось чуть больше половины секунды!
Взгляд в будущее
Если вы читаете это из будущего (конец 2026 года или позже), проверьте, используете ли вы Python 3.15 или более новую версию. Ожидается, что в Python 3.15 появится встроенный профилировщик выборки в стандартной библиотеке, предлагающий функциональность, аналогичную py-spy, без необходимости внешней установки. Для тех, кто использует Python 3.14 или более старые версии, py-spy остается золотым стандартом.
Заключение
В этой статье рассматривается инструмент для решения распространенной проблемы в области анализа данных — скрипт, который работает как положено, но написан неэффективно и занимает много времени. Был приведен пример скрипта для определения того, какие аэропорты вылета в США имеют наибольшую среднюю дальность полета согласно расстоянию Хаверсина. Этот скрипт работал как ожидалось, но выполнялся почти три минуты.
С помощью профилировщика Python py-spy мы выяснили, что причиной неэффективности было использование функции iterrows(). Заменив iterrows() на более эффективный векторизованный расчет расстояния Хаверсина, мы оптимизировали время выполнения с трех минут до чуть более половины секунды.
Код из этой статьи, включая предварительную обработку исходных данных с BTS, можно найти в моем репозитории на GitHub.
Спасибо за прочтение!
Источники данных
- Данные о рейсах : Бюро транспортной статистики. (22 января 2026 г.). Отчеты о пунктуальности рейсов перевозчиков (1987 г. – настоящее время) [Набор данных]. Министерство транспорта США. Получено с https://www.transtats.bts.gov/DL_SelectFields.aspx?gnoyr_VQ=FGJ&QO_fu146_anzr=b0-gvzr
- Координаты аэропортов : Бюро транспортной статистики. (22 января 2026 г.). Авиация: Основные координаты [Набор данных]. Министерство транспорта США. Получено с https://transtats.bts.gov/DL_SelectFields.aspx?gnoyr_VQ=FLL&QO_fu146_anzr=N8vn6v10
Данные Бюро транспортной статистики (BTS) являются результатом работы федерального правительства США и находятся в общественном достоянии в соответствии со статьей 105 раздела 17 Свода законов США . Их можно свободно использовать, распространять и адаптировать без ограничений авторского права.
Источник: towardsdatascience.com























