Хищник-жертва (Лотка-Вольтерра)

До сих пор популяция жила сама по себе. Но в природе всё связано: зайцы кормят рысей, рыси едят зайцев. Стало много зайцев — расплодились рыси — съели зайцев — рыси остались без еды и вымерли — зайцы снова расплодились. Эти качели и описывает классическая модель хищник-жертва.

Модель Лотки-Вольтерры — модель двух связанных популяций: жертвы размножаются и поедаются хищниками, а хищники растут за счёт съеденных жертв и вымирают без них. В результате обе численности колеблются циклически, причём пик хищников отстаёт от пика жертв.

Зачем эта модель? Она объясняет загадку, которую веками наблюдали натуралисты: численность зверей в лесу не постоянна и не растёт монотонно, а колеблется волнами. Классический пример — записи пушной компании Гудзонова залива: шкурки зайца и рыси то густо, то пусто, и пики рыси аккуратно отстают от пиков зайца на пару лет. Две простые «связи» порождают эти вечные качели.

Четыре стрелки взаимодействия

За шаг времени $dt$ жертвы (prey) и хищники (pred) меняются на четыре величины, по числу параметров. В непрерывном виде система выглядит так:

$$\begin{cases} \frac{d(\text{prey})}{dt} = a \cdot \text{prey} - b \cdot \text{prey} \cdot \text{pred} \\ \frac{d(\text{pred})}{dt} = c \cdot \text{prey} \cdot \text{pred} - d \cdot \text{pred} \end{cases}$$

  • $a$ — рождаемость жертв. Член $a \cdot \text{prey}$: будь жертвы одни, они росли бы экспоненциально (как в уроке 5.1).
  • $b$ — успешность охоты. Член $-b \cdot \text{prey} \cdot \text{pred}$: чем больше встреч «жертва $\times$ хищник», тем больше съедено. Это минус для жертв.
  • $c$ — эффективность размножения хищников. Член $+c \cdot \text{prey} \cdot \text{pred}$: те же встречи дают хищникам энергию для потомства. Это плюс для хищников.
  • $d$ — смертность хищников. Член $-d \cdot \text{pred}$: без еды хищники вымирают сами по себе.

Обратите внимание: член взаимодействия $\text{prey} \cdot \text{pred}$ один и тот же в обоих уравнениях, только у жертв он со знаком минус (их едят), а у хищников — с плюсом (они сыты). Это и есть «связь» двух популяций.

Запустим дискретную имитацию методом явного Эйлера: на каждом крошечном шаге $dt$ считаем изменения и прибавляем их.

def lotka_volterra(prey, pred, a, b, c, d, dt, steps):
    out = []
    for s in range(steps):
        dprey = (a * prey - b * prey * pred) * dt
        dpred = (c * prey * pred - d * pred) * dt
        prey += dprey
        pred += dpred
        out.append((s, prey, pred))
    return out

res = lotka_volterra(40, 9, 0.1, 0.01, 0.005, 0.1, 0.05, 400)
print(f"{'шаг':>4} {'жертвы':>8} {'хищники':>8}")
for s, pr, pd in res[::50]:
    print(f"{s:>4} {pr:>8.2f} {pd:>8.2f}")

Вывод:

 шаг   жертвы  хищники
   0    40.02     9.04
  50    39.76    11.62
 100    36.81    14.65
 150    31.59    17.53
 200    25.47    19.51
 250    19.85    20.17
 300    15.46    19.57
 350    12.37    18.12

Запаздывание фаз — главный сюжет

Прочитаем числа как историю. В начале жертв много (40), хищников мало (9). Хищникам есть что есть — они начинают размножаться: к шагу 100 их уже почти 15, к шагу 250 — больше 20. Но пока хищники набирали силу, они выедали жертв: жертвы сползают с 40 до 20 и ниже.

И вот ключевой момент: хищники продолжают расти даже тогда, когда жертв уже стало мало — по инерции, их ведь много и они ещё доедают остатки (пик хищников около шага 250–270). А затем, когда еды совсем не осталось, хищники начинают вымирать (к шагу 350 их уже 18 и падает). Это и есть запаздывание: пик хищников приходит позже пика жертв. Хищник всегда «реагирует с опозданием» на численность добычи.

жертвы:   /‾‾\__        пик раньше
                \__
хищники:    __/‾‾\      пик позже (с запаздыванием)
         __/
         ----------------> время

Если продолжить имитацию дальше, жертвы (оставшись почти без хищников) снова расплодятся, хищники с запозданием снова поднимутся — и цикл повторится. Получаются незатухающие колебания: классические циклы Лотки-Вольтерры.

