Py-Spy профилирование Python кода: оптимизация Data Science на 05.02.2026 | AiManual
AiManual Logo Ai / Manual.
05 Фев 2026 Гайд

Py-Spy: практическое руководство по профилированию медленного Python-кода для Data Science

Пошаговое руководство по использованию Py-Spy для поиска узких мест в Python-коде для Data Science. Реальные примеры, сравнение с другими инструментами.

Когда твой Data Science скрипт работает как улитка

Знакомо чувство, когда запускаешь обработку данных, а прогресс-бар движется так медленно, что успеваешь выпить три чашки кофе? Ты проверяешь логи - все вроде работает. Память в норме. Но время выполнения зашкаливает.

Ты открываешь код. Там вложенные циклы, сложные преобразования, куча Pandas операций. Где именно тормозит? В каком конкретно месте? Обычный print() уже не помогает, а cProfile выдает гору данных, которую нужно часами анализировать.

Вот классическая ошибка: начинаешь оптимизировать не то место. Тратишь два дня на ускорение функции, которая занимает 2% времени выполнения. А настоящий убийца производительности скрывается в трех строчках кода, которые вызываются миллион раз.

Py-Spy: снайпер для медленного кода

Py-Spy - это не просто еще один профайлер. Это инструмент, который работает без модификации кода. Без декораторов. Без импортов. Он просто подключается к запущенному Python процессу и показывает, где именно процессор проводит время.

Сравни с другими инструментами:

Инструмент Требует изменения кода Накладные расходы Лучше всего для
cProfile Да Высокие Детальный анализ
line_profiler Да Средние Построчный анализ
memory_profiler Да Высокие Анализ памяти
Py-Spy Нет Минимальные Быстрое обнаружение узких мест

1 Установка и быстрый старт

На 05.02.2026 Py-Spy поддерживает все актуальные версии Python (включая Python 3.13). Устанавливается одной командой:

pip install py-spy
# Или через conda для Data Science окружений
conda install -c conda-forge py-spy
💡
На Linux и macOS может потребоваться установить дополнительные права для ptrace. На Windows работает через Windows Performance Toolkit.

Реальный пример: анализ геоданных, который тормозил 4 часа

Возьмем типичную Data Science задачу - расчет расстояний между точками. Изначальный код выглядел так:

import pandas as pd
import numpy as np
from math import radians, sin, cos, sqrt, atan2

def haversine_distance(lat1, lon1, lat2, lon2):
    """Вычисление расстояния по формуле Haversine"""
    R = 6371.0  # Радиус Земли в километрах
    
    lat1_rad = radians(lat1)
    lon1_rad = radians(lon1)
    lat2_rad = radians(lat2)
    lon2_rad = radians(lon2)
    
    dlat = lat2_rad - lat1_rad
    dlon = lon2_rad - lon1_rad
    
    a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
    c = 2 * atan2(sqrt(a), sqrt(1 - a))
    
    return R * c

def calculate_all_distances(df):
    """Плохая реализация: O(n²) сложность"""
    distances = []
    
    for i in range(len(df)):
        for j in range(len(df)):
            if i != j:
                dist = haversine_distance(
                    df.iloc[i]['lat'], df.iloc[i]['lon'],
                    df.iloc[j]['lat'], df.iloc[j]['lon']
                )
                distances.append((i, j, dist))
    
    return pd.DataFrame(distances, columns=['point_a', 'point_b', 'distance'])

# Генерация тестовых данных
np.random.seed(42)
df = pd.DataFrame({
    'lat': np.random.uniform(-90, 90, 100),
    'lon': np.random.uniform(-180, 180, 100)
})

# Этот вызов работал 4+ часа
result = calculate_all_distances(df)

Проблема очевидна для опытного глаза - O(n²) сложность. Но что если код сложнее? Что если там несколько вложенных функций и ты не видишь картину целиком?

2 Запускаем Py-Spy на медленном скрипте

Сохраняем код в файл slow_geo.py и запускаем:

# Запускаем скрипт в фоне
python slow_geo.py &

# Получаем PID процесса
ps aux | grep slow_geo.py

# Запускаем Py-Spy для записи профиля
py-spy record -o profile.svg --pid PID_NUMBER

# Или проще - сразу запускаем с профилированием
py-spy record -o profile.svg -- python slow_geo.py

