Воспроизводимая фигура: скрипт вместо редактора

Фигура, собранная кодом, переживает рецензию, новые данные и собственную забывчивость автора.

Воспроизводимая фигура — это график, который полностью порождается скриптом из исходных данных, так что любой может пересобрать его той же командой и получить идентичный результат.

Почему не редактор

Соблазнительно «доделать» график в редакторе: подвинуть подпись, стереть выброс, подкрасить линию. Проблема в том, что эти правки нигде не записаны. Через полгода рецензент попросит изменить ось или добавить точку — и вы не сможете повторить ручные движения. А если данные обновились, всю ручную работу придётся делать заново, рискуя внести новую ошибку. Ручная правка фигуры — это технический долг, который всегда возвращается.

Скрипт же — это рецепт. Изменились данные — перезапустил, получил обновлённую фигуру за секунду. Рецензент спросил «а что если по-другому?» — поменял одну строку. Кто-то усомнился — прислал ему скрипт, он воспроизвёл сам. Это и есть научный стандарт.

Анатомия скрипта фигуры

Хороший скрипт читается сверху вниз как последовательность шагов: загрузка данных → расчёт → построение → оформление → сохранение в файл. Вот скелет на matplotlib (он рисует, поэтому не запускается в браузере — это образец для копирования):

import matplotlib.pyplot as plt
import numpy as np

# 1. данные (из файла, а не из головы)
data = np.loadtxt("experiment.csv", delimiter=",")
x, y = data[:, 0], data[:, 1]

# 2. расчёт (тренд)
coef = np.polyfit(x, y, 1)
trend = np.poly1d(coef)

# 3. построение
fig, ax = plt.subplots(figsize=(4, 3))
ax.scatter(x, y, s=15, alpha=0.7, label="измерения")
ax.plot(x, trend(x), "r-", label=f"тренд: {coef[0]:.2f}x+{coef[1]:.2f}")

# 4. оформление
ax.set_xlabel("время, с")
ax.set_ylabel("сигнал, мВ")
ax.legend()

# 5. сохранение в вектор
fig.savefig("figure1.pdf", bbox_inches="tight", dpi=300)

Обратите внимание: подпись тренда вычисляется из данных (coef[0]), а не вписывается руками. Если данные изменятся, подпись обновится сама — рассинхрона быть не может.

Закрепляем случайность

Если в фигуре есть случайность (джиттер, бутстрэп, симуляция), её надо зафиксировать seed'ом, иначе «воспроизводимый» скрипт будет давать разную картинку. Покажем, что разные seed дают разный результат, а один и тот же seed — всегда одинаковый.

import random

def sample_mean(seed):
    random.seed(seed)
    return round(sum(random.random() for _ in range(5)) / 5, 4)

print("разные seed :", sample_mean(1), sample_mean(2))
print("один seed=42:", sample_mean(42), sample_mean(42))

Вывод:

разные seed : 0.4992 0.5762
один seed=42: 0.3798 0.3798

Один и тот же seed даёт идентичный результат в обоих вызовах — фигура станет полностью детерминированной, а разные seed закономерно различаются.

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

Воспроизводимость держится на трёх китах: данные хранятся отдельным файлом (а не вшиты в скрипт), весь процесс описан кодом, а версии библиотек зафиксированы (например, в requirements.txt). Тогда фигура — детерминированная функция от входа: одни и те же данные плюс один и тот же код всегда дают один и тот же PDF.

Воспроизводимость как часть науки

Кризис воспроизводимости — большая тема современной науки: значительная доля опубликованных результатов не удаётся повторить. Фигуры играют здесь особую роль, потому что именно через них читатель чаще всего знакомится с результатом. Если фигуру нельзя пересобрать из исходных данных, то невозможно и проверить, не закралась ли ошибка между данными и картинкой — а такие ошибки случаются сплошь и рядом: перепутанные столбцы, потерянный фильтр, случайно исключённая группа.

Поэтому ведущие журналы всё чаще требуют публиковать вместе со статьёй не только данные, но и код, порождающий фигуры. Хорошая привычка — держать рядом с каждой фигурой её скрипт под контролем версий (git), фиксировать версии библиотек и seed случайности. Тогда через год, когда придёт правка от рецензента или обнаружится новая порция данных, обновление фигуры займёт минуту, а не день, и вы будете уверены, что картинка соответствует именно этим данным.

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

  • Финальные правки в редакторе — потерянный, неповторимый труд.
  • Незафиксированный seed — «воспроизводимый» скрипт каждый раз рисует чуть другое.
  • Данные, вписанные прямо в код числами — невозможно обновить и легко ошибиться.
  • Подписи и числа в тексте фигуры, набранные вручную — рассинхронизируются с данными.

Итог

  • Фигуру собирает скрипт, а не рука в редакторе.
  • Данные — отдельный файл; код — рецепт; результат детерминирован.
  • Случайность фиксируется seed'ом.
  • Подписи и числа вычисляются из данных, а не вписываются.
Проверьте себя
1. Почему финальные правки фигуры в графическом редакторе — плохая практика в науке?
AРедакторы портят качество изображения
BПравки нигде не записаны, их нельзя повторить при обновлении данных или по просьбе рецензента
CРедакторы не поддерживают формат PDF
DЭто занимает слишком много места на диске
2. Зачем фиксировать seed генератора случайных чисел в скрипте фигуры?
AЧтобы график рисовался быстрее
BЧтобы случайные элементы (джиттер, бутстрэп) давали при каждом запуске одинаковую картинку
CЧтобы уменьшить размер файла
DЭто нужно только для цветных графиков