В мире Data Science принятие решений на основе данных требует не только вычислений, но и грамотной интерпретации статистических результатов. Одними из ключевых понятий при проверке гипотез являются доверительная вероятность (confidence level) и уровень значимости (significance level).
Эти параметры помогают нам определить, насколько можно доверять полученным выводам и где пролегает граница между случайным совпадением и статистически значимым результатом. В этой статье мы разберем, что означают эти понятия, как они связаны между собой и какое значение имеют при анализе данных и построении моделей.
Вероятность и статистические гипотезы
Статистический вывод основан на идее выборки — мы наблюдаем лишь часть данных из генеральной совокупности и пытаемся сделать выводы о всей совокупности. Это фундаментальное ограничение приводит к неизбежной неопределенности в наших выводах. В отличие от детерминистических алгоритмов, где результат всегда однозначен, статистические методы всегда сопряжены с вероятностью ошибки.
Статистические гипотезы — это утверждения о параметрах или свойствах генеральной совокупности, которые мы либо принимаем, либо отвергаем на основе данных выборки. Например, мы можем выдвинуть гипотезу, что средняя доходность новой торговой стратегии превышает определенное пороговое значение. Здесь возникает важный вопрос: как определить, достаточно ли убедительны наши данные для принятия или отклонения гипотезы?
Нулевая гипотеза (H₀) обычно представляет собой утверждение об отсутствии эффекта или разницы, в то время как альтернативная гипотеза (H₁) предполагает наличие эффекта. Процесс проверки гипотез требует баланса между двумя типами ошибок:
- Ошибка I рода (ложноположительный результат): отклонение H₀, когда она верна;
- Ошибка II рода (ложноотрицательный результат): сохранение H₀, когда она неверна.
Именно в этом контексте возникают понятия уровня значимости и доверительной вероятности.
Доверительная вероятность и доверительные интервалы
Доверительная вероятность (confidence level) — это вероятность того, что доверительный интервал, построенный на основе выборки, содержит истинное значение параметра генеральной совокупности. Обычно доверительная вероятность выражается как 1-α, где α — уровень значимости.
Например, 95% доверительная вероятность (что соответствует α = 0.05) означает, что если мы повторим процесс построения доверительного интервала для многих выборок из одной и той же генеральной совокупности, то примерно 95% этих интервалов будут содержать истинное значение параметра.
Доверительный интервал можно интерпретировать как диапазон, в котором с определенной вероятностью находится истинное значение параметра. Важно понимать, что доверительный интервал не говорит о вероятности того, что параметр находится в этом интервале — после построения интервала параметр либо находится в нем (с вероятностью 100%), либо нет (с вероятностью 0%). Доверительная вероятность относится к процедуре построения интервала, а не к конкретному интервалу.
Формула для вычисления доверительного интервала для среднего при нормальном распределении выглядит следующим образом:
x̄ ± z₍α/₂₎ × (σ/√n)
где:
- x̄ — среднее значение выборки;
- z₍α/₂₎ — критическое значение из стандартного нормального распределения;
- σ — стандартное отклонение;
- n — размер выборки.
Для 95% доверительного интервала z₍α/₂₎ = 1.96, а для 99% доверительного интервала z₍α/₂₎ = 2.576.
Уровень значимости и p-значение
Уровень значимости (significance level) или α — это вероятность отклонить нулевую гипотезу, когда она верна (вероятность ошибки I рода). По сути, это порог, при котором мы готовы признать результат статистически значимым.
Традиционно в научных исследованиях используются уровни значимости 0.05, 0.01 и 0.001, что соответствует доверительным вероятностям 95%, 99% и 99.9% соответственно. Выбор конкретного уровня значимости зависит от области применения и последствий потенциальных ошибок. Например, в медицинских исследованиях, где цена ошибки может быть человеческая жизнь, часто используют более строгие уровни значимости (0.01 или даже 0.001).
p-значение (p-value) — это вероятность получить наблюдаемое значение статистики или более экстремальное при условии, что нулевая гипотеза верна. Если p-значение меньше заранее выбранного уровня значимости α, мы отклоняем нулевую гипотезу.
Важно понимать, что p-значение не является вероятностью того, что нулевая гипотеза верна. Это вероятность получить наблюдаемые или более экстремальные данные при условии, что нулевая гипотеза верна. Эта тонкость часто упускается из виду и приводит к неправильной интерпретации результатов.
Практическое применение в анализе данных
Давайте рассмотрим практический пример построения доверительных интервалов для оценки средней доходности финансового инструмента. Я буду использовать данные по ETF на развивающиеся рынки (EEM), чтобы продемонстрировать, как вычислять и интерпретировать доверительные интервалы.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from scipy import stats
import seaborn as sns
# Настройки для графиков
plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['font.size'] = 12
# Загрузка данных
ticker = 'EEM' #iShares MSCI Emerging Markets ETF
start_date = '2022-07-01'
end_date = '2025-07-01'
data = yf.download(ticker, start=start_date, end=end_date)
# Проверка на наличие Multiindex
if isinstance(data.index, pd.MultiIndex):
data = data.reset_index(level=0)
# Расчет дневных доходностей
data['Returns'] = data['Close'].pct_change() * 100 # в процентах
data = data.dropna()
# Описательная статистика
mean_return = data['Returns'].mean()
std_return = data['Returns'].std()
n = len(data)
# Вычисление доверительных интервалов
confidence_levels = [0.90, 0.95, 0.99]
results = []
for conf_level in confidence_levels:
alpha = 1 - conf_level
z_critical = stats.norm.ppf(1 - alpha/2)
margin_error = z_critical * (std_return / np.sqrt(n))
lower_bound = mean_return - margin_error
upper_bound = mean_return + margin_error
results.append([conf_level, lower_bound, upper_bound, margin_error])
# Создание датафрейма с результатами
conf_intervals = pd.DataFrame(
results,
columns=['Confidence Level', 'Lower Bound', 'Upper Bound', 'Margin of Error']
)
# Вывод результатов
print(f"Средняя дневная доходность {ticker}: {mean_return:.4f}%")
print(f"Стандартное отклонение: {std_return:.4f}%")
print(f"Размер выборки: {n} дней")
print("\nДоверительные интервалы:")
print(conf_intervals.to_string(index=False, float_format=lambda x: f"{x:.4f}"))
# Визуализация доверительных интервалов
plt.figure(figsize=(10, 6))
y_positions = range(len(confidence_levels))
plt.errorbar(
[mean_return] * len(confidence_levels),
y_positions,
xerr=conf_intervals['Margin of Error'],
fmt='o',
color='black',
capsize=5,
capthick=2,
elinewidth=2,
markersize=8
)
plt.yticks(y_positions, [f"{cl:.0%}" for cl in confidence_levels])
plt.axvline(x=0, color='gray', linestyle='--', alpha=0.7)
plt.xlabel('Средняя дневная доходность (%)')
plt.ylabel('Доверительный уровень')
plt.title(f'Доверительные интервалы для средней доходности {ticker}')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Средняя дневная доходность EEM: 0.0416%
Стандартное отклонение: 1.1107%
Размер выборки: 750 дней
Доверительные интервалы:
Confidence Level Lower Bound Upper Bound Margin of Error
0.9000 -0.0251 0.1083 0.0667
0.9500 -0.0379 0.1211 0.0795
0.9900 -0.0628 0.1461 0.1045