Через 30 секунд (не нужно ждать 4 часа!) прерываем выполнение Ctrl+C. Получаем файл profile.svg - интерактивную flame graph.

Flame graph - это визуализация стека вызовов. Ширина прямоугольника показывает, сколько времени проводит процессор в этой функции. Самые широкие прямоугольники - главные кандидаты на оптимизацию.

Читаем flame graph как профессионал

Открываем profile.svg в браузере. Видим картину:

  • 90% времени уходит в функцию calculate_all_distances
  • Внутри нее 85% времени - вызовы haversine_distance
  • Каждый вызов haversine_distance тратит время на математические операции

Но самое главное - видим масштаб проблемы. Функция вызывается 9900 раз (100×100 минус диагональ). Это и есть O(n²).

3 Режим top: реальное время в реальном времени

Еще один мощный режим - top. Он показывает текущие выполняющиеся функции, как системный top, но для Python:

py-spy top --pid PID_NUMBER

Вывод в реальном времени:

%Own   %Total  OwnTime  TotalTime  Function
 85.2%  85.2%   102.4s    102.4s   haversine_distance
  8.7%  93.9%    10.5s    112.9s   calculate_all_distances
  3.1%  97.0%     3.7s    116.6s   <listcomp>
  1.2%  98.2%     1.4s    118.0s   radians

Цифры говорят сами за себя. 85% времени в одной функции.

Оптимизация: от 4 часов до 2 секунд

Теперь, когда точно знаем проблему, можно оптимизировать. Вот три уровня оптимизации:

Уровень 1: Векторизация с NumPy

def haversine_vectorized(lat1, lon1, lat2, lon2):
    """Векторизованная версия"""
    R = 6371.0
    
    lat1_rad = np.radians(lat1)
    lon1_rad = np.radians(lon1)
    lat2_rad = np.radians(lat2)
    lon2_rad = np.radians(lon2)
    
    dlat = lat2_rad - lat1_rad
    dlon = lon2_rad - lon1_rad
    
    a = np.sin(dlat/2)**2 + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(dlon/2)**2
    c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1-a))
    
    return R * c

def calculate_distances_vectorized(df):
    """Оптимизированная версия"""
    n = len(df)
    
    # Создаем матрицы всех пар
    lat1 = df['lat'].values[:, np.newaxis]
    lon1 = df['lon'].values[:, np.newaxis]
    lat2 = df['lat'].values[np.newaxis, :]
    lon2 = df['lon'].values[np.newaxis, :]
    
    # Векторизованный расчет
    distances = haversine_vectorized(lat1, lon1, lat2, lon2)
    
    # Убираем диагональ (расстояние до себя)
    np.fill_diagonal(distances, np.nan)
    
    return distances

Это уже дает ускорение в 100-200 раз. Но можно лучше.

Уровень 2: Используем специализированные библиотеки

# Установка: pip install geopy
from geopy.distance import geodesic
import numpy as np

def calculate_distances_geopy(df):
    """Используем оптимизированную библиотеку"""
    from geopy.distance import geodesic
    from concurrent.futures import ThreadPoolExecutor
    
    points = list(zip(df['lat'], df['lon']))
    n = len(points)
    distances = np.zeros((n, n))
    
    def calc_pair(i, j):
        if i != j:
            return geodesic(points[i], points[j]).km
        return 0
    
    # Параллельное выполнение
    with ThreadPoolExecutor(max_workers=8) as executor:
        futures = []
        for i in range(n):
            for j in range(n):
                futures.append((i, j, executor.submit(calc_pair, i, j)))
        
        for i, j, future in futures:
            distances[i, j] = future.result()
    
    return distances

Уровень 3: Переосмысливаем задачу

А нужно ли вообще считать все попарные расстояния? Часто в Data Science достаточно:

  • Расстояния до k ближайших соседей
  • Расстояния только для точек в определенном радиусе
  • Использовать приближенные алгоритмы для больших данных
from sklearn.neighbors import BallTree
import numpy as np

def calculate_knn_distances(df, k=5):
    """Только k ближайших соседей вместо всех попарных"""
    # Конвертируем в радианы для BallTree
    points = np.radians(df[['lat', 'lon']].values)
    
    # Используем гаверсинусовое расстояние
    tree = BallTree(points, metric='haversine')
    
    # Расстояния до k ближайших соседей
    distances, indices = tree.query(points, k=k+1)  # +1 чтобы исключить саму точку
    
    # Конвертируем радианы в километры
    distances = distances * 6371.0
    
    return distances[:, 1:], indices[:, 1:]  # Исключаем первую колонку (точка сама с собой)

Итог: было 4 часа, стало 2 секунды. Ускорение в 7200 раз.

💡
Py-Spy помог найти проблему за 30 секунд. Без него пришлось бы либо ждать 4 часа, либо гадать, где тормозит.

Продвинутые фичи Py-Spy для Data Science

Профилирование Jupyter Notebook

Да, Py-Spy работает и с Jupyter. Запускаем в отдельном терминале:

# Находим PID процесса Jupyter
pgrep -f "jupyter.*notebook"

# Профилируем
py-spy record -o notebook_profile.svg --pid PID --duration 30

Многопроцессорные приложения

Для многопроцессных Data Science пайплайнов:

# Профилируем все процессы
py-spy record -o multi.svg --pid PID --subprocesses

# Или только определенный subprocess
py-spy record -o worker.svg --pid WORKER_PID

Интеграция с Docker

Py-Spy отлично работает в контейнерах. Главное - запустить с правильными привилегиями:

# Внутри контейнера
docker exec -it --privileged CONTAINER_ID /bin/bash
pip install py-spy
py-spy record -o /tmp/profile.svg --pid 1 --duration 60

# Или с хоста
py-spy record -o profile.svg --pid $(docker inspect --format '{{.State.Pid}}' CONTAINER_NAME)

Чего не может Py-Spy (и что использовать вместо)

Py-Spy - не серебряная пуля. Вот его ограничения:

  1. Память - не отслеживает использование памяти. Для этого нужен memory_profiler или filprofiler
  2. GPU - не профилирует GPU операции. Для PyTorch/TensorFlow используй NVIDIA Nsight или PyTorch Profiler
  3. Детальный анализ строк - не показывает, какая именно строка в функции тормозит. Используй line_profiler после того, как Py-Spy найдет проблемную функцию
  4. I/O операции - плохо показывает блокирующие вызовы. Для асинхронного кода лучше подходит viztracer

Распространенные ошибки при профилировании

Видел эти ошибки сотни раз:

Ошибка 1: Профилирование в development окружении с другими данными. Профилируй всегда на production-like данных.

Ошибка 2: Слишком короткое время профилирования. Минимум 30 секунд, лучше 2-3 минуты.

Ошибка 3: Игнорирование теплого старта. Первый запуск всегда медленнее из-за компиляции, кэширования и т.д.

Ошибка 4: Оптимизация не того. Сначала измерь, потом оптимизируй. Всегда.

Py-Spy в пайплайне CI/CD

На 05.02.2026 лучшие практики включают автоматическое профилирование:

# .github/workflows/profile.yml
name: Performance Profiling

on:
  pull_request:
    branches: [ main ]

jobs:
  profile:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install dependencies
      run: |
        pip install py-spy pandas numpy
        pip install -r requirements.txt
    
    - name: Run performance tests
      run: |
        # Запускаем тест с профилированием
        timeout 60s py-spy record -o profile.svg -- python performance_test.py || true
        
        # Анализируем результаты
        if [ -f profile.svg ]; then
          # Проверяем, нет ли функций, которые занимают >50% времени
          # Можно добавить автоматический анализ flame graph
          echo "Performance profile generated"
        fi

Что дальше после Py-Spy?

Нашел узкое место с Py-Spy? Отлично. Теперь:

  1. Используй line_profiler для детального анализа конкретной функции
  2. Проверь память с memory_profiler - может быть, проблема в аллокациях
  3. Для числового кода посмотри в сторону Mojo, Nuitka или Cython
  4. Если проблема в GPU - переходи к NVIDIA Nsight Systems
  5. Рефактори код, используя принципы чистого ML кода

Помни: Py-Spy - это диагностический инструмент. Он не лечит, он ставит диагноз. А лечение - это уже твоя работа.

Самый важный урок: не гадай. Не предполагай. Не оптимизируй наугад. Запускай Py-Spy, смотри flame graph, находи реальную проблему. Часто это будет не та функция, которую ты подозреваешь.

В Data Science особенно: кажется, что тормозит сложный ML алгоритм, а на деле - банальный pandas merge без индекса. Или преобразование типов в цикле. Или сериализация данных.

Py-Spy покажет правду. Даже если она неудобная.