Центральная предельная теорема

\[\newcommand{\st}{\, : \:} \newcommand{\ind}[1]{\mathbf{1}_{#1}} \newcommand{\dd}{\mathrm{d}}\]
Важное уведомлениеИсследуйте центральную предельную теорему

Это краткий обзор центральной предельной теоремы (ЦПТ), целью которого является придать конкретный смысл природе сходимости (по распределению, а не по вероятности).

Эта страница содержит (в конце) интерактивный контент, который вы можете свободно изучать и изменять. Для более сложных изменений рекомендуется запустить и отредактировать интерактивный Jupyter Notebook. Вы можете:

  • Запустить его на компьютере с установленным Python/Jupyter-lab (требуются ipywidgets, numpy, matplotlib).
  • Запустить его с помощью онлайн-сервиса. Официальный сайт Jupyter предоставляет бесплатный и открытый сервис. Если вам нужна большая вычислительная мощность, вы можете импортировать блокнот в Google Colab (требуется учётная запись Google).

Результаты о сходимости

Рассмотрим некоторые основные результаты о сходимости для сумм центрированных случайных величин с конечной дисперсией.

Классическая формулировка

Теорема 1 (Центральная предельная теорема) Пусть \((X_n)_{n\ge 1}\) — последовательность независимых одинаково распределённых (i.i.d.) вещественнозначных случайных величин с \(\mathbb E[|X_n|^2]<\infty\). Обозначим \(m:= \mathbb E[X_n]\), \(\sigma:= \sqrt{\mathbb{D}[X_n]}\) и \[ S_n:= \frac{1}{\sqrt{n}} \sum_{i=1}^n \frac{X_i -m}{\sigma} \tag{1}\] Тогда \(S_n\) сходится по распределению к стандартной нормальной случайной величине, скажем \(Z\sim \mathcal N(0,1)\). Другими словами, для каждой ограниченной измеримой функции \(f \colon \mathbb R\to \mathbb R\), непрерывной почти всюду, выполняется \[ \lim_{n\to \infty} \mathbb E[f(S_n)] = \frac{1}{\sqrt{2 \pi}} \int f(x) e^{-x^2/2} dx \]

В частности, из предыдущей теоремы можно вывести равномерную сходимость функции распределения \[ \lim_{n\to \infty} \sup_{a<b}\left| \mathbb P( a< S_n \le b) - \mathbb P(a<Z\le b)\right|=0 \]

Количественная версия

Предыдущая Теорема 1 не затрагивает скорость сходимости.

Теорема 2 (Количественная центральная предельная теорема) Пусть \((Y_n)_{n\ge 1}\) — последовательность независимых случайных величин с \(\mathbb E[Y_n]=0\) и \(\mathbb E[Y_n^2]=1\), \(\mathbb E[|Y_n|^3]<\infty\). Пусть \[ S_n:= \frac{1}{\sqrt{n}} \sum_{i=1}^n Y_i \] Для \(g\in C^3_b(\mathbb R)\) с \(C:= \sup_x |g'''(x)|\) выполняется \[ |\mathbb{E}[g(S_n)] - \mathbb{E}[g(Z)]| \le \frac{C}{6\sqrt{n}} \left( \frac{2^{3/2}}{\sqrt{\pi}}+\frac{1}{n}\sum_{k=1}^n \mathbb E[|Y_k|^3] \right) \] где \(Z\sim \mathcal N(0,1)\); а именно \[ \mathbb E[g(Z)] = \tfrac{1}{\sqrt{2 \pi}} \int g(x) e^{-x^2/2} dx \]

Лемма 1 Пусть \(V\), \(Y\) и \(Z\) — три случайные величины, такие что

  • \(V\) и \(Y\) независимы; \(V\) и \(Z\) независимы.
  • \(Y\) и \(Z\) имеют конечный третий момент.
  • \(\mathbb E[Y] = \mathbb E[Z]\) и \(\mathbb E[Y^2] =\mathbb E[Z^2]\).

Тогда для любой \(g \in C_b^3\), полагая \(C := \sup_{x \in \mathbb{R}} |g'''(x)|\), выполняется следующее неравенство: \[ |\mathbb E[g(V +Y )] - \mathbb E[g(V + Z)]| \le \frac{C}{6} \left( \mathbb E[|Y|^3] + \mathbb E[|Z|^3] \right) \]

Доказательство (Лемма 1). По формуле Тейлора, для трёх точек \(v,y,z \in \mathbb R\) выполняется \[ g(v+y)-g(v+z)= g'(v) (y-z) + \tfrac 12 g''(v)(y^2-z^2) + R(v,y)- R(v,z) \] где остаточные члены \(R(v,\cdot)\) ограничены как \(|R(v,x)|\le C |x|^3/6\). Теперь вычислим предыдущую формулу для каждого \(\omega\in \Omega\) в точках \(v=V(\omega)\), \(y=Y(\omega)\) и \(z=Z(\omega)\), и возьмём математическое ожидание. Тогда \(\mathbb E[g'(V) (Y-Z)]=\mathbb E[g'(V)] \mathbb E[(Y-Z)]=0\) (используя гипотезы о независимости и равенстве математических ожиданий). Аналогично для члена \(g''(V)(Y^2-Z^2)\). Таким образом

\[ \left| \mathbb E[g(V+Y)-g(V+Z)] \right| = \left| \mathbb E[R(V,Y) - R(V,Z)] \right| \le \frac{C}{6} \left( \mathbb E[|Y|^3] + \mathbb E[|Z|^3] \right) \]

Лемма 2 Пусть \(g \in C^3_b(\mathbb R)\), пусть \(Y_1,\ldots,Y_n\) — независимые случайные величины, и \(Z_1,\ldots,Z_n\) — другой набор независимых случайных величин. Предположим, что \(\mathbb E[Y_i]=\mathbb E[Z_i]\) и \(\mathbb E[Y_i^2]= \mathbb E[Z_i^2]<\infty\). Пусть \(C\) определено как в Лемма 1. Тогда \[ \left|\mathbb E\left[g\left(\tfrac{Y_1+\ldots+Y_n}{\sqrt{n}}\right) - g\left(\tfrac{Z_1+\ldots+Z_n}{\sqrt{n}}\right) \right] \right| \le \frac{C}{6 n^{3/2}}\sum_{k=1}^{n} \left( \mathbb E[|Y_k|^3] + \mathbb E[|Z_k|^3]\right) \] В частности, если \(Y_i\) являются i.i.d. и \(Z_i\) являются i.i.d. (в общем случае с другим распределением) \[ \left|\mathbb E\left[g\left(\tfrac{Y_1+\ldots+Y_n}{\sqrt{n}}\right) - g\left(\tfrac{Z_1+\ldots+Z_n}{\sqrt{n}}\right) \right] \right| \le \frac{C \left( \mathbb E[|Y_1|^3] + \mathbb E[|Z_1|^3] \right)}{6 \sqrt{n}} \]

Доказательство (Лемма 2). Без ограничения общности, можно предположить, что все \(Y_1,\ldots,Y_n,Z_1,\ldots,Z_n\) являются независимыми случайными величинами. Тогда запишем \(V_k=(Y_1+\ldots +Y_{k-1} + Z_{k+1}+\ldots +Z_{n})/\sqrt{n}\), чтобы получить \[ \begin{aligned} \left| \mathbb E\left[g\left(\tfrac{Y_1+\ldots+Y_n}{\sqrt{n}}\right) - g\left(\tfrac{Z_1+\ldots+Z_n}{\sqrt{n}}\right) \right] \right| & = \left| \sum_{k=1}^{n} \mathbb E\left[g\left(V_k + \tfrac{Y_{k}}{\sqrt{n}}\right) - g\left(V_k+\tfrac{Z_{k}}{\sqrt{n}}\right) \right] \right| \\ & \le \sum_{k=1}^{n} \frac{C}6 \left( \mathbb E[|Y_k/\sqrt{n}|^3] + \mathbb E[|Z_k/\sqrt{n}|^3]\right) \end{aligned} \] где в последнем неравенстве мы использовали Лемма 1 \(n\) раз.

Доказательство (Теорема 2). Если \(Z_i\) являются i.i.d. стандартными нормальными, то \((Z_1+\ldots + Z_n)/\sqrt{n}\) также является стандартной нормальной величиной, и, следовательно, её распределение не зависит от \(n\). Теорема 2, таким образом, является следствием Лемма 2 и тождества \(\mathbb E[|Z_i|^3]=2^{3/2}/\sqrt{\pi}\).

Упражнение 1 Пусть \(Z\sim \mathcal N(0,1)\) — стандартная нормальная случайная величина. Пусть \((Y_n)_{n\ge 1}\) — i.i.d. последовательность с \(\mathbb E[Y_i^k]= \mathbb E[Z^k]\) для \(k=1,\ldots, \ell\). Пусть \(g\in C^\ell_b(\mathbb R)\). Докажите, что существует константа \(C\) (зависящая от \(g\) и распределения \(Y_i\)), такая что \[ \left| \mathbb E[g(S_n)]-\mathbb E[g(Z)] \right| \le C n^{-(\ell-1)/2} \]

Мартингальная версия

Стоит упомянуть, что Центральная предельная теорема выходит далеко за рамки независимых случайных величин. В конечном счёте, для такого типа результата даже не требуется, чтобы величины были определены на линейном пространстве (например, совершение малых случайных шагов на многообразии, по мере уменьшения шагов, будет сходиться к распределению на пространстве непрерывных кривых на многообразии, называемому броуновским движением). Таким образом, существуют сильные локальные версии ЦПТ, версии для метрических пространств, эргодические версии и так далее. Интересный пример, требующий лишь элементарных гипотез, охватывает случай мартингалов.

Теорема 3 (Центральная предельная теорема для мартингалов) Пусть \((X_n)_{n\ge 1}\) — последовательность вещественнозначных случайных величин и пусть \[ M_n:= X_1+\ldots+X_n \] Предположим, что

  • \(\mathbb E[X_n| M_{n-1}]=0\).
  • Для \(Q_n:= \mathbb E[X_n^2| M_{n-1} ]\), выполняется \(\sum_{n=1}^\infty Q_n=\infty\) п.н. (почти наверное).
  • \(\mathbb E\left[\sup_n \mathbb E[|X_n|^3| M_{n-1} ] \right]<\infty\).

Пусть \(\tau_\ell:=\inf \{N\in \mathbb N \,:\: \sum_{n=1}^N Q_n \ge \ell\}\). Тогда \(M_{\tau_\ell}/\sqrt{\ell}\) сходится к стандартной нормальной величине по распределению при \(\ell \to \infty\).

Результаты о несходимости

Пока всё хорошо. Если \(Y_n\) — центрированные i.i.d. случайные величины с конечной дисперсией, то \(S_n\) сходится к нормальному пределу. По распределению. Будет ли эта сходимость иметь место по вероятности или даже п.н.?

Утверждение 1 Независимо от вероятностного пространства и распределения \(Y_n\), Центральная предельная теорема не выполняется по вероятности, даже для подпоследовательностей.

Суть в том, что если две последовательности \((S_n), (S_n')\) сходятся по вероятности, то \(S_n+S_n'\) также сходится по вероятности (по неравенству треугольника). То же утверждение неверно для сходимости по распределению, так как сходимость по распределению касается не самих случайных величин, а только их распределений. Таким образом, сходимость \(S_n\) или \(S_n'\) ничего не говорит об их совместном распределении.

Доказательство (Утверждение 1). Любая предельная точка (вдоль некоторой подпоследовательности) по вероятности \(S\) для \(S_n\) будет иметь стандартное нормальное распределение. В частности, \(\sqrt{2} S_{2n}-S_n\) сходилась бы к \((\sqrt{2}-1) S \sim \mathcal N(0,3-2\sqrt{2})\) по вероятности (вдоль той же подпоследовательности). Но \[ S_n':= \sqrt{2} S_{2n} - S_n = \frac{1}{\sqrt{n}}\sum_{i=n+1}^{2n} Y_i \] является суммой \(n\) i.i.d. величин, делённой на \(\sqrt{n}\), следовательно, Теорема 1 применима к \(S'_n\). А именно, для любой предельной точки \(S\), величина \((\sqrt{2}-1) S\) также должна иметь закон \(\mathcal{N}(0,1)\). Следовательно, предельных точек не существует.

Визуализация сходимости

Что означает, что последовательность сходится по распределению? Зафиксируем \(\mu\), центрированную вероятностную меру на \(\mathbb R\), и некоторое значение \(n\), ‘достаточно большое’ (как мы видели, насколько большое, зависит от \(\mu\), например, от её третьего момента), и рассмотрим i.i.d. величины \(X_1,\ldots,X_n\) с законом \(\mu\) и соответствующую им \(S_n\), как в Уравнение 1. Мы можем многократно, скажем \(N\) раз, независимо сгенерировать выборку \((X_1,\ldots,X_n)\) и, следовательно, \(S_n\). Центральная предельная теорема говорит нам, что с большой вероятностью доля выборок, для которых \(S_n\) попадает в заданный интервал \([a,b]\), приблизительно равна интегралу Гаусса по \([a,b]\). Здесь приблизительно означает, что вероятность этого события сходится к \(1\) по мере роста \(N\) и \(n\).

При большом \(n\) вероятность нахождения выборки в заданном интервале сходится к интегралу Гаусса по этому интервалу. В этом и заключается содержание центральной предельной теоремы. Здесь мы берём \(N\) выборок, строим гистограмму их распределения по интервалам и сравниваем результат с теоретической плотностью Гаусса.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| viewerHeight: 1080
#| editorHeight: 240
#| layout: vertical

# ========= 1. IMPORTS & CONFIGURATION =========
from shiny import App, render, ui, reactive, req
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm, cauchy

# Configuration dictionary for UI inputs
DIST_CONFIG = {
    'n_slider': {
        'min': 10, 'max': 10000, 'value': 1000, 'step': 1,
    },
    'N_samples_slider': {
        'min': 100, 'max': 10000, 'value': 5000, 'step': 100,
    },
    'distribution': {
        'choices': ['{-1, +1} (Bernoulli)', 'Uniform [-1, 1]', 'Standard Normal N(0,1)', 'Centered Exponential (Exp(1)-1)', 'Standard Cauchy'],
        'selected': '{-1, +1} (Bernoulli)',
    },
    'filename': {'value': 'clt_distribution.png'},
}

# ========= 2. UI DEFINITION =========
app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.h4("Convergence in Distribution"),
        ui.input_slider("n", "Sequence Length (n):", **DIST_CONFIG['n_slider']),
        ui.input_slider("N_samples", "Number of Samples (N):", **DIST_CONFIG['N_samples_slider']),
        ui.input_select("distribution", "Distribution:", **DIST_CONFIG['distribution']),
        ui.hr(),
        ui.input_action_button("run_button", "Run Simulation", class_="btn-success w-100"),
        ui.hr(),
        ui.h4("Saving Options"),
        ui.input_checkbox("save_plot", "Save Plot", value=False),
        ui.panel_conditional(
            "input.save_plot",
            ui.input_text("filename", "Filename:", **DIST_CONFIG['filename']),
        ),
        title="Controls",
    ),
    ui.output_plot("dist_plot", height="700px"),
    title="Distribution of the Normalized Sum",
)

# ========= 3. SERVER LOGIC =========
def server(input, output, session):
    # Reactive value to hold the generated matplotlib figure
    figure_to_display = reactive.Value(None)

    # --- Helper Functions (from original script) ---
    def dist_generate_steps(dist_name, size):
        if dist_name == '{-1, +1} (Bernoulli)': return np.random.choice([-1, 1], size=size)
        if dist_name == 'Uniform [-1, 1]': return np.random.uniform(-1, 1, size=size)
        if dist_name == 'Standard Normal N(0,1)': return np.random.randn(*size)
        if dist_name == 'Centered Exponential (Exp(1)-1)': return np.random.standard_exponential(size=size) - 1
        if dist_name == 'Standard Cauchy': return np.random.standard_cauchy(size=size)
        raise ValueError(f"Unknown distribution: {dist_name}")

    def dist_get_limit_pdf(dist_name, n):
        if dist_name in ['{-1, +1} (Bernoulli)', 'Standard Normal N(0,1)', 'Centered Exponential (Exp(1)-1)']:
            return 'Normal(0, 1)', lambda x: norm.pdf(x, loc=0, scale=1)
        if dist_name == 'Uniform [-1, 1]':
            # The variance of U[-1,1] is (1 - (-1))^2 / 12 = 4/12 = 1/3
            return 'Normal(0, 1/√3)', lambda x: norm.pdf(x, loc=0, scale=np.sqrt(1/3))
        if dist_name == 'Standard Cauchy':
            # The normalized sum of Cauchy variables is still Cauchy.
            # S_n / n ~ Cauchy(0,1), so S_n / sqrt(n) is not standard.
            # Let's assume the user wants to see the stable distribution, which is just Cauchy.
            return 'Cauchy(0, 1)', lambda x: cauchy.pdf(x, loc=0, scale=1)

    @reactive.Effect
    @reactive.event(input.run_button, ignore_init=True)
    def _():
        """This function runs ONLY when the run_button is clicked."""
        n = input.n()
        N = input.N_samples()
        dist_name = input.distribution()

        try:
            # Generate the sample data
            X = dist_generate_steps(dist_name, size=(N, n))
            # For Cauchy, the stable sum is normalized by n, not sqrt(n)
            if dist_name == 'Standard Cauchy':
                S_n_samples = np.sum(X, axis=1) / n
            else:
                S_n_samples = np.sum(X, axis=1) / np.sqrt(n)

            # Create the plot
            fig, ax = plt.subplots(figsize=(10, 6))
            num_bins = min(150, max(20, N // 100))
            ax.hist(S_n_samples, bins=num_bins, density=True, alpha=0.75, label=f'Empirical Dist. of $S_n$ (N={N:,})')
            
            # Get and plot the theoretical PDF
            limit_name, limit_pdf = dist_get_limit_pdf(dist_name, n)
            x_min, x_max = ax.get_xlim()
            # For Cauchy, the range can be very wide, so we limit it for plotting
            if dist_name == 'Standard Cauchy':
                x_min, x_max = -10, 10
            x_axis = np.linspace(x_min, x_max, 400)
            ax.plot(x_axis, limit_pdf(x_axis), 'r-', lw=2, alpha=0.9, label=f'Theoretical PDF: {limit_name}')

            ax.set_title(f'Distribution of Normalized Sum for $X_i \\sim$ {dist_name}')
            ax.set_xlabel('Value of the Normalized Sum')
            ax.set_ylabel('Probability Density')
            ax.legend()
            ax.grid(True, linestyle='--', alpha=0.6)
            ax.set_xlim(x_min, x_max) # Enforce x-limits

            # Save if requested
            if input.save_plot():
                req(input.filename(), cancel_output=True)
                fig.savefig(input.filename(), dpi=400, bbox_inches='tight')
            
            # Update the reactive value to display the plot
            figure_to_display.set(fig)
            plt.close(fig)

        except Exception as e:
            fig, ax = plt.subplots()
            ax.text(0.5, 0.5, f"An error occurred: {e}", ha='center', va='center', wrap=True)
            figure_to_display.set(fig)
            plt.close(fig)

    @output
    @render.plot
    def dist_plot():
        """Renders the plot from the reactive value."""
        fig = figure_to_display.get()
        req(fig)  # Do not render until the button is clicked
        return fig

# ========= 4. APP INSTANTIATION =========
app = App(app_ui, server)
Рисунок 1: При большом \(n\) вероятность нахождения выборки в заданном интервале сходится к интегралу Гаусса по этому интервалу. В этом и заключается содержание центральной предельной теоремы. Здесь мы берём \(N\) выборок, строим гистограмму их распределения по интервалам и сравниваем результат с теоретической плотностью Гаусса.

С другой стороны, тот факт, что \(S_n\) не сходится п.н., означает, что если мы зафиксируем одну реализацию (так сказать, одно \(\omega\)) и будем отслеживать значение \(S_n\) в зависимости от \(n\), оно не сойдётся ни к какому значению.

Каждая отдельная реализация не сходится как функция от \(n\). Мы генерируем \(X_i\) как i.i.d. и строим график \(S_n\) как функции от \(n\). Даже при больших \(n\) график колеблется, и сходимость не наступает.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| viewerHeight: 1080
#| editorHeight: 240
#| layout: vertical

# ========= 1. IMPORTS & CONFIGURATION =========
from shiny import App, render, ui, reactive, req
import numpy as np
import matplotlib.pyplot as plt

# Configuration dictionary for UI inputs
PATHS_CONFIG = {
    'n_slider': {
        'min': 1000, 'max': 1000000, 'value': 10000, 'step': 1000,
    },
    'N_slider': {
        'min': 1, 'max': 50, 'value': 5, 'step': 1,
    },
    'distribution': {
        'choices': ['{-1, +1} (Bernoulli)', 'Uniform [-1, 1]', 'Standard Normal N(0,1)', 'Standard Cauchy', 'Centered Exponential (Exp(1)-1)'],
        'selected': 'Standard Normal N(0,1)',
    },
    'linewidth_slider': {
        'min': 0.1, 'max': 3.0, 'value': 1.0, 'step': 0.1,
    },
    'filename': {'value': 'clt_paths.png'},
}

# ========= 2. UI DEFINITION =========
app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.h4("Path Trajectory Simulation"),
        ui.input_slider("n", "Sequence Length (n):", **PATHS_CONFIG['n_slider']),
        ui.input_slider("N", "Number of Paths:", **PATHS_CONFIG['N_slider']),
        ui.input_select("distribution", "Distribution:", **PATHS_CONFIG['distribution']),
        ui.hr(),
        ui.h4("Plotting & Saving Options"),
        ui.input_slider("linewidth", "Line Width:", **PATHS_CONFIG['linewidth_slider']),
        ui.input_action_button("resample_button", "Resample & Plot", class_="btn-success w-100"),
        ui.hr(),
        ui.input_checkbox("save_plot", "Save Plot", value=False),
        ui.panel_conditional(
            "input.save_plot",
            ui.input_text("filename", "Filename:", **PATHS_CONFIG['filename']),
        ),
        title="Controls",
    ),
    ui.output_plot("paths_plot", height="600px"),
    title="CLT Paths",
)

# ========= 3. SERVER LOGIC =========
def server(input, output, session):
    # Reactive value to hold the generated matplotlib figure
    figure_to_display = reactive.Value(None)

    def paths_generate_steps(dist_name, size):
        """Generates random steps based on the selected distribution."""
        if dist_name == '{-1, +1} (Bernoulli)':
            return np.random.choice([-1, 1], size=size)
        if dist_name == 'Uniform [-1, 1]':
            return np.random.uniform(-1, 1, size=size)
        if dist_name == 'Standard Normal N(0,1)':
            return np.random.randn(*size)
        if dist_name == 'Standard Cauchy':
            return np.random.standard_cauchy(size=size)
        if dist_name == 'Centered Exponential (Exp(1)-1)':
            return np.random.standard_exponential(size=size) - 1
        raise ValueError(f"Unknown distribution: {dist_name}")

    @reactive.Effect
    @reactive.event(input.resample_button, ignore_init=True)
    def _():
        """This function runs ONLY when the resample_button is clicked."""
        dist_name = input.distribution()
        max_n = input.n()
        N_trajectories = input.N()
        line_width = input.linewidth()
        
        try:
            # Generate data
            X = paths_generate_steps(dist_name, size=(N_trajectories, max_n))
            cumulative_sums = np.cumsum(X, axis=1)
            n_axis = np.arange(1, max_n + 1)
            S_n = cumulative_sums / np.sqrt(n_axis)
            
            # Create plot
            fig, ax = plt.subplots(figsize=(10, 6))
            for i in range(N_trajectories):
                ax.plot(n_axis, S_n[i, :], alpha=0.7, linewidth=line_width)
            
            ax.set_title(f'{N_trajectories} Sample Paths of $S_n$ using {dist_name} steps')
            ax.set_xlabel('n (Sequence Length)')
            ax.set_ylabel('$S_n = (X_1 + ... + X_n) / \\sqrt{n}$')
            ax.grid(True, linestyle='--', alpha=0.6)
            ax.axhline(0, color='black', linewidth=0.8)
            
            # Save the plot if requested
            if input.save_plot():
                req(input.filename(), cancel_output=True)
                fig.savefig(input.filename(), dpi=400, bbox_inches='tight')
            
            # Set the reactive value, which will trigger the plot output to update
            figure_to_display.set(fig)
            
            # Close the figure object to free memory
            plt.close(fig)

        except Exception as e:
            # On error, create a plot showing the error message
            fig, ax = plt.subplots()
            ax.text(0.5, 0.5, f"An error occurred: {e}", ha='center', va='center', wrap=True)
            figure_to_display.set(fig)
            plt.close(fig)

    @output
    @render.plot
    def paths_plot():
        """Renders the plot held in the reactive value."""
        fig = figure_to_display.get()
        # Don't render anything until the button has been clicked at least once
        req(fig)
        return fig

# ========= 4. APP INSTANTIATION =========
app = App(app_ui, server)
Рисунок 2: Каждая отдельная реализация не сходится как функция от \(n\). Мы генерируем \(X_i\) как i.i.d. и строим график \(S_n\) как функции от \(n\). Даже при больших \(n\) график колеблется, и сходимость не наступает. Здесь мы строим \(N=5\) различных реализаций до \(n=10^6\).