Рис. 1: Доверительные интервалы средней доходности ETF iShares MSCI Emerging Markets за последние 3 года
Приведенный код демонстрирует построение доверительных интервалов для средней дневной доходности ETF на развивающиеся рынки. Я использовал библиотеку yfinance для получения исторических данных и вычислил доверительные интервалы для трех уровней доверия: 90%, 95% и 99%.
Что делает этот код?
- Сначала мы загружаем исторические данные по выбранному тикеру;
- Затем вычисляем дневные доходности;
- После этого рассчитываем среднюю доходность и стандартное отклонение, которые необходимы для построения доверительных интервалов;
- Для каждого уровня доверия мы вычисляем критическое значение z (используя обратную функцию нормального распределения);
- Далее рассчитываем погрешность и определяем нижнюю и верхнюю границы доверительного интервала;
- Наконец, мы визуализируем результаты с помощью графика, где отображается средняя доходность и соответствующие доверительные интервалы.
Интерпретация результатов крайне важна. Например, если 95% доверительный интервал для средней дневной доходности составляет [-0.1%, 0.3%], это означает, что с вероятностью 95% истинное значение средней доходности находится в этом диапазоне. Если интервал включает ноль, мы не можем с уверенностью утверждать, что средняя доходность отличается от нуля (то есть не можем отвергнуть нулевую гипотезу о том, что средняя доходность равна нулю).
Ширина доверительного интервала зависит от трех факторов:
- Уровень доверия — чем выше уровень доверия, тем шире интервал;
- Стандартное отклонение — чем больше разброс данных, тем шире интервал;
- Размер выборки — чем больше наблюдений, тем интервал более узкий.
Проверка статистических гипотез и p-значения
Теперь давайте рассмотрим практический пример проверки гипотезы о том, превосходит ли наша торговая стратегия некоторый бенчмарк. Мы проведем t-тест для сравнения средних доходностей и проанализируем полученные p-значения.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from scipy import stats
import seaborn as sns
# Загрузка данных для двух активов
ticker1 = 'MELI' # Наша стратегия (или актив), в данном случае MercadoLibre ("латинский Amazon")
ticker2 = 'EEM' # Бенчмарк iShares MSCI Emerging Markets ETF
start_date = '2022-07-01'
end_date = '2025-07-01'
# Загрузка данных
data1 = yf.download(ticker1, start=start_date, end=end_date)
data2 = yf.download(ticker2, start=start_date, end=end_date)
# Проверка на наличие Multiindex
if isinstance(data1.index, pd.MultiIndex):
data1 = data1.reset_index(level=0)
if isinstance(data2.index, pd.MultiIndex):
data2 = data2.reset_index(level=0)
# Расчет дневных доходностей
data1['Returns'] = data1['Close'].pct_change() * 100 # в процентах
data2['Returns'] = data2['Close'].pct_change() * 100 # в процентах
# Объединение данных
merged_data = pd.DataFrame({
'Strategy': data1['Returns'].dropna(),
'Benchmark': data2['Returns'].dropna()
})
# Обеспечиваем совпадение дат
merged_data = merged_data.dropna()
# Описательная статистика
mean1 = merged_data['Strategy'].mean()
mean2 = merged_data['Benchmark'].mean()
std1 = merged_data['Strategy'].std()
std2 = merged_data['Benchmark'].std()
n = len(merged_data)
# Проведение t-теста для зависимых выборок (paired t-test)
t_stat, p_value = stats.ttest_rel(merged_data['Strategy'], merged_data['Benchmark'])
# Вычисление эффекта (разницы средних)
mean_diff = mean1 - mean2
# Доверительный интервал для разницы
alpha = 0.05 # для 95% доверительного интервала
degrees_freedom = n - 1
t_critical = stats.t.ppf(1 - alpha/2, degrees_freedom)
std_diff = np.std(merged_data['Strategy'] - merged_data['Benchmark'], ddof=1)
standard_error = std_diff / np.sqrt(n)
margin_error = t_critical * standard_error
ci_lower = mean_diff - margin_error
ci_upper = mean_diff + margin_error
# Вывод результатов
print(f"Средняя доходность стратегии ({ticker1}): {mean1:.4f}%")
print(f"Средняя доходность бенчмарка ({ticker2}): {mean2:.4f}%")
print(f"Разница средних: {mean_diff:.4f}%")
print("\nРезультаты t-теста:")
print(f"t-статистика: {t_stat:.4f}")
print(f"p-значение: {p_value:.6f}")
print(f"95% доверительный интервал для разницы: [{ci_lower:.4f}, {ci_upper:.4f}]")
# Оценка статистической значимости
alpha_levels = [0.1, 0.05, 0.01, 0.001]
for alpha in alpha_levels:
significant = p_value < alpha
print(f"Статистически значимо при α = {alpha}: {significant}")
# Визуализация распределения доходностей
plt.figure(figsize=(12, 6))
sns.kdeplot(merged_data['Strategy'], label=f'Стратегия ({ticker1})', color='black')
sns.kdeplot(merged_data['Benchmark'], label=f'Бенчмарк ({ticker2})', color='gray')
plt.axvline(mean1, color='black', linestyle='--', alpha=0.7,
label=f'Среднее {ticker1}: {mean1:.4f}%')
plt.axvline(mean2, color='gray', linestyle='--', alpha=0.7,
label=f'Среднее {ticker2}: {mean2:.4f}%')
plt.axvline(x=0, color='red', linestyle='-', alpha=0.3)
plt.xlabel('Дневная доходность (%)')
plt.ylabel('Плотность вероятности')
plt.title('Распределение дневных доходностей')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Визуализация накопленной доходности
cumulative_returns = (1 + merged_data / 100).cumprod() - 1
plt.figure(figsize=(12, 6))
plt.plot(cumulative_returns.index, cumulative_returns['Strategy'] * 100,
label=f'Стратегия ({ticker1})', color='black')
plt.plot(cumulative_returns.index, cumulative_returns['Benchmark'] * 100,
label=f'Бенчмарк ({ticker2})', color='gray')
plt.xlabel('Дата')
plt.ylabel('Накопленная доходность (%)')
plt.title('Сравнение накопленной доходности')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Средняя доходность стратегии (MELI): 0.2216%
Средняя доходность бенчмарка (EEM): 0.0416%
Разница средних: 0.1800%
Результаты t-теста:
t-статистика: 1.9600
p-значение: 0.050372
95% доверительный интервал для разницы: [-0.0003, 0.3603]
Статистически значимо при α = 0.1: True
Статистически значимо при α = 0.05: False
Статистически значимо при α = 0.01: False
Статистически значимо при α = 0.001: False

Рис. 2: Динамика накопленной доходности акций MercadoLibre и индекса развивающихся стран iShares MSCI Emerging Markets ETF

Рис. 3: Распределение дневных доходностей акции MercadoLibre и индекса развивающихся стран
В приведенном примере я сравниваю доходности двух активов на развивающихся рынках: MELI (MercadoLibre) и EEM (iShares MSCI Emerging Markets ETF) в роли бенчмарка. Я использую парный t-тест (paired t-test), который учитывает зависимость между выборками, так как оба актива реагируют на одни и те же рыночные условия.
Код выполняет следующие операции:
- Загружает исторические данные для обоих активов;
- Рассчитывает дневные доходности;
- Проводит t-тест для проверки гипотезы о равенстве средних;
- Вычисляет доверительный интервал для разницы средних;
- Визуализирует распределение доходностей и накопленную доходность.
Интерпретация p-значения здесь очень важна. p-значение представляет собой вероятность получить наблюдаемую или более экстремальную разницу в средних доходностях при условии, что истинная разница равна нулю (нулевая гипотеза). Если p-значение меньше выбранного уровня значимости α (например, 0.05), мы отклоняем нулевую гипотезу и заключаем, что разница статистически значима.
Доверительный интервал для разницы средних дополняет анализ p-значения. Если интервал не включает ноль, это также указывает на статистически значимую разницу. Более того, доверительный интервал предоставляет информацию о величине эффекта и его неопределенности, что может быть важнее, чем просто знание о наличии статистически значимой разницы.
Визуализация накопленной доходности помогает оценить практическую значимость обнаруженной разницы. Даже если разница статистически значима, она может быть слишком мала, чтобы оправдать затраты на реализацию новой стратегии или изменение инвестиционного портфеля.
Множественное тестирование и проблема ложных открытий
Одна из наиболее распространенных ошибок в статистическом анализе связана с проблемой множественного тестирования. Если мы проводим много статистических тестов, вероятность получить хотя бы один ложноположительный результат возрастает.
Например, если мы тестируем 20 независимых торговых стратегий с уровнем значимости 0.05, вероятность того, что хотя бы одна из них покажет статистически значимый результат чисто случайно, составляет около 64% (1 — 0.95^20).
Существует несколько методов коррекции для множественного тестирования, включая поправку Бонферрони, метод Холма-Бонферрони и процедуру контроля ложных открытий Бенджамини-Хохберга (FDR). Давайте рассмотрим пример применения этих методов при оценке эффективности нескольких торговых стратегий.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from scipy import stats
import seaborn as sns
from statsmodels.stats.multitest import multipletests
import warnings
warnings.filterwarnings('ignore')
# Настройки для графиков
plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['font.size'] = 12
# Тикеры для различных стратегий (или активов)
tickers = ['EWZ', 'RSX', 'FXI', 'EWW', 'EWY', 'EWJ', 'EWC', 'EWA', 'EWG', 'EWU']
benchmark_ticker = 'SPY' # Бенчмарк
# Период анализа
start_date = '2022-07-01'
end_date = '2025-07-01'
print("Загружаем данные...")
# Загрузка данных бенчмарка
print(f"Загружаем {benchmark_ticker}...")
benchmark_data = yf.download(benchmark_ticker, start=start_date, end=end_date, progress=False)
benchmark_returns = benchmark_data['Close'].pct_change().dropna()
# Хранение результатов
results = []
returns_data = {}
failed_tickers = []
# Вычисляем статистики бенчмарка один раз
benchmark_mean = float(benchmark_returns.mean())
benchmark_std = float(benchmark_returns.std())
for ticker in tickers:
try:
print(f"Обрабатываем {ticker}...")
# Загрузка данных
stock_data = yf.download(ticker, start=start_date, end=end_date, progress=False)
if stock_data.empty:
print(f"Нет данных для {ticker}")
failed_tickers.append(ticker)
continue
stock_returns = stock_data['Close'].pct_change().dropna()
# Обеспечиваем совпадение дат
common_index = benchmark_returns.index.intersection(stock_returns.index)
if len(common_index) > 30: # Минимум 30 наблюдений для значимого анализа
aligned_stock = stock_returns.loc[common_index]
aligned_benchmark = benchmark_returns.loc[common_index]
returns_data[ticker] = aligned_stock
# Преобразуем в numpy массивы для получения скалярных значений
stock_array = aligned_stock.values
benchmark_array = aligned_benchmark.values
# Проведение t-теста
t_stat, p_value = stats.ttest_ind(stock_array, benchmark_array, equal_var=False)
# Вычисление статистик - убеждаемся, что это скаляры
mean_stock = float(np.mean(stock_array))
mean_benchmark = float(np.mean(benchmark_array))
std_stock = float(np.std(stock_array, ddof=1))
mean_diff = mean_stock - mean_benchmark
# Сохранение результатов
results.append({
'Ticker': ticker,
'Mean_Return': mean_stock * 100, # в процентах
'Benchmark_Return': mean_benchmark * 100, # в процентах
'Difference': mean_diff * 100, # в процентах
'Volatility': std_stock * 100, # в процентах
't_statistic': float(t_stat),
'p_value': float(p_value),
'Observations': len(common_index)
})
else:
print(f"Предупреждение: Недостаточно совпадающих данных для {ticker} ({len(common_index)} наблюдений)")
failed_tickers.append(ticker)
except Exception as e:
print(f"Ошибка обработки {ticker}: {str(e)}")
failed_tickers.append(ticker)
# Создание датафрейма с результатами
if len(results) == 0:
print("Не удалось обработать данные. Проверьте тикеры и диапазон дат.")
exit()
results_df = pd.DataFrame(results)
print(f"\nУспешно обработано {len(results_df)} ETF")
if failed_tickers:
print(f"Не удалось обработать: {', '.join(failed_tickers)}")
# Применение методов коррекции для множественного тестирования
p_values = results_df['p_value'].values
# 1. Поправка Бонферрони
bonferroni_results = multipletests(p_values, alpha=0.05, method='bonferroni')
results_df['Bonferroni_Significant'] = bonferroni_results[0]
results_df['Bonferroni_Adjusted_p'] = bonferroni_results[1]
# 2. Метод Холма-Бонферрони
holm_results = multipletests(p_values, alpha=0.05, method='holm')
results_df['Holm_Significant'] = holm_results[0]
results_df['Holm_Adjusted_p'] = holm_results[1]
# 3. Процедура FDR Бенджамини-Хохберга
fdr_results = multipletests(p_values, alpha=0.05, method='fdr_bh')
results_df['FDR_Significant'] = fdr_results[0]
results_df['FDR_Adjusted_p'] = fdr_results[1]
# Сортировка по p-значению
results_df = results_df.sort_values('p_value').reset_index(drop=True)
# Вывод результатов
print(f"\nРезультаты сравнения с бенчмарком {benchmark_ticker}:")
print("="*100)
# Делаем лучше форматирование таблицы
display_df = results_df[['Ticker', 'Mean_Return', 'Benchmark_Return', 'Difference',
'Volatility', 'p_value', 'Bonferroni_Adjusted_p',
'Holm_Adjusted_p', 'FDR_Adjusted_p']].round(4)
print(display_df.to_string(index=False))
# Сводка значимых результатов
print("\n" + "="*100)
print("СВОДКА ЗНАЧИМЫХ РЕЗУЛЬТАТОВ (α = 0.05):")
print("="*100)
methods = [('Бонферрони', 'Bonferroni_Significant'),
('Холм', 'Holm_Significant'),
('FDR', 'FDR_Significant')]
for method_name, sig_col in methods:
significant_count = int(results_df[sig_col].sum())
print(f"{method_name:12}: {significant_count:2d} из {len(results_df):2d} ETF показывают значимое различие")
if significant_count > 0:
sig_results = results_df[results_df[sig_col] == True]
for _, row in sig_results.iterrows():
direction = "превосходит" if row['Difference'] > 0 else "уступает"
print(f" {row['Ticker']} {direction} бенчмарк на {abs(row['Difference']):.3f}% ежедневно")
print(f"\nЛучший ETF: {results_df.loc[results_df['Mean_Return'].idxmax(), 'Ticker']}")
print(f"Худший ETF: {results_df.loc[results_df['Mean_Return'].idxmin(), 'Ticker']}")
# Визуализация 1: p-значения до и после коррекции
fig, ax = plt.subplots(figsize=(14, 8))
# Создание позиций для столбцов
x_pos = np.arange(len(results_df))
width = 0.2
# Создание столбцов для каждого метода
bars1 = ax.bar(x_pos - 1.5*width, results_df['p_value'], width,
label='Исходное p-значение', alpha=0.8, color='skyblue')
bars2 = ax.bar(x_pos - 0.5*width, results_df['Bonferroni_Adjusted_p'], width,
label='Бонферрони', alpha=0.8, color='lightcoral')
bars3 = ax.bar(x_pos + 0.5*width, results_df['Holm_Adjusted_p'], width,
label='Холм', alpha=0.8, color='lightgreen')
bars4 = ax.bar(x_pos + 1.5*width, results_df['FDR_Adjusted_p'], width,
label='FDR', alpha=0.8, color='gold')
# Добавление линии значимости
ax.axhline(y=0.05, color='red', linestyle='--', alpha=0.7, linewidth=2, label='α = 0.05')
ax.set_xlabel('Тикер ETF')
ax.set_ylabel('p-значение')
ax.set_title('p-значения до и после коррекции на множественное тестирование')
ax.set_xticks(x_pos)
ax.set_xticklabels(results_df['Ticker'], rotation=45)
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
ax.set_yscale('log') # Логарифмическая шкала для лучшего отображения малых p-значений
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Визуализация 2: Сравнение средней доходности
plt.figure(figsize=(12, 6))
sorted_results = results_df.sort_values('Mean_Return', ascending=True)
# Создание горизонтальной столбчатой диаграммы для лучшей читаемости
bars = plt.barh(range(len(sorted_results)), sorted_results['Mean_Return'],
color='steelblue', alpha=0.7)
# Добавление линии бенчмарка
benchmark_return = sorted_results['Benchmark_Return'].iloc[0]
plt.axvline(x=benchmark_return, color='red', linestyle='--',
alpha=0.8, linewidth=2, label=f'Бенчмарк {benchmark_ticker} ({benchmark_return:.3f}%)')
# Выделение значимо отличающихся ETF (используем FDR как менее консервативный)
for i, (_, row) in enumerate(sorted_results.iterrows()):
if row['FDR_Significant']:
bars[i].set_color('orange')
bars[i].set_alpha(0.9)
plt.ylabel('Тикер ETF')
plt.xlabel('Средняя дневная доходность (%)')
plt.title(f'Доходность ETF в сравнении с бенчмарком {benchmark_ticker}\n(Оранжевые столбцы показывают статистически значимое различие по методу FDR)')
plt.yticks(range(len(sorted_results)), sorted_results['Ticker'])
plt.legend()
plt.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.show()
# Визуализация 3: График риск-доходность
plt.figure(figsize=(10, 8))
# Создание точечного графика
scatter = plt.scatter(results_df['Volatility'], results_df['Mean_Return'],
s=100, alpha=0.7, c=results_df['p_value'],
cmap='RdYlBu_r', edgecolors='black', linewidth=0.5)
# Добавление точки бенчмарка
plt.scatter(benchmark_std * 100, benchmark_mean * 100, s=200, color='red',
marker='D', label=f'Бенчмарк {benchmark_ticker}', edgecolors='black')
# Добавление подписей для каждой точки
for _, row in results_df.iterrows():
plt.annotate(row['Ticker'],
(row['Volatility'], row['Mean_Return']),
xytext=(5, 5), textcoords='offset points',
fontsize=10, alpha=0.8, weight='bold')
plt.colorbar(scatter, label='p-значение (в сравнении с бенчмарком)')
plt.xlabel('Дневная волатильность (%)')
plt.ylabel('Средняя дневная доходность (%)')
plt.title(f'Профиль риск-доходность ETF в сравнении с бенчмарком {benchmark_ticker}\n(Цвет показывает статистическую значимость)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"\nАнализ завершен!")
print(f"Проанализировано {len(results_df)} ETF в сравнении с бенчмарком {benchmark_ticker}")
print(f"Период данных: {start_date} до {end_date}")
print(f"Примечание: Все статистические тесты сравнивают дневную доходность используя t-тест Уэлча")
Загружаем данные...
Загружаем SPY...
Обрабатываем EWZ...
Обрабатываем RSX...
Обрабатываем FXI...
Обрабатываем EWW...
Обрабатываем EWY...
Обрабатываем EWJ...
Обрабатываем EWC...
Обрабатываем EWA...
Обрабатываем EWG...
Обрабатываем EWU...
Успешно обработано 10 ETF
Результаты сравнения с бенчмарком SPY:
====================================================================================================
Ticker Mean_Return Benchmark_Return Difference Volatility p_value Bonferroni_Adjusted_p Holm_Adjusted_p FDR_Adjusted_p
RSX 0.0165 0.0761 -0.0596 0.3196 0.1557 1.0 1.0 0.8079
FXI 0.0423 0.0761 -0.0338 2.0289 0.6888 1.0 1.0 0.8079
EWA 0.0522 0.0761 -0.0239 1.2822 0.6987 1.0 1.0 0.8079
EWY 0.0499 0.0761 -0.0263 1.5430 0.7047 1.0 1.0 0.8079
EWG 0.0983 0.0761 0.0222 1.2789 0.7187 1.0 1.0 0.8079
EWC 0.0557 0.0761 -0.0204 1.1007 0.7205 1.0 1.0 0.8079
EWU 0.0587 0.0761 -0.0174 1.0170 0.7507 1.0 1.0 0.8079
EWZ 0.0535 0.0761 -0.0226 1.7162 0.7620 1.0 1.0 0.8079
EWJ 0.0598 0.0761 -0.0163 1.1429 0.7791 1.0 1.0 0.8079
EWW 0.0597 0.0761 -0.0164 1.4811 0.8079 1.0 1.0 0.8079
====================================================================================================
СВОДКА ЗНАЧИМЫХ РЕЗУЛЬТАТОВ (α = 0.05):
====================================================================================================
Бонферрони : 0 из 10 ETF показывают значимое различие
Холм : 0 из 10 ETF показывают значимое различие
FDR : 0 из 10 ETF показывают значимое различие
Лучший ETF: EWG
Худший ETF: RSX
Анализ завершен!
Проанализировано 10 ETF в сравнении с бенчмарком SPY
Период данных: 2022-07-01 до 2025-07-01
Примечание: Все статистические тесты сравнивают дневную доходность используя t-тест Уэлча

Рис. 4: Сравнение p-значений статистических тестов до и после применения методов коррекции на множественное тестирование. Представлены результаты t-тестов Уэлча для сравнения средней дневной доходности 10 региональных ETF с бенчмарком SPY за период с июля 2022 по июль 2025 года. Горизонтальная пунктирная линия показывает критический уровень значимости α = 0.05. Логарифмическая шкала по оси ординат использована для улучшения визуализации малых p-значений. Методы коррекции включают поправку Бонферрони, процедуру Холма-Бонферрони и контроль ложных открытий (FDR) по методу Бенджамини-Хохберга

Рис. 5: Ранжированное распределение средней дневной доходности региональных ETF в сравнении с бенчмарком SPY. ETF упорядочены по возрастанию доходности для демонстрации относительной эффективности. Красная пунктирная линия обозначает доходность бенчмарка SPY. Анализ основан на ежедневных логарифмических доходностях закрытия торгов за трехлетний период наблюдений

Рис. 6: Двумерное распределение региональных ETF в координатах риск-доходность относительно бенчмарка SPY. По оси абсцисс представлена дневная волатильность (стандартное отклонение дневных доходностей), по оси ординат — средняя дневная доходность. Цветовая градация точек отражает уровень статистической значимости различий с бенчмарком (чем холоднее цвет, тем выше значимость). Красный ромб обозначает позицию бенчмарка SPY. Распределение позволяет оценить эффективность ETF с поправкой на принимаемый риск и идентифицировать инструменты, демонстрирующие аномальную доходность относительно систематического риска
В этом примере я сравниваю доходности 10 ETF на различные страны с бенчмарком SPY (S&P 500). Для каждого актива проводится t-тест для проверки гипотезы о равенстве средних доходностей, а затем применяются три метода коррекции для множественного тестирования. Мы видим лишь один из сравниваемых с бенчмарком (SP500) индексов смог его обогнать за последние 3 года. Это EWG (ETF iShares MSCI Germany), то есть по сути акции компаний из Германии.
Давайте разберем, что делает этот код:
- Загружает исторические данные для всех активов и бенчмарка;
- Проводит t-тесты для сравнения каждого актива с бенчмарком;
- Применяет три метода коррекции для множественного тестирования: Бонферрони, Холма-Бонферрони и FDR;
- Визуализирует p-значения до и после коррекции, а также средние доходности.
Интерпретация результатов множественного тестирования
Понимание различных методов коррекции на множественное тестирование является ключевым для правильной интерпретации результатов. Рассмотрим подробнее каждый метод:
- Поправка Бонферрони — самый консервативный метод, который просто умножает исходные p-значения на количество проведенных тестов. Хотя этот метод эффективно контролирует вероятность ошибки I рода, он часто бывает слишком строгим, особенно при большом количестве тестов, что приводит к увеличению вероятности ошибки II рода (ложноотрицательные результаты);
- Метод Холма-Бонферрони — это последовательная процедура, которая сначала сортирует p-значения по возрастанию, а затем применяет все более строгие пороги для каждого последующего теста. Этот метод обеспечивает лучший баланс между контролем ошибок I и II рода;
- Процедура FDR Бенджамини-Хохберга — контролирует ожидаемую долю ложных открытий среди всех отклонений нулевой гипотезы. Этот метод является наименее консервативным из трех, что делает его особенно полезным в ситуациях, когда мы хотим обнаружить как можно больше потенциально интересных сигналов, даже ценой некоторого увеличения числа ложноположительных результатов.
В контексте оценки торговых стратегий выбор метода коррекции зависит от наших целей. Если мы хотим быть уверены в каждой выбранной стратегии (т.е. минимизировать риск включения неэффективных стратегий), то следует использовать более консервативные методы, такие как поправка Бонферрони. Если же мы готовы принять некоторое количество ложноположительных результатов, чтобы не пропустить потенциально прибыльные стратегии, то процедура FDR может быть более подходящей.
Важно отметить, что статистическая значимость не всегда означает практическую значимость. Стратегия может показывать статистически значимое превосходство над бенчмарком, но разница в доходности может быть слишком мала, чтобы оправдать затраты на ее реализацию, особенно с учетом транзакционных издержек и других факторов.
Мощность статистического теста и размер выборки
Мощность статистического теста — это вероятность правильно отклонить нулевую гипотезу, когда она действительно неверна. Мощность зависит от нескольких факторов:
- Размер выборки — чем больше наблюдений, тем выше мощность;
- Величина эффекта — чем больше разница между группами, тем выше мощность;
- Уровень значимости — более высокий уровень значимости (например, 0.10 вместо 0.05) увеличивает мощность, но также увеличивает риск ошибки I рода;
- Вариабельность данных — меньшая вариабельность приводит к более высокой мощности.
Расчет необходимого размера выборки для достижения желаемой мощности — важный этап планирования статистического исследования. Давайте рассмотрим, как это можно сделать на практике для оценки эффективности торговой стратегии.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
import seaborn as sns
from statsmodels.stats.power import ttest_power
# Настройки для графиков
plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['font.size'] = 12
# Параметры для расчета мощности и размера выборки
effect_sizes = np.arange(0.1, 1.1, 0.1) # Различные размеры эффекта (Cohen's d)
power_levels = [0.8, 0.9, 0.95] # Желаемые уровни мощности
alpha = 0.05 # Уровень значимости
# Функция для расчета необходимого размера выборки
def calculate_sample_size(effect_size, power, alpha=0.05):
"""
Вычисляет необходимый размер выборки для независимых t-тестов
используя итеративный поиск
"""
n = 10 # Начальная оценка
max_n = 50000 # Максимальный размер выборки для поиска
while n < max_n: calculated_power = ttest_power(effect_size, n, alpha, alternative='two-sided') if calculated_power >= power:
return n
n += 1
return max_n # Если не нашли подходящий размер
# Расчет необходимого размера выборки для различных размеров эффекта и уровней мощности
sample_sizes = {}
for power in power_levels:
sample_sizes[power] = []
for es in effect_sizes:
ss = calculate_sample_size(es, power, alpha)
sample_sizes[power].append(ss)
# Визуализация зависимости размера выборки от размера эффекта и мощности
plt.figure(figsize=(12, 8))
for power in power_levels:
plt.plot(effect_sizes, sample_sizes[power], marker='o', linewidth=2,
label=f'Мощность = {power}')
plt.xlabel('Размер эффекта (Cohen\'s d)')
plt.ylabel('Необходимый размер выборки (для каждой группы)')
plt.title('Зависимость необходимого размера выборки от размера эффекта и мощности')
plt.grid(True, alpha=0.3)
plt.legend()
plt.yscale('log')
plt.tight_layout()
plt.show()
# Теперь рассмотрим конкретный пример с реальными данными
# Предположим, что мы хотим обнаружить разницу в среднедневной доходности 0.1%
# между нашей стратегией и бенчмарком
# Оценка стандартного отклонения на основе исторических данных
historical_std = 1.5 # Предположим, что это стандартное отклонение дневных доходностей в процентах
# Расчет размера эффекта (Cohen's d)
effect_size = 0.1 / historical_std # Разница в 0.1% при стандартном отклонении 1.5%
# Расчет необходимого размера выборки для различных уровней мощности
print(f"Размер эффекта (Cohen's d) для разницы в 0.1% при std = {historical_std}%: {effect_size:.4f}")
print("\nНеобходимый размер выборки для каждой группы:")
for power in [0.8, 0.9, 0.95]:
sample_size = calculate_sample_size(effect_size, power, alpha)
trading_days = sample_size # Предполагаем, что каждое наблюдение - это торговый день
years = trading_days / 252 # Примерно 252 торговых дня в году
print(f"Мощность = {power}: {sample_size:.0f} наблюдений ({years:.2f} лет данных)")
# Визуализация мощности в зависимости от размера выборки для нашего эффекта
sample_sizes_range = np.arange(100, 5000, 100)
powers = []
for ss in sample_sizes_range:
power = ttest_power(effect_size, ss, alpha, alternative='two-sided')
powers.append(power)
plt.figure(figsize=(12, 6))
plt.plot(sample_sizes_range, powers, marker='o', linewidth=2, color='black')
plt.axhline(y=0.8, color='red', linestyle='--', alpha=0.7, label='Мощность = 0.8')
plt.axhline(y=0.9, color='blue', linestyle='--', alpha=0.7, label='Мощность = 0.9')
plt.axhline(y=0.95, color='green', linestyle='--', alpha=0.7, label='Мощность = 0.95')
plt.xlabel('Размер выборки (для каждой группы)')
plt.ylabel('Мощность статистического теста')
plt.title(f'Мощность теста для обнаружения разницы в {0.1}% при std = {historical_std}%')
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()
# Анализ компромисса между уровнем значимости и мощностью
alpha_levels = np.arange(0.01, 0.2, 0.01)
powers_by_alpha = []
fixed_sample_size = 500 # Фиксированный размер выборки
for a in alpha_levels:
power = ttest_power(effect_size, fixed_sample_size, a, alternative='two-sided')
powers_by_alpha.append(power)
plt.figure(figsize=(12, 6))
plt.plot(alpha_levels, powers_by_alpha, marker='o', linewidth=2, color='black')
plt.xlabel('Уровень значимости (α)')
plt.ylabel('Мощность статистического теста')
plt.title(f'Компромисс между уровнем значимости и мощностью\n(размер выборки = {fixed_sample_size}, размер эффекта = {effect_size:.4f})')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Дополнительный анализ: как размер эффекта влияет на мощность при фиксированной выборке
effect_sizes_detailed = np.arange(0.01, 0.5, 0.01)
powers_by_effect = []
for es in effect_sizes_detailed:
power = ttest_power(es, fixed_sample_size, alpha, alternative='two-sided')
powers_by_effect.append(power)
plt.figure(figsize=(12, 6))
plt.plot(effect_sizes_detailed, powers_by_effect, linewidth=2, color='navy')
plt.axhline(y=0.8, color='red', linestyle='--', alpha=0.7, label='Мощность = 0.8')
plt.axvline(x=effect_size, color='orange', linestyle='--', alpha=0.7,
label=f'Наш размер эффекта = {effect_size:.4f}')
plt.xlabel('Размер эффекта (Cohen\'s d)')
plt.ylabel('Мощность статистического теста')
plt.title(f'Зависимость мощности от размера эффекта\n(размер выборки = {fixed_sample_size}, α = {alpha})')
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()
# Практические выводы
print("\n" + "="*60)
print("ПРАКТИЧЕСКИЕ ВЫВОДЫ ДЛЯ ПЛАНИРОВАНИЯ ИССЛЕДОВАНИЯ:")
print("="*60)
print(f"\n1. Для обнаружения разницы в доходности {0.1}% при волатильности {historical_std}%:")
print(f" - Размер эффекта: {effect_size:.4f} (очень малый)")
print(f" - Это требует очень больших выборок (>3000 наблюдений)")
print(f"\n2. Альтернативные стратегии:")
minimal_detectable_effects = [0.2, 0.3, 0.5]
print(" Размеры эффекта, которые можно обнаружить за разумное время:")
for mde in minimal_detectable_effects:
es = mde / historical_std
ss = calculate_sample_size(es, 0.8, alpha)
years = ss / 252
print(f" - Разница {mde}%: размер эффекта {es:.4f}, нужно {ss} дней ({years:.1f} лет)")
print(f"\n3. Рекомендации:")
print(f" - Увеличить α до 0.1 может снизить требуемую выборку")
print(f" - Рассмотреть односторонний тест если есть четкие ожидания")
print(f" - Фокусироваться на больших размерах эффекта (>0.2% разница)")
print(f" - Использовать комбинированные метрики (Sharpe ratio, максимальная просадка)")
Размер эффекта (Cohen's d) для разницы в 0.1% при std = 1.5%: 0.0667
Необходимый размер выборки для каждой группы:
Мощность = 0.8: 1768 наблюдений (7.02 лет данных)
Мощность = 0.9: 2367 наблюдений (9.39 лет данных)
Мощность = 0.95: 2926 наблюдений (11.61 лет данных)

Рис. 7: Зависимость необходимого размера выборки от величины размера эффекта (Cohen’s d) для достижения заданных уровней статистической мощности при двустороннем t-тесте независимых выборок. Расчеты выполнены для уровня значимости α = 0.05 и соотношения размеров групп 1:1. Логарифмическая шкала по оси ординат используется для демонстрации экспоненциального роста требований к размеру выборки при уменьшении размера эффекта

Рис. 8: Статистическая мощность двустороннего t-теста как функция размера выборки для обнаружения разности средних дневных доходностей в 0.1% при стандартном отклонении 1.5% (размер эффекта Cohen’s d = 0.067). Горизонтальные пунктирные линии обозначают общепринятые пороговые значения статистической мощности 0.8, 0.9 и 0.95

Рис. 9: Зависимость статистической мощности теста от уровня значимости α при фиксированном размере выборки (n = 500) и размере эффекта Cohen’s d = 0.067. График иллюстрирует фундаментальный статистический компромисс между вероятностью ошибки I рода (ложное отклонение нулевой гипотезы) и вероятностью ошибки II рода (ложное принятие нулевой гипотезы). Положительная зависимость отражает возможность повышения чувствительности теста за счет ослабления требований к уровню значимости, что особенно актуально в условиях ограниченных объемов финансовых данных

Рис. 10: Статистическая мощность двустороннего t-теста как функция размера эффекта (Cohen’s d) при фиксированном размере выборки n = 500 и уровне значимости α = 0.05. Вертикальная пунктирная линия показывает расчетный размер эффекта для исследуемой разности доходностей (0.067), а горизонтальная — минимально приемлемый уровень мощности 0.8. График демонстрирует нелинейную зависимость между величиной исследуемого эффекта и способностью статистического теста его обнаружить
============================================================
ПРАКТИЧЕСКИЕ ВЫВОДЫ ДЛЯ ПЛАНИРОВАНИЯ ИССЛЕДОВАНИЯ:
============================================================
1. Для обнаружения разницы в доходности 0.1% при волатильности 1.5%:
- Размер эффекта: 0.0667 (очень малый)
- Это требует очень больших выборок (>3000 наблюдений)
2. Альтернативные стратегии:
Размеры эффекта, которые можно обнаружить за разумное время:
- Разница 0.2%: размер эффекта 0.1333, нужно 444 дней (1.8 лет)
- Разница 0.3%: размер эффекта 0.2000, нужно 199 дней (0.8 лет)
- Разница 0.5%: размер эффекта 0.3333, нужно 73 дней (0.3 лет)
3. Рекомендации:
- Увеличить α до 0.1 может снизить требуемую выборку
- Рассмотреть односторонний тест если есть четкие ожидания
- Фокусироваться на больших размерах эффекта (>0.2% разница)
- Использовать комбинированные метрики (Sharpe ratio, максимальная просадка)
Этот код демонстрирует, как рассчитать необходимый размер выборки для достижения желаемой мощности статистического теста при различных размерах эффекта. Я использовал функцию ttest_power из библиотеки statsmodels.
Что мы можем узнать из этого кода?
- Во-первых, существует обратная зависимость между размером эффекта и необходимым размером выборки. Чем меньше разница, которую мы хотим обнаружить (меньший размер эффекта), тем больший размер выборки нам требуется;
- Во-вторых, для достижения более высокой мощности (вероятности обнаружить эффект, если он существует) требуется больший размер выборки. Например, для увеличения мощности с 80% до 95% может потребоваться значительное увеличение размера выборки;
- В-третьих, существует компромисс между уровнем значимости и мощностью. При фиксированном размере выборки увеличение уровня значимости (например, с 0.01 до 0.05) увеличивает мощность, но также увеличивает вероятность ложноположительных результатов.
Практическое применение этих расчетов в контексте оценки торговых стратегий очень важно. Например, если мы хотим обнаружить разницу в среднедневной доходности 0.1% при стандартном отклонении 1.5%, нам может потребоваться несколько лет данных для достижения достаточной мощности. Это подчеркивает важность долгосрочного тестирования стратегий и опасность принятия решений на основе краткосрочных результатов.
Применение доверительной вероятности и уровня значимости в машинном обучении
Оценка качества моделей и кросс-валидация
В контексте машинного обучения доверительная вероятность и уровень значимости играют важную роль в оценке качества моделей и выборе лучшей модели. Кросс-валидация, особенно k-fold cross-validation, является мощным инструментом для получения надежных оценок производительности модели.
Давайте рассмотрим, как можно использовать k-fold cross-validation для оценки и сравнения моделей регрессии для прогнозирования финансовых временных рядов.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.ensemble import RandomForestRegressor
from sklearn.svm import SVR
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from sklearn.model_selection import KFold, cross_val_score, cross_validate
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
# Настройки для графиков
plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['font.size'] = 12
# Загрузка данных
ticker = 'EEM' # iShares MSCI Emerging Markets ETF
start_date = '2022-07-01'
end_date = '2025-07-01'
print(f"Загружаем данные для {ticker}...")
data = yf.download(ticker, start=start_date, end=end_date, progress=False)
# Проверка структуры данных
print(f"Загружено {len(data)} наблюдений")
print(f"Столбцы данных: {list(data.columns)}")
# Создание признаков (лаги, технические индикаторы)
print("Создаем признаки...")
# Базовые доходности
data['Returns'] = data['Close'].pct_change()
data['LogReturns'] = np.log(data['Close'] / data['Close'].shift(1))
# Лаги доходностей
for i in range(1, 6):
data[f'Returns_lag_{i}'] = data['Returns'].shift(i)
data[f'LogReturns_lag_{i}'] = data['LogReturns'].shift(i)
# Технические индикаторы
for window in [5, 10, 20, 50]:
# Относительное отклонение от скользящего среднего
data[f'SMA_{window}'] = (data['Close'] / data['Close'].rolling(window=window).mean()) - 1
# Волатильность
data[f'Volatility_{window}'] = data['Returns'].rolling(window=window).std()
# RSI (Relative Strength Index)
def calculate_rsi(prices, window=14):
delta = prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
data['RSI'] = calculate_rsi(data['Close'])
# Bollinger Bands
for window in [10, 20]:
rolling_mean = data['Close'].rolling(window=window).mean()
rolling_std = data['Close'].rolling(window=window).std()
data[f'BB_upper_{window}'] = (data['Close'] - (rolling_mean + 2 * rolling_std)) / data['Close']
data[f'BB_lower_{window}'] = (data['Close'] - (rolling_mean - 2 * rolling_std)) / data['Close']
# Целевая переменная: доходность на следующий день
data['Target'] = data['Returns'].shift(-1)
# Удаление пропущенных значений
print(f"Удаляем пропущенные значения...")
data_clean = data.dropna()
print(f"Осталось {len(data_clean)} наблюдений после очистки")
# Отбор признаков
feature_columns = []
for col in data_clean.columns:
if any(keyword in col for keyword in ['lag', 'SMA', 'Volatility', 'RSI', 'BB']):
feature_columns.append(col)
print(f"Выбрано {len(feature_columns)} признаков: {feature_columns}")
# Проверка наличия признаков
if len(feature_columns) == 0:
print("Ошибка: не найдено подходящих признаков!")
exit()
X = data_clean[feature_columns]
y = data_clean['Target']
print(f"Размер матрицы признаков: {X.shape}")
print(f"Размер вектора целевой переменной: {y.shape}")
# Проверка на наличие бесконечных значений
X = X.replace([np.inf, -np.inf], np.nan).dropna()
y = y.loc[X.index]
print(f"После удаления inf значений: X={X.shape}, y={y.shape}")
if len(X) < 50: print("Предупреждение: очень мало данных для обучения!") # Стандартизация признаков scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Подготовка моделей для сравнения models = { 'Ridge': Ridge(alpha=1.0, random_state=42), 'RandomForest': RandomForestRegressor(n_estimators=100, max_depth=5, random_state=42), 'SVR': SVR(kernel='rbf', C=1.0, epsilon=0.1) } # Настройка k-fold cross-validation n_splits = 5 kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) # Метрики для оценки scoring = { 'MSE': 'neg_mean_squared_error', 'MAE': 'neg_mean_absolute_error', 'R2': 'r2' } print("Проводим кросс-валидацию...") # Проведение кросс-валидации для каждой модели results = {} for name, model in models.items(): print(f"Обучение модели: {name}...") cv_results = cross_validate(model, X_scaled, y, cv=kf, scoring=scoring, return_train_score=True) results[name] = { 'test_MSE': -cv_results['test_MSE'], 'test_MAE': -cv_results['test_MAE'], 'test_R2': cv_results['test_R2'], 'train_MSE': -cv_results['train_MSE'], 'train_MAE': -cv_results['train_MAE'], 'train_R2': cv_results['train_R2'] } # Расчет среднего и стандартного отклонения метрик summary_data = [] for name, result in results.items(): for metric, values in result.items(): mean_val = np.mean(values) std_val = np.std(values, ddof=1) # Несмещенная оценка стандартного отклонения # Расчет 95% доверительного интервала n = len(values) sem = std_val / np.sqrt(n) # Стандартная ошибка среднего t_critical = stats.t.ppf(0.975, df=n-1) # Критическое значение t для 95% CI ci_lower = mean_val - t_critical * sem ci_upper = mean_val + t_critical * sem summary_data.append({ 'Model': name, 'Metric': metric, 'Mean': mean_val, 'Std': std_val, 'CI_Lower': ci_lower, 'CI_Upper': ci_upper }) summary = pd.DataFrame(summary_data) # Вывод результатов print("\n" + "="*80) print("РЕЗУЛЬТАТЫ КРОСС-ВАЛИДАЦИИ:") print("="*80) for model in models.keys(): print(f"\n{model}:") for metric in ['test_MSE', 'test_MAE', 'test_R2']: model_metric = summary[(summary['Model'] == model) & (summary['Metric'] == metric)] if len(model_metric) > 0:
mean_val = model_metric['Mean'].values[0]
std_val = model_metric['Std'].values[0]
ci_lower = model_metric['CI_Lower'].values[0]
ci_upper = model_metric['CI_Upper'].values[0]
print(f" {metric:8}: {mean_val:8.6f} ± {std_val:8.6f} (95% CI: [{ci_lower:8.6f}, {ci_upper:8.6f}])")
# Визуализация результатов с доверительными интервалами
metrics_to_plot = ['test_MSE', 'test_MAE', 'test_R2']
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
for i, metric in enumerate(metrics_to_plot):
metric_data = summary[summary['Metric'] == metric].copy()
# Сортировка по среднему значению метрики
if metric in ['test_MSE', 'test_MAE']:
metric_data = metric_data.sort_values('Mean')
title_suffix = " (меньше - лучше)"
else:
metric_data = metric_data.sort_values('Mean', ascending=False)
title_suffix = " (больше - лучше)"
ax = axes[i]
# Построение баров с доверительными интервалами
colors = ['skyblue', 'lightcoral', 'lightgreen']
bars = ax.bar(
metric_data['Model'],
metric_data['Mean'],
yerr=[(metric_data['Mean'] - metric_data['CI_Lower']).values,
(metric_data['CI_Upper'] - metric_data['Mean']).values],
capsize=10,
color=colors[:len(metric_data)],
alpha=0.8,
edgecolor='black'
)
# Добавление значений на столбцы
for bar, mean_val in zip(bars, metric_data['Mean']):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + max(metric_data['CI_Upper'] - metric_data['Mean']) * 0.1,
f'{mean_val:.4f}', ha='center', va='bottom', fontweight='bold')
ax.set_title(f"{metric.replace('test_', '')}{title_suffix}")
ax.set_xlabel('Модель')
if metric == 'test_MSE':
ax.set_ylabel('Mean Squared Error')
elif metric == 'test_MAE':
ax.set_ylabel('Mean Absolute Error')
else:
ax.set_ylabel('R² Score')
ax.grid(True, alpha=0.3, axis='y')
ax.tick_params(axis='x', rotation=0)
plt.tight_layout()
plt.show()
# Дополнительная визуализация: сравнение обучающей и тестовой ошибки
fig, ax = plt.subplots(figsize=(12, 6))
model_names = list(models.keys())
x = np.arange(len(model_names))
width = 0.35
train_mse = [np.mean(results[model]['train_MSE']) for model in model_names]
test_mse = [np.mean(results[model]['test_MSE']) for model in model_names]
bars1 = ax.bar(x - width/2, train_mse, width, label='Обучающая выборка', alpha=0.8, color='lightblue')
bars2 = ax.bar(x + width/2, test_mse, width, label='Тестовая выборка', alpha=0.8, color='salmon')
ax.set_xlabel('Модель')
ax.set_ylabel('Mean Squared Error')
ax.set_title('Сравнение ошибок на обучающей и тестовой выборках')
ax.set_xticks(x)
ax.set_xticklabels(model_names)
ax.legend()
ax.grid(True, alpha=0.3, axis='y')
# Добавление значений на столбцы
for bars in [bars1, bars2]:
for bar in bars:
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + max(test_mse) * 0.01,
f'{height:.5f}', ha='center', va='bottom', fontsize=9)
plt.tight_layout()
plt.show()
# Статистический тест для сравнения моделей
print("\n" + "="*80)
print("СТАТИСТИЧЕСКОЕ СРАВНЕНИЕ МОДЕЛЕЙ:")
print("="*80)
models_list = list(models.keys())
metric = 'test_MSE'
print(f"\nПарные t-тесты для сравнения {metric} (p-значения):")
comparison_matrix = np.ones((len(models_list), len(models_list)))
p_value_matrix = np.ones((len(models_list), len(models_list)))
for i, model1 in enumerate(models_list):
for j, model2 in enumerate(models_list):
if i != j:
# Парный t-тест для сравнения MSE на каждом фолде
model1_errors = results[model1][metric]
model2_errors = results[model2][metric]
t_stat, p_value = stats.ttest_rel(model1_errors, model2_errors)
p_value_matrix[i, j] = p_value
# Проверяем, значимо ли model1 лучше model2
if p_value < 0.05 and np.mean(model1_errors) < np.mean(model2_errors):
comparison_matrix[i, j] = -1 # model1 значимо лучше
elif p_value < 0.05 and np.mean(model1_errors) > np.mean(model2_errors):
comparison_matrix[i, j] = 1 # model2 значимо лучше
else:
comparison_matrix[i, j] = 0 # нет значимой разности
# Создание DataFrame для удобного вывода
comparison_df = pd.DataFrame(p_value_matrix,
index=models_list,
columns=models_list)
print(comparison_df.round(4))
print(f"\nИнтерпретация (α = 0.05):")
for i, model1 in enumerate(models_list):
for j, model2 in enumerate(models_list):
if i < j: # Избегаем дублирования
p_val = p_value_matrix[i, j]
mean1 = np.mean(results[model1][metric])
mean2 = np.mean(results[model2][metric])
if p_val < 0.05:
better_model = model1 if mean1 < mean2 else model2
print(f"• {better_model} значимо лучше {model2 if better_model == model1 else model1} (p = {p_val:.4f})")
else:
print(f"• {model1} и {model2}: нет значимой разности (p = {p_val:.4f})")
# Рейтинг моделей
print(f"\nРЕЙТИНГ МОДЕЛЕЙ (по {metric}):")
model_ranking = []
for model in models_list:
mean_error = np.mean(results[model][metric])
std_error = np.std(results[model][metric], ddof=1)
model_ranking.append((model, mean_error, std_error))
model_ranking.sort(key=lambda x: x[1]) # Сортировка по средней ошибке (меньше - лучше)
for rank, (model, mean_err, std_err) in enumerate(model_ranking, 1):
print(f"{rank}. {model:12}: {mean_err:.6f} ± {std_err:.6f}")
print(f"\nВывод: Лучшая модель - {model_ranking[0][0]}")
агружаем данные для EEM...
Загружено 751 наблюдений
Столбцы данных: [('Close', 'EEM'), ('High', 'EEM'), ('Low', 'EEM'), ('Open', 'EEM'), ('Volume', 'EEM')]
Создаем признаки...
Удаляем пропущенные значения...
Осталось 700 наблюдений после очистки
Выбрано 1 признаков: [('RSI', '')]
Размер матрицы признаков: (700, 1)
Размер вектора целевой переменной: (700,)
После удаления inf значений: X=(700, 1), y=(700,)
Проводим кросс-валидацию...
Обучение модели: Ridge...
Обучение модели: RandomForest...
Обучение модели: SVR...
================================================================================
РЕЗУЛЬТАТЫ КРОСС-ВАЛИДАЦИИ:
================================================================================
Ridge:
test_MSE: 0.000125 ± 0.000022 (95% CI: [0.000097, 0.000152])
test_MAE: 0.008198 ± 0.000547 (95% CI: [0.007519, 0.008877])
test_R2 : -0.012977 ± 0.012073 (95% CI: [-0.027967, 0.002014])
RandomForest:
test_MSE: 0.000128 ± 0.000022 (95% CI: [0.000101, 0.000155])
test_MAE: 0.008341 ± 0.000537 (95% CI: [0.007674, 0.009009])
test_R2 : -0.042178 ± 0.014192 (95% CI: [-0.059799, -0.024556])
SVR:
test_MSE: 0.000207 ± 0.000086 (95% CI: [0.000101, 0.000314])
test_MAE: 0.011352 ± 0.002769 (95% CI: [0.007914, 0.014789])
test_R2 : -0.642811 ± 0.429674 (95% CI: [-1.176321, -0.109301])

Рис. 11: Сравнительный анализ производительности алгоритмов машинного обучения для прогнозирования дневной доходности ETF EEM по трем ключевым метрикам качества. Представлены результаты 5-кратной кросс-валидации с 95% доверительными интервалами для среднеквадратичной ошибки (MSE), средней абсолютной ошибки (MAE) и коэффициента детерминации (R²). Модели обучались на наборе технических индикаторов, включающем лаги доходностей, скользящие средние, показатели волатильности, RSI и полосы Боллинджера за период 2022-2025 гг. Усы ошибок отражают статистическую неопределенность оценок качества моделей на основе t-распределения Стьюдента

Рис. 12: Диагностика переобучения алгоритмов машинного обучения через сопоставление среднеквадратичной ошибки на обучающей и валидационной выборках. График демонстрирует способность каждой модели к генерализации путем сравнения производительности внутри выборки (обучающие данные) и вне выборки (тестовые данные). Близость значений ошибок указывает на отсутствие переобучения, тогда как существенное превышение тестовой ошибки над обучающей сигнализирует о недостаточной способности модели к обобщению на новых данных. Результаты получены методом k-кратной кросс-валидации для обеспечения робастности оценок
================================================================================
СТАТИСТИЧЕСКОЕ СРАВНЕНИЕ МОДЕЛЕЙ:
================================================================================
Парные t-тесты для сравнения test_MSE (p-значения):
Ridge RandomForest SVR
Ridge 1.0000 0.0084 0.0500
RandomForest 0.0084 1.0000 0.0583
SVR 0.0500 0.0583 1.0000
Интерпретация (α = 0.05):
• Ridge значимо лучше RandomForest (p = 0.0084)
• Ridge и SVR: нет значимой разности (p = 0.0500)
• RandomForest и SVR: нет значимой разности (p = 0.0583)
РЕЙТИНГ МОДЕЛЕЙ (по test_MSE):
1. Ridge : 0.000125 ± 0.000022
2. RandomForest: 0.000128 ± 0.000022
3. SVR : 0.000207 ± 0.000086
Вывод: Лучшая модель - Ridge
Этот код демонстрирует применение k-fold cross-validation для оценки и сравнения нескольких моделей регрессии для прогнозирования доходности акций. Я использовал три модели: Ridge регрессия, случайный лес и метод опорных векторов (SVR).
Что делает этот код?
- Загружает исторические данные и создает различные признаки, включая лаги доходностей и технические индикаторы;
- Проводит k-fold cross-validation для каждой модели, вычисляя несколько метрик: MSE, MAE и R²;
- Рассчитывает среднее значение, стандартное отклонение и 95% доверительный интервал для каждой метрики;
- Визуализирует результаты с доверительными интервалами;
- Проводит статистические тесты для сравнения моделей.
Доверительные интервалы здесь играют ключевую роль. Они позволяют оценить неопределенность в оценке производительности модели и определить, являются ли различия между моделями статистически значимыми. Если доверительные интервалы для двух моделей перекрываются, мы не можем с уверенностью утверждать, что одна модель лучше другой.
Парный t-тест также используется для формального сравнения моделей. Он учитывает зависимость между результатами на одних и тех же фолдах, что повышает мощность теста. Если p-значение меньше выбранного уровня значимости (например, 0.05), мы можем заключить, что различие между моделями статистически значимо.
Важно отметить, что статистическая значимость не всегда означает практическую значимость. Даже если различие между двумя моделями статистически значимо, оно может быть слишком мало, чтобы оправдать выбор более сложной модели или дополнительные затраты на ее реализацию.
Доверительные интервалы для прогнозов
При использовании статистических моделей или моделей машинного обучения для прогнозирования важно не только получить точечный прогноз, но и оценить неопределенность этого прогноза. Доверительные интервалы для прогнозов могут предоставить ценную информацию о надежности предсказаний.
Давайте рассмотрим, как можно построить доверительные интервалы для прогнозов с использованием байесовского подхода и библиотеки PyMC.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
import pymc as pm
import arviz as az
import seaborn as sns
from sklearn.preprocessing import StandardScaler
# Настройки для графиков
plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['font.size'] = 12
# Загрузка данных
ticker = 'EEM' # iShares MSCI Emerging Markets ETF
start_date = '2022-07-01'
end_date = '2025-07-01'
data = yf.download(ticker, start=start_date, end=end_date)
# Проверка на наличие Multiindex
if isinstance(data.index, pd.MultiIndex):
data = data.reset_index(level=0)
# Создание признаков
data['Returns'] = data['Close'].pct_change()
# Скользящие средние
for window in [5, 10, 20]:
data[f'SMA_{window}'] = data['Close'].rolling(window=window).mean() / data['Close'] - 1
# Целевая переменная: доходность на следующий день
data['Target'] = data['Returns'].shift(-1)
# Удаление пропущенных значений
data = data.dropna()
# Выбор признаков
feature_columns = [f'SMA_{window}' for window in [5, 10, 20]]
X = data[feature_columns].values
y = data['Target'].values
# Стандартизация признаков
scaler_X = StandardScaler()
X_scaled = scaler_X.fit_transform(X)
# Разделение на обучающую и тестовую выборки
train_size = int(0.8 * len(data))
X_train, X_test = X_scaled[:train_size], X_scaled[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
print(f"Размер обучающей выборки: {X_train.shape}")
print(f"Размер тестовой выборки: {X_test.shape}")
# Построение байесовской модели линейной регрессии с PyMC
with pm.Model() as linear_model:
# Приоры для коэффициентов
alpha = pm.Normal('alpha', mu=0, sigma=1)
betas = pm.Normal('betas', mu=0, sigma=1, shape=X_train.shape[1])
# Приор для шума
sigma = pm.HalfNormal('sigma', sigma=1)
# Ожидаемое значение
mu = alpha + pm.math.dot(X_train, betas)
# Правдоподобие
likelihood = pm.Normal('likelihood', mu=mu, sigma=sigma, observed=y_train)
# Отбор проб с использованием NUTS
trace = pm.sample(2000, tune=1000, return_inferencedata=True)
# Анализ результатов
summary = az.summary(trace, var_names=['alpha', 'betas', 'sigma'])
print("Сводка апостериорного распределения:")
print(summary)
# Визуализация апостериорного распределения
az.plot_trace(trace, var_names=['alpha', 'betas', 'sigma'])
plt.tight_layout()
plt.show()
# Прогнозирование с доверительными интервалами
alpha_samples = trace.posterior['alpha'].values.flatten() # Выравниваем по chain и draw
beta_samples = trace.posterior['betas'].values.reshape(-1, X_train.shape[1]) # Правильная форма для бет
sigma_samples = trace.posterior['sigma'].values.flatten()
print(f"Форма alpha_samples: {alpha_samples.shape}")
print(f"Форма beta_samples: {beta_samples.shape}")
print(f"Форма sigma_samples: {sigma_samples.shape}")
# Функция для прогнозирования с использованием одного набора параметров
def predict_one_sample(alpha, betas, X):
return alpha + np.dot(X, betas)
# Генерация прогнозов для каждого набора параметров из апостериорного распределения
n_samples = len(alpha_samples)
predictions = np.zeros((n_samples, len(X_test)))
for i in range(n_samples):
alpha_sample = alpha_samples[i]
betas_sample = beta_samples[i]
predictions[i] = predict_one_sample(alpha_sample, betas_sample, X_test)
# Расчет среднего прогноза и доверительных интервалов
mean_prediction = np.mean(predictions, axis=0)
lower_bound = np.percentile(predictions, 2.5, axis=0)
upper_bound = np.percentile(predictions, 97.5, axis=0)
# Визуализация прогнозов с доверительными интервалами
plt.figure(figsize=(14, 8))
plt.plot(mean_prediction, color='blue', linewidth=2, label='Средний прогноз')
plt.fill_between(range(len(mean_prediction)), lower_bound, upper_bound,
color='lightblue', alpha=0.5, label='95% доверительный интервал')
plt.scatter(range(len(y_test)), y_test, color='red', alpha=0.7, s=20, label='Фактические значения')
plt.xlabel('Индекс наблюдения (тестовая выборка)')
plt.ylabel('Доходность')
plt.title('Прогноз доходности EEM ETF с байесовскими доверительными интервалами')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Оценка качества прогнозов
mse = np.mean((mean_prediction - y_test) ** 2)
mae = np.mean(np.abs(mean_prediction - y_test))
rmse = np.sqrt(mse)
coverage = np.mean((y_test >= lower_bound) & (y_test <= upper_bound))
print(f"\nМетрики качества модели:")
print(f"MSE: {mse:.6f}")
print(f"MAE: {mae:.6f}")
print(f"RMSE: {rmse:.6f}")
print(f"Покрытие 95% доверительным интервалом: {coverage:.2%}")
# Анализ ширины доверительных интервалов
interval_width = upper_bound - lower_bound
mean_interval_width = np.mean(interval_width)
print(f"Средняя ширина доверительного интервала: {mean_interval_width:.6f}")
# Дополнительная диагностика модели
print(f"\nДиагностика модели:")
print(f"Количество наблюдений в обучающей выборке: {len(y_train)}")
print(f"Количество наблюдений в тестовой выборке: {len(y_test)}")
print(f"Количество признаков: {X_train.shape[1]}")
# Анализ остатков
residuals = y_test - mean_prediction
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.scatter(mean_prediction, residuals, alpha=0.6)
plt.xlabel('Прогнозируемые значения')
plt.ylabel('Остатки')
plt.title('График остатков')
plt.axhline(y=0, color='red', linestyle='--')
plt.grid(True, alpha=0.3)
plt.subplot(1, 2, 2)
plt.hist(residuals, bins=20, alpha=0.7, density=True)
plt.xlabel('Остатки')
plt.ylabel('Плотность')
plt.title('Распределение остатков')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Важность признаков (анализ коэффициентов)
beta_means = np.mean(beta_samples, axis=0)
beta_stds = np.std(beta_samples, axis=0)
print(f"\nАнализ коэффициентов модели:")
for i, feature in enumerate(feature_columns):
print(f"{feature}: {beta_means[i]:.6f} ± {beta_stds[i]:.6f}")
Размер обучающей выборки: (584, 3)
Размер тестовой выборки: (147, 3)
Progress Draws Divergences Step size Grad evals Sampling Speed Elapsed Remaining
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
━━━━━━━━━━━━━━━━━━━━━━━━━ 3000 0 0.28 15 666.39 draws/s 0:00:04 0:00:00
━━━━━━━━━━━━━━━━━━━━━━━━━ 3000 0 0.32 7 350.82 draws/s 0:00:08 0:00:00
Сводка апостериорного распределения:
mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
alpha 0.000 0.000 -0.001 0.001 0.0 0.0 3420.0 2446.0 1.0
betas[0] 0.000 0.001 -0.002 0.002 0.0 0.0 2348.0 2137.0 1.0
betas[1] -0.001 0.002 -0.003 0.002 0.0 0.0 2137.0 2475.0 1.0
betas[2] 0.001 0.001 -0.001 0.002 0.0 0.0 2488.0 2804.0 1.0
sigma 0.011 0.000 0.010 0.011 0.0 0.0 2926.0 2717.0 1.0

Рис. 13: Трассировочные графики и апостериорные распределения параметров байесовской линейной регрессии. Левая панель показывает эволюцию параметров по итерациям MCMC для оценки сходимости алгоритма выборки. Правая панель отображает гистограммы апостериорных распределений параметров: α (свободный член), β₁₋₃ (коэффициенты при скользящих средних SMA_5, SMA_10, SMA_20) и σ (стандартное отклонение шума). Отсутствие трендов в трассах и нормальный вид распределений свидетельствуют о корректной работе алгоритма NUTS

Рис. 14: Байесовские прогнозы доходности ETF EEM с 95% доверительными интервалами на тестовой выборке. Синяя линия представляет среднее апостериорное предсказание, заливка голубым — 95% байесовский доверительный интервал, красные точки — фактические значения доходности. Ширина доверительных интервалов отражает неопределенность модели в каждой временной точке. Покрытие фактических значений доверительными интервалами служит индикатором калибровки модели
Метрики качества модели:
MSE: 0.000161
MAE: 0.008390
RMSE: 0.012685
Покрытие 95% доверительным интервалом: 14.97%
Средняя ширина доверительного интервала: 0.003547
Диагностика модели:
Количество наблюдений в обучающей выборке: 584
Количество наблюдений в тестовой выборке: 147
Количество признаков: 3
Анализ коэффициентов модели:
SMA_5: 0.000343 ± 0.001046
SMA_10: -0.000683 ± 0.001565
SMA_20: 0.000515 ± 0.001014

Рис. 15: Диагностика остатков байесовской модели линейной регрессии. Левый график показывает зависимости остатков от прогнозируемых значений, где горизонтальное расположение точек относительно нулевой линии (красная пунктирная) указывает на отсутствие систематических ошибок, а гомоскедастичность свидетельствует о корректности предположений модели. Правый график: гистограмма распределения остатков, симметричная форма которой, близкая к нормальной, подтверждает корректность предположения о гауссовском характере шума в модели линейной регрессии
Этот код реализует байесовскую линейную регрессию для прогнозирования доходности ETF развивающихся рынков (EEM) на основе технических индикаторов — скользящих средних разных периодов (5, 10, 20 дней) и делает это по следующим этапам:
- Загружает исторические данные по ETF EEM с Yahoo Finance;
- Создает технические индикаторы (отклонения от скользящих средних);
- Строит байесовскую модель для предсказания доходности следующего дня;
- Генерирует прогнозы с доверительными интервалами, отражающими неопределенность;
- Проводит диагностику качества модели.
В чем практическая польза такого подхода:
- Для трейдеров и инвесторов: Получение прогнозов доходности с количественной оценкой риска (доверительные интервалы показывают, насколько модель уверена в предсказаниях);
- Управление рисками: Байесовский подход позволяет явно моделировать неопределенность, что крайне важно для финансовых решений;
- Техническая надежность: В отличие от классической регрессии, которая дает точечные прогнозы, байесовская модель показывает полный спектр возможных исходов;
- Основа для торговых стратегий: Результаты можно использовать для построения алгоритмических торговых систем, где решения принимаются на основе вероятностных прогнозов;
- Научная обоснованность: Код демонстрирует современный подход к финансовому моделированию с полной статистической диагностикой
Анализ ширины доверительных интервалов и надежности прогнозов
Полученные результаты демонстрируют важный аспект байесовского моделирования — мы получаем не только точечные прогнозы, но и оценку неопределенности в виде доверительных интервалов. Ширина этих интервалов варьируется в зависимости от характеристик данных и уверенности модели в своих предсказаниях.
Анализируя ширину доверительных интервалов, можно заметить, что они обычно шире в периоды высокой волатильности рынка. Это логично, поскольку в такие периоды предсказуемость будущих доходностей снижается. Покрытие доверительным интервалом (процент фактических значений, попадающих в интервал) является ключевой метрикой для оценки качества наших неопределенностей. Идеальное 95% доверительное покрытие должно составлять примерно 95%.
В контексте финансовых прогнозов доверительные интервалы могут служить основой для оценки риска. Например, нижняя граница 95% интервала может использоваться как оценка Value-at-Risk (VaR) с 95% уровнем доверия. Это предоставляет трейдерам и риск-менеджерам ценную информацию о возможных потерях.
Интерпретация статистических результатов
Баланс между статистической и практической значимостью
В ходе нашего обсуждения мы неоднократно касались важной дихотомии: статистическая значимость не всегда равна практической значимости. Это особенно актуально в контексте анализа финансовых данных и разработки торговых стратегий.
Статистическая значимость говорит нам о том, что наблюдаемый эффект, скорее всего, не является результатом случайной вариации. Однако она ничего не говорит о величине этого эффекта и его практической применимости. Стратегия может показывать статистически значимое превосходство над бенчмарком, но если разница в доходности составляет всего 0.05% в год, это вряд ли оправдает затраты на ее реализацию, особенно с учетом транзакционных издержек, проскальзывания и других факторов.
С другой стороны, отсутствие статистической значимости не всегда означает отсутствие эффекта. Это может быть результатом недостаточной мощности теста из-за малого размера выборки или высокой вариабельности данных. В таких случаях полезно рассматривать доверительные интервалы, которые дают представление о возможном диапазоне значений параметра.
При принятии решений на основе статистического анализа необходимо учитывать оба аспекта: статистическую значимость (p-значение) и практическую значимость (величину эффекта). Доверительные интервалы помогают объединить эти два аспекта, предоставляя информацию как о статистической значимости (включает ли интервал нулевое значение), так и о практической значимости (насколько широк интервал и какие значения он включает).
Распространенные ошибки и заблуждения
За годы работы с статистическими методами в области анализа данных я заметил несколько распространенных ошибок и заблуждений:
- Неправильная интерпретация p-значения. Многие ошибочно считают, что p-значение — это вероятность того, что нулевая гипотеза верна. На самом деле это вероятность получить наблюдаемые или более экстремальные данные при условии, что нулевая гипотеза верна;
- Фокус только на статистической значимости. Часто исследователи концентрируются исключительно на p-значениях, игнорируя размер эффекта и его практическую значимость;
- Игнорирование множественного тестирования. При проведении множества тестов вероятность получить хотя бы один ложноположительный результат возрастает. Это особенно актуально при бэктестинге торговых стратегий, когда может быть протестировано множество комбинаций параметров;
- Неправильное понимание доверительных интервалов. Часто 95% доверительный интервал интерпретируется как «вероятность 95%, что истинное значение находится в этом интервале». Правильная интерпретация: «если мы повторим процесс построения интервала для многих выборок, то примерно 95% этих интервалов будут содержать истинное значение»;
- Overfitting (переобучение). Оптимизация параметров модели или стратегии на исторических данных может привести к отличным результатам на обучающей выборке, но плохим результатам на новых данных. Кросс-валидация и оценка на отложенной выборке помогают выявить эту проблему;
- Игнорирование предпосылок статистических тестов. Многие параметрические тесты (например, t-тест) имеют предпосылки о распределении данных, которые не всегда оказываются верны на практике;
- Путаница между корреляцией и причинно-следственной связью. Статистически значимая корреляция не обязательно означает причинно-следственную связь.
Заключение
Доверительная вероятность и уровень значимости — это фундаментальные концепции статистического вывода, которые лежат в основе многих методов анализа данных и машинного обучения. Правильное понимание и применение этих концепций критически важно для принятия обоснованных решений в условиях неопределенности.
В контексте финансового анализа и разработки торговых стратегий эти концепции помогают отличить настоящие закономерности от случайного шума, оценить надежность наших моделей и количественно измерить неопределенность прогнозов.
Ключевые моменты, которые стоит запомнить:
- Доверительная вероятность (confidence level) и уровень значимости (significance level или α) связаны соотношением: доверительная вероятность = 1 — α;
- p-значение — это не вероятность того, что нулевая гипотеза верна, а вероятность получить наблюдаемые или более экстремальные данные при условии, что нулевая гипотеза верна;
- Доверительные интервалы предоставляют информацию как о статистической значимости, так и о практической значимости результатов;
- При проведении множества тестов необходимо применять методы коррекции для множественного тестирования, чтобы контролировать вероятность ложноположительных результатов;
- Мощность статистического теста зависит от размера выборки, величины эффекта, уровня значимости и вариабельности данных;
- В машинном обучении кросс-валидация помогает получить более надежные оценки производительности моделей и уменьшить риск переобучения;
- Байесовский подход позволяет естественным образом учитывать неопределенность и строить доверительные интервалы для прогнозов;
- При принятии решений необходимо учитывать как статистическую, так и практическую значимость результатов.
В моей практике эти концепции неоднократно доказывали свою ценность, помогая избегать ошибочных выводов и принимать более обоснованные решения. Однако важно помнить, что статистика — это инструмент, а не оракул. Она помогает структурировать неопределенность и количественно оценить риски, но не устраняет необходимость в экспертной оценке и здравом смысле.
Надеюсь, эта статья помогла вам лучше понять концепции доверительной вероятности и уровня значимости, а также их практическое применение в анализе данных и машинном обучении. Применяя эти концепции с пониманием их ограничений и нюансов, вы сможете повысить качество своих моделей и обоснованность принимаемых решений.