Как работает под капотом

Метод явного Эйлера — самый простой способ имитировать непрерывную систему. Идея: «производная — это скорость изменения, так умножим скорость на маленький шаг времени $dt$ и прибавим». Мы дробим время на 400 крошечных кусочков по $dt = 0.05$ и на каждом делаем линейный шажок.

Почему $dt$ должен быть маленьким? Потому что между шагами мы считаем скорость постоянной, хотя на самом деле она всё время меняется. Чем мельче шаг, тем меньше ошибка. Если взять $dt$ слишком большим, явный Эйлер начинает врать — траектория искажается, и численная «популяция» уходит от верной динамики, хотя настоящая система устойчиво колеблется. Сравним аккуратный и грубый шаги на одинаковом промежутке времени:

def lv_last(dt, steps):
    prey, pred = 40.0, 9.0
    a, b, c, d = 0.1, 0.01, 0.005, 0.1
    for _ in range(steps):
        dprey = (a * prey - b * prey * pred) * dt
        dpred = (c * prey * pred - d * pred) * dt
        prey += dprey
        pred += dpred
    return prey, pred

p1 = lv_last(0.05, 400)   # мелкий шаг, время = 20
p2 = lv_last(2.0, 10)     # грубый шаг, то же время = 20
print(f"dt=0.05:  жертвы={p1[0]:.2f}  хищники={p1[1]:.2f}")
print(f"dt=2.00:  жертвы={p2[0]:.2f}  хищники={p2[1]:.2f}")

Вывод:

dt=0.05:  жертвы=10.36  хищники=16.29
dt=2.00:  жертвы=7.25  хищники=18.12

Оба прогона покрывают одинаковый промежуток «времени» (20 единиц), но дают заметно разные числа: грубый шаг искажает динамику. Вывод практический — в явном Эйлере $dt$ надо брать заведомо маленьким и при сомнении уменьшать его вдвое, проверяя, что результат почти не меняется.

Важно помнить: это имитация непрерывной системы. Настоящая модель Лотки-Вольтерры записывается дифференциальными уравнениями, и её подробный вывод, анализ устойчивости и фазовый портрет разбираются в курсе про дифференциальные уравнения. Здесь мы лишь «прокручиваем» её по шагам, чтобы своими глазами увидеть колебания и запаздывание.

Частые ошибки

  • Брать слишком большой $dt$. Явный Эйлер тогда искажает динамику и даёт нефизичный результат. $dt$ должен быть маленьким; проверяйте сходимость, уменьшая его вдвое.
  • Думать, что пики совпадают. Нет: пик хищников всегда отстаёт от пика жертв — в этом запаздывании вся суть модели.
  • Путать знаки членов взаимодействия. Один и тот же $\text{prey} \cdot \text{pred}$ у жертв идёт с минусом (их едят), а у хищников — с плюсом (они сыты).
  • Ждать, что система придёт к равновесию. В классической модели колебания не затухают — это вечные циклы, а не выход на постоянный уровень.
  • Забывать член $-d \cdot \text{pred}$. Без него хищники не вымирали бы без еды, и модель потеряла бы смысл «качелей».

Итоги

  • Хищник-жертва — две связанные популяции: жертвы плодятся ($a$) и поедаются ($b$), хищники растут за счёт еды ($c$) и вымирают без неё ($d$).
  • Член взаимодействия $\text{prey} \cdot \text{pred}$ общий для обоих уравнений: минус для жертв, плюс для хищников.
  • Численности колеблются циклически, причём пик хищников запаздывает относительно пика жертв.
  • Мы имитируем систему явным Эйлером — дробим время на шаги $dt$; слишком большой $dt$ искажает динамику.
  • Это дискретная имитация непрерывной модели; её строгий вывод — в курсе про дифференциальные уравнения.
Проверьте себя
1. Что означает запаздывание фаз в модели хищник-жертва?
AЖертвы и хищники достигают пика численности одновременно
BПик численности хищников наступает позже пика численности жертв
CХищники вымирают раньше, чем появляются жертвы
DКолебания со временем затухают до нуля
2. Что описывает член -b * prey * pred в уравнении для жертв?
AРождаемость жертв
BГибель жертв от хищников: чем больше встреч жертва×хищник, тем больше съедено
CЕстественную смертность хищников
DРазмножение хищников
3. Почему в имитации методом явного Эйлера нельзя брать слишком большой dt?
AС большим dt программа работает медленнее
BС большим dt накапливается ошибка: явный Эйлер искажает динамику
CБольшой dt всегда обнуляет обе популяции
DРазмер dt вообще не влияет на результат
py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →