Социология парламентарни избори 2026

Този бележник анализира разликите електоралните нагласи отчетени от различните социологически агенции и реалните изборни резултати.
sociology
Author

Григор Димитров

Published

April 23, 2026

Анализ на точността на социологическите проучвания

Този бележник анализира разликите в електоралните нагласи, отчетени от различните социологически агенции, и реалните изборни резултати.

  • Зареждане на данни: Данните се хостват публично в GitHub с цел да бъдат достъпни и проверими. Ако не сте съгласни с части от данните или анализа, можете да го модифицирате.
  • Технически параметри: Преглед на данните, публикувани към проучванията (период, метод, извадка).
  • Резултатна матрица: Сравнителна таблица с всички резултати (хоризонтално — партиите, вертикално — проучванията).
  • Матрица на отклоненията: Изчисляване на разликите между нагласите в проучванията и крайните изборни резултати.
  • Ранкинг по средна абсолютна грешка (MAE): Класиране на проучванията според тяхното средно линейно отклонение.
  • Ранкинг по средноквадратична грешка (RMSE): Класиране според корен квадратен от средната стойност на квадратите на разликите (методът наказва по-тежко големите отклонения).
  • Регресионен анализ (OLS): Изследване на влиянието на фактори като оставащо време до изборите, методология (хибридна или офлайн) и големина на извадката върху точността на прогнозите.

Забележки: Анализът е мотивиран от любопитство и има за цел да постави основи за по-сериозна работа по темата. Сегашният му статус е на ниво „чернова“. Данните не трябва да се използват за вземане на решения, преди да бъдат вложени допълнителни усилия в сравняването на входящите данни с резултатите от изследванията и валидиране на качеството на кода. В допълнение, има няколко теста, препоръчителни при валидацията на регресионния анализ, които не са направени. Чувствайте се свободни да копирате, възпроизвеждате и модифицирате кода.

Последващи интересни анализи биха били:

  • Съставяне на база данни с всички социологически проучвания от последните 8 избора.
  • Използване на други видове индикатори, които измерват процентното отклонение (например MAPE или wMAPE).
  • Анализиране на данните с MDS (Multi-dimensional Scaling) или подобни методи, за да се проверят съмненията за връзка между конкретна агенция (или поръчител) и системно надценяване или подценяване на определени партии.
  • Добавяне на допълнителни атрибути в регресионния анализ — например източник на извадката или подизпълнител на проекта.
Show the code
import pandas as pd
import requests
import json
import numpy as np

# 1. Use the RAW URL from your gist (Note the 'gist.githubusercontent' domain)
url = "https://gist.githubusercontent.com/gregordimi/e0d3fe30e4f898c84ad25c38b39f6e44/raw/311a84fb7ce48c8bc47d61b359405d602b2aabd6/results2026.json"

# 2. Fetch the data using requests
response = requests.get(url)
data = response.json()

# 3. Your existing processing logic
survey_list = []
for s in data['surveys']:
    entry = s['results'].copy()
    entry['Agency'] = s['agency']
    entry['Date'] = s['date_iso']
    entry['Label'] = f"{s['agency']} ({s['date_label']})"

    # Store metadata separately if you want to use it for the cards
    entry['metadata'] = s.get('metadata', {})

    survey_list.append(entry)

df = pd.DataFrame(survey_list).set_index('Label')
actual = pd.Series(data['actual'], name='РЕАЛНИ РЕЗУЛТАТИ')

print(f"Данните за {len(df)} проучвания са заредени успешно")
print(url)
# df.head()
Данните за 20 проучвания са заредени успешно
https://gist.githubusercontent.com/gregordimi/e0d3fe30e4f898c84ad25c38b39f6e44/raw/311a84fb7ce48c8bc47d61b359405d602b2aabd6/results2026.json

Паспорти на публичните социологически изследвания за парламентарни избори 2026

Техническите данни за изследванията

Show the code
from IPython.display import HTML, display

def render_facet_row(data):
    all_cards = ""

    for s in data['surveys']:
        m = s.get('metadata', {})
        o = m.get('other_metadata', {})

        # Strip-down card template
        card_html = f"""
        <div style="flex: 1; min-width: 220px; max-width: 300px; border: 1px solid #888;
                    border-radius: 8px; padding: 12px; background: #fff; font-family: sans-serif;
                    box-shadow: 2px 2px 5px rgba(0,0,0,0.05); font-size: 12px;">
            <div style="color: #111; font-weight: bold; border-bottom: 1px solid #eee;
                        margin-bottom: 8px; padding-bottom: 4px; font-size: 13px;">
                {s['agency']} ({s['date_label']})
            </div>
            <div style="color: #777; margin-bottom: 4px;"><b>Период:</b> {m.get('date', 'N/A')}</div>
            <div style="color: #777; margin-bottom: 4px;"><b>Поръчител:</b> {m.get('purchaser', 'N/A')}</div>
            <div style="color: #777; margin-bottom: 4px;"><b>Извадка:</b> {m.get('sample_size', 'N/A')}</div>
            <div style="color: #777; margin-bottom: 4px;"><b>Метод:</b> {m.get('methodology', 'N/A')}</div>
            <div style="color: #777; font-size: 11px; margin-top: 8px; font-style: italic;">
                Грешка: {o.get('margin_of_error', 'N/A')}
            </div>
        </div>
        """
        all_cards += card_html

    # Display wrapper with Flexbox
    display(HTML(f"""
    <div style="display: flex; flex-wrap: wrap; gap: 15px; background: #f4f4f4; padding: 20px; border-radius: 12px;">
        {all_cards}
    </div>
    """))

# Execute to render all 19 cards
render_facet_row(data)
Маркет Линкс (16/02)
Период: 07 - 13 февруари 2026 г.
Поръчител: бТВ и Маркет Линкс
Извадка: 1019 лица над 18 г. в страната
Метод: Пряко-лично интервю и онлайн анкета
Грешка: Не е посочена
Мяра (17/02)
Период: 9-15 февруари 2026 г.
Поръчител: Независима ежемесечна изследователска програма на социологическата агенция „Мяра“
Извадка: 812 пълнолетни български граждани
Метод: “Лице в лице” с таблети
Грешка: Не е посочена
Тренд (23/02)
Период: 12-18 февруари 2026 г.
Поръчител: 24 часа
Извадка: 1002 души
Метод: Стандартизирано интервю „лице в лице“
Грешка: ± 3,1%
Център за анализи и маркетинг (26/02)
Период: 17 - 24 февруари 2026 г.
Поръчител: Център за анализи и маркетинг
Извадка: 1010 души
Метод: Пряко стандартизирано интервю по домовете
Грешка: ± 3,1%
Алфа Рисърч (05/03)
Период: 23 февруари – 2 март 2026 г.
Поръчител: Реализирано със собствени средства
Извадка: 1000 души
Метод: Пряко стандартизирано интервю по домовете с таблети
Грешка: Не е посочена
Галъп (09/03)
Период: 10 - 28 февруари 2026 г.
Поръчител: Независима изследователска програма (собствени средства)
Извадка: 800 души
Метод: Стандартизирано персонално интервю тип face-to-face с таблети (TAPI)
Грешка: ±3,5%
Сова Харис (16/03)
Период: 7 - 12 март 2026 г.
Поръчител: Вестник „Труд”
Извадка: 1000 души
Метод: Стандартизирано интервю „лице в лице” в дома на респондента
Грешка: +/- 3,5%
Маркет Линкс (18/03)
Период: 7 - 15 март 2026 г.
Поръчител: бТВ и „Маркет линкс”
Извадка: 1006 души
Метод: Пряко-лично интервю и онлайн анкета
Грешка: Не е посочена
Мяра (20/03)
Период: 7 - 16 март 2026 г.
Поръчител: Редовна изследователска програма на „Мяра”
Извадка: 809 души
Метод: “Лице в лице” с таблети
Грешка: +/- 3,5%
Тренд (23/03)
Период: 13-19 март 2026 г.
Поръчител: “24 часа”
Извадка: 1001 души
Метод: Пряко полустандартизирано интервю „лице в лице” с таблет
Грешка: +/- 3,1%
Маркет Линкс (23/03)
Период: 17 - 21 март 2026 г.
Поръчител: бТВ и „Маркет линкс”
Извадка: 1008 души
Метод: Пряко-лично интервю и онлайн анкета
Грешка: Не е посочена
Алфа Рисърч (23/03)
Период: 12-20 март 2026 г.
Поръчител: бТВ
Извадка: 1000 души
Метод: Пряко стандартизирано интервю по домовете с таблети
Грешка: Не е посочена
Алфа Рисърч (29/03)
Период: 19-26 март 2026 г.
Поръчител: БНР
Извадка: 1000 души
Метод: Пряко стандартизирано интервю по домовете с таблети
Грешка: Не е посочена
Галъп (03/04)
Период: 20 - 30 март 2026 г.
Поръчител: Независима изследователска програма (собствени средства)
Извадка: 820 души
Метод: Стандартизирано персонално интервю тип face-to-face с таблети (TAPI)
Грешка: ±3,5%
Сова Харис (09/04)
Период: 2 - 6 април 2026 г.
Поръчител: Вестник „Труд”
Извадка: 800 души
Метод: Стандартизирано интервю „лице в лице” в дома на респондента
Грешка: +/- 3,5%
Маркет Линкс (16/04)
Период: 7-14 април 2026 г.
Поръчител: bTV и Маркет Линкс
Извадка: 1003 души
Метод: Пряко-лично интервю и онлайн анкета
Грешка: Липсва информация
Алфа Рисърч (17/04)
Период: 13 - 15 април 2026 г.
Поръчител: Реализирано със собствени средства
Извадка: 1000 души
Метод: Пряко стандартизирано интервю по домовете с таблети
Грешка: Не е посочена
Тренд (17/04)
Период: 13 - 16 април 2026 г.
Поръчител: “24 часа”
Извадка: 1004 души
Метод: Пряко стандартизирано интервю „лице в лице” с таблет
Грешка: +/- 3,1%
Галъп (17/04)
Период: 08 - 16 април 2026 г.
Поръчител: Реализирано със собствени средства
Извадка: 803 души
Метод: Стандартизирано персонално интервю тип face-to-face
Грешка: +/- 3,3%
Мяра (17/04)
Период: 4 - 13 април 2026 г.
Поръчител: БНР
Извадка: 1002 души
Метод: Лице в лице с таблети
Грешка: +/- 3,1%

Данните

Тази графика показва всички публикувани данни на горните проучвания които са анализирани в това мета изследване.

Show the code
import seaborn as sns
import matplotlib.pyplot as plt

# 1. Филтрираме колоните (същата логика)
cols = [c for c in df.columns if c not in ['Agency', 'Date', 'metadata', 'Не съм решил']]

# 2. Сортираме партиите по техния реален резултат
sorted_parties = actual[cols].sort_values(ascending=False).index

# 3. Вземаме СУРОВИТЕ ДАННИ (df вместо residuals)
raw_data_matrix = df[sorted_parties]

# 4. Визуализация
plt.figure(figsize=(10, 10))

# Използваме 'YlGnBu' (Yellow-Green-Blue), защото това са абсолютни стойности (0-100),
# а не отклонения около нулата.
sns.heatmap(raw_data_matrix,
            annot=True,
            fmt='.1f',
            cmap='YlGnBu',
            linewidths=.5,
            cbar_kws={'label': 'Прогнозиран процент (%)'})

plt.title('Матрица данни: Всички нагласи по партии', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Партия (подредена по реален изборен резултат)', fontsize=12)
plt.ylabel('Агенция / Дата', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()

plt.show()

Отклоненията (Residual Heatmap)

Тази графика показва отклоненията на публукуваните електорални нагласи в различните проучвания от реалния изборен резултат

Show the code
import seaborn as sns
import matplotlib.pyplot as plt

# 1. Филтрираме само колоните с партии (изключваме метаданните и служебните категории)
cols = [c for c in df.columns if c not in ['Agency', 'Date', 'metadata', 'Не съм решил']]

# 2. Изчисляваме остатъците (Прогноза минус Реалност)
# Резултат > 0: Партията е била надценена (Survey > Actual)
# Резултат < 0: Партията е била подценена (Survey < Actual)
residuals = df[cols].sub(actual[cols], axis=1)

# 3. Сортираме партиите по техния реален изборен резултат за по-добра четимост
sorted_parties = actual[cols].sort_values(ascending=False).index

# 4. Визуализация на Heatmap
plt.figure(figsize=(10, 10))
sns.heatmap(residuals[sorted_parties],
            annot=True,
            fmt='.1f',
            cmap='RdBu_r',
            center=0,
            linewidths=.5,
            cbar_kws={'label': 'Отклонение (Процентни пунктове)'})

plt.title('Матрица на отклоненията: Прогноза срещу Реалност', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Партия (подредена по изборен резултат)', fontsize=12)
plt.ylabel('Агенция / Дата', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()

plt.show()

Средна Абсолютна Грешка (MAE)

Средна абсолютна грешка. Измерва средната големина на отклоненията в набор от прогнози, без да отчита тяхната посока (дали е надценено или подценено). Всички грешки се третират с еднаква тежест.

Колкото по-ниска е стойността, толкова по-точни са били данните спрямо официално регистрирания вот в деня на изборите.

Show the code
import matplotlib.pyplot as plt
import numpy as np

# 1. Избор на партиите за сравнение (премахваме метаданните и служебните категории)
cols = [c for c in df.columns if c not in ['Agency', 'Date', 'metadata', 'Не съм решил']]

# 2. Изчисляване на MAE за всеки ред спрямо 'actual'
# Използваме .dropna(), за да сме сигурни, че сравняваме само съществуващи данни
mae = df[cols].apply(lambda x: np.mean(np.abs(x - actual[cols])), axis=1).sort_values()

# 3. Визуализация
plt.figure(figsize=(10, 8), facecolor='#f9f9f9')
colors = plt.cm.RdYlGn_r(np.linspace(0, 1, len(mae)))

# Хоризонтален бар
bars = plt.barh(mae.index, mae.values, color=colors, edgecolor='black', alpha=0.8)

# Добавяне на стойностите върху баровете
for bar in bars:
    plt.text(bar.get_width() + 0.05, bar.get_y() + bar.get_height()/2,
             f'{bar.get_width():.2f}%', va='center', fontsize=9, fontweight='bold')

plt.title('Класация по точност (MAE) - Парламентарни избори 2026', fontsize=14, pad=20, fontweight='bold')
plt.xlabel('Средна грешка в процентни пунктове (по-малко е по-добре)', fontsize=11)
plt.gca().invert_yaxis()  # Най-точните най-отгоре
plt.grid(axis='x', linestyle='--', alpha=0.5)
plt.tight_layout()

plt.show()

# Бонус: Кратка присъда
winner = mae.index[0]
print(f"🏆 НАЙ-ТОЧНА ПРОГНОЗА: {winner} (Грешка: {mae.iloc[0]:.2f}%)")

🏆 НАЙ-ТОЧНА ПРОГНОЗА: Мяра (17/04) (Грешка: 1.99%)

Средноквадратична грешка. (RMSE)

Изчислява се като корен квадратен от средната стойност на квадратите на разликите. Тъй като грешките се вдигат на квадрат преди да се усреднят, тази метрика дава много по-голяма тежест на големите отклонения.

Колкото по-ниска е стойността, толкова по-точни са били данните спрямо официално регистрирания вот в деня на изборите.

Show the code
import matplotlib.pyplot as plt
import numpy as np

# 1. Избор на партиите
cols = [c for c in df.columns if c not in ['Agency', 'Date', 'metadata', 'Не съм решил']]

# 2. Изчисляване на RMSE (Root Mean Square Error)
# RMSE = Квадратен корен от средната стойност на квадратите на разликите
rmse = df[cols].apply(lambda x: np.sqrt(np.mean((x - actual[cols])**2)), axis=1).sort_values()

# 3. Визуализация
plt.figure(figsize=(10, 8), facecolor='#f9f9f9')
colors = plt.cm.RdYlGn_r(np.linspace(0, 1, len(rmse)))

bars = plt.barh(rmse.index, rmse.values, color=colors, edgecolor='black', alpha=0.8)

for bar in bars:
    plt.text(bar.get_width() + 0.05, bar.get_y() + bar.get_height()/2,
             f'{bar.get_width():.2f}', va='center', fontsize=9, fontweight='bold')

plt.title('Класация по точност (RMSE) - Парламентарни избори 2026', fontsize=14, pad=20, fontweight='bold')
plt.xlabel('Корен квадратен от средноквадратичната грешка (по-малко е по-добре)', fontsize=11)
plt.gca().invert_yaxis()
plt.grid(axis='x', linestyle='--', alpha=0.5)
plt.tight_layout()

plt.show()

winner = rmse.index[0]
print(f"🏆 НАЙ-ТОЧНА ПРОГНОЗА (RMSE): {winner} (Грешка: {rmse.iloc[0]:.2f})")

🏆 НАЙ-ТОЧНА ПРОГНОЗА (RMSE): Мяра (17/04) (Грешка: 3.33)

Tренд

Тук проследяваме как прогнозите за основните партии са се променяли във времето спрямо крайния резултат.

Show the code
import matplotlib.pyplot as plt

# 1. Списък с партиите, които искаме да проследим
target_parties = ['Прогресивна България', 'ГЕРБ', 'ПП-ДБ', 'ДПС', 'Възраждане']

# 2. Сортираме данните хронологично
trend_df = df.sort_values('Date')

# 3. Цикъл за генериране на всяка графика
for party in target_parties:
    plt.figure(figsize=(15, 6))

    # Проверка дали партията съществува в данните (за избягване на грешки)
    if party in trend_df.columns:
        # Плотване на прогнозите
        plt.plot(trend_df.index, trend_df[party],
                 marker='o', color='#1f77b4', linewidth=2.5, markersize=8, label=f'Прогнози за {party}')

        # Линия за реалния резултат
        actual_val = actual[party]
        plt.axhline(y=actual_val, color='#d62728', linestyle='--',
                    linewidth=3, label=f'Реален резултат ({actual_val}%)')

        # Оформление
        plt.title(f'Динамика на прогнозите: {party}', fontsize=16, fontweight='bold', pad=15)
        plt.ylabel('Подкрепа (%)', fontsize=12)
        plt.xlabel('Социологическо проучване', fontsize=12)
        plt.xticks(rotation=45, ha='right')
        plt.legend(fontsize=11, loc='best')
        plt.grid(axis='y', alpha=0.3)
        plt.tight_layout()

        plt.show()
    else:
        print(f"Партията '{party}' не беше намерена в данните.")

Анализ на ефекта от методологията (Offline vs. Hybrid)

Статистическият модел (OLS) изолира ефекта на изследователския метод върху точността на прогнозите. Вместо просто да гледаме крайната грешка, ние измерваме колко точно методологията тежи върху резултата, като контролираме за фактори кога е правено проучването или колко голяма е била извадката.

Трябва да се отбележи, че в текущата извадка хибридният метод се припокрива с една от конкретните агенции и не може категорично да се изолира дали източникът на влияние е самата агенция, или методологията т.е. наличе е мултиколинеарити. Както при много статистически изследвания, основна препоръка е: „further research needed, need more data“.

Относно разликата в MAE и RMSE в долната и горните графики

Има разминаване между предходните графики и графиките на OLS модела за двата индикатора (MAE и RMSE), тъй като те са изчислени на различна база. В горните графики индикаторите са изчислени на база на всички данни докато регресионния анализ е излислен само на база парламентарно представен партии.

  • [‘Не подкрепям никого’, ‘Не съм решил’, ‘Други’, ‘Величие’, ‘MEЧ’, ‘АПС’, ‘Има такъв народ’, ‘Сияние’, ‘БСП’, ‘Възраждане’, ‘ДПС’ ‘ПП-ДБ’, ‘ГЕРБ’, ‘Прогресивна България’]
  • [‘Прогресивна България’, ‘ГЕРБ’, ‘ПП-ДБ’, ‘ДПС’, ‘Възраждане’]

Резултати:

Методология (Is_Hybrid)

  • MAE (0.0874, P > 0.05): Методологията не влияе на средното линейно отклонение.
  • RMSE (2.1713, P < 0.01): Хибридните методи имат висока вариативност. Те не бъркат по малко за всичко, а допускат големи отклонения (outliers) при конкретни партии. Статистически значимо е (\(P=0.0063\)), че хибридният метод носи риск от подобен тип грешки.
Show the code
import statsmodels.api as sm
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# --- 1. ПРЕЦИЗНО ИЗЧИСЛЯВАНЕ НА МЕТРИКИТЕ ---
# party_cols = [c for c in df.columns if c in actual.index]
party_cols = ['Прогресивна България',
              'ГЕРБ', 
              'ПП-ДБ', 
              'ДПС',
              'Възраждане',
              #'БСП', 
              #'MEЧ', 
              #'Има такъв народ', 
              #'Величие',
              #'Не подкрепям никого', 
              #'Сияние', 
              #'АПС'
              ]

print(df.columns)
print(party_cols)

def calculate_metrics(row):
    diffs = row[party_cols] - actual[party_cols]
    return pd.Series({
        'MAE': np.abs(diffs).mean(),
        'RMSE': np.sqrt((diffs**2).mean())
    })

metrics_df = df.apply(calculate_metrics, axis=1)

# --- 2. ПОДГОТОВКА НА ДАННИТЕ ЗА РЕГРЕСИЯ ---
election_date = pd.to_datetime('2026-04-19')
ols_data = []

for idx, row in df.iterrows():
    enc = row['metadata'].get('encoded', {})
    if not enc: continue

    ols_data.append({
        'Label': idx,
        'MAE': metrics_df.loc[idx, 'MAE'],
        'RMSE': metrics_df.loc[idx, 'RMSE'],
        'Is_Hybrid': 1 if enc.get('methodology') == 'hybrid' else 0,
        'Days_To_Election': (election_date - pd.to_datetime(enc.get('date'))).days,
        'Sample_Size': enc.get('sample_size', 0)
    })

reg_df = pd.DataFrame(ols_data).set_index('Label').dropna()

# --- 3. ИЗПЪЛНЕНИЕ НА РЕГРЕСИИТЕ ---
X = sm.add_constant(reg_df[['Is_Hybrid', 'Days_To_Election', 'Sample_Size']])
model_mae = sm.OLS(reg_df['MAE'], X).fit()
model_rmse = sm.OLS(reg_df['RMSE'], X).fit()

# --- 4. ВИЗУАЛИЗАЦИЯ 1: ВЛИЯНИЕ НА ФАКТОРИТЕ (КОЕФИЦИЕНТИ) ---
fig1, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 4), sharey=True)
plt.subplots_adjust(wspace=0.1)

# MAE Coefs
model_mae.params[1:].plot(kind='barh', xerr=model_mae.bse[1:], color='#3498db', ax=ax1, alpha=0.8, edgecolor='black')
ax1.axvline(0, color='red', linestyle='--', linewidth=1)
ax1.set_title('Въздействие върху MAE\n(Линейна средна грешка)', fontweight='bold')
ax1.set_xlabel('Промяна в % пунктове')

# RMSE Coefs
model_rmse.params[1:].plot(kind='barh', xerr=model_rmse.bse[1:], color='#e74c3c', ax=ax2, alpha=0.8, edgecolor='black')
ax2.axvline(0, color='red', linestyle='--', linewidth=1)
ax2.set_title('Въздействие върху RMSE\n(Наказание за големи грешки)', fontweight='bold')
ax2.set_xlabel('Промяна в % пунктове')

plt.suptitle('АНАЛИЗ НА ФАКТОРИТЕ ЗА ГРЕШКА (OLS REGRESSION COEF)', fontsize=16, fontweight='bold', y=1.05)
plt.show()

# --- 5. ВИЗУАЛИЗАЦИЯ 2: SIDE-BY-SIDE ТАГНАТА ДИНАМИКА (MAE vs RMSE) ---
fig2, (ax3, ax4) = plt.subplots(2, 1, figsize=(16, 14), sharex=True)
palette = ['#3498db', '#e74c3c']

for ax, metric, color, title in zip([ax3, ax4], ['MAE', 'RMSE'], palette, ['MAE (Линейна)', 'RMSE (Квадратична)']):
    # Scatter plot
    sns.scatterplot(
        data=reg_df, x='Days_To_Election', y=metric,
        hue='Is_Hybrid', size='Sample_Size', sizes=(150, 800),
        palette=palette, alpha=0.6, edgecolor='white', linewidth=1.5, ax=ax
    )

    # Тагване на всяка точка
    for label, row in reg_df.iterrows():
        ax.text(
            x=row['Days_To_Election'],
            y=row[metric] + (row[metric] * 0.02), # динамично отместване нагоре
            s=label, fontsize=8, fontweight='semibold', ha='center',
            bbox=dict(facecolor='white', alpha=0.5, edgecolor='none', pad=0.5)
        )

    ax.invert_xaxis()
    ax.set_title(f'Динамика на {title} грешката', fontsize=14, fontweight='bold')
    ax.set_xlabel('Дни до изборния ден', fontsize=11)
    ax.set_ylabel('Стойност на грешката', fontsize=11)
    ax.grid(True, linestyle=':', alpha=0.6)
    ax.get_legend().remove() # премахваме индивидуалните легенди за по-чист вид

# Единна легенда за целия плот
handles, labels = ax4.get_legend_handles_labels()
fig2.legend(handles, labels, title='Метод (0=Offline, 1=Hybrid) & Извадка', loc='center right', bbox_to_anchor=(1.1, 0.5))

plt.suptitle('СРАВНИТЕЛНА ХРОНОЛОГИЯ НА КАЧЕСТВОТО (ТАГНАТИ НАБЛЮДЕНИЯ)', fontsize=16, fontweight='bold', y=0.98)
plt.tight_layout()
plt.show()

# --- 6. СТРУКТУРИРАН ТЕКСТОВ АУТПУТ ---
print("\n" + "="*60)
print(" СРАВНИТЕЛЕН АНАЛИЗ НА КОЕФИЦИЕНТИТЕ (MAE vs RMSE)")
print("="*60)
summary_table = pd.DataFrame({
    'Metric': ['Is_Hybrid', 'Days_To_Election', 'Sample_Size'],
    'MAE Coef': model_mae.params[1:].values,
    'MAE P-value': model_mae.pvalues[1:].values,
    'RMSE Coef': model_rmse.params[1:].values,
    'RMSE P-value': model_rmse.pvalues[1:].values
}).round(4)
print(summary_table.to_string(index=False))
print("="*60)
Index(['Не подкрепям никого', 'Не съм решил', 'Други', 'Величие', 'MEЧ', 'АПС',
       'Има такъв народ', 'Сияние', 'БСП', 'Възраждане', 'ДПС', 'ПП-ДБ',
       'ГЕРБ', 'Прогресивна България', 'Agency', 'Date', 'metadata'],
      dtype='str')
['Прогресивна България', 'ГЕРБ', 'ПП-ДБ', 'ДПС', 'Възраждане']


============================================================
 СРАВНИТЕЛЕН АНАЛИЗ НА КОЕФИЦИЕНТИТЕ (MAE vs RMSE)
============================================================
          Metric  MAE Coef  MAE P-value  RMSE Coef  RMSE P-value
       Is_Hybrid    0.0874       0.8408     2.1713        0.0063
Days_To_Election   -0.0018       0.8178     0.0025        0.8432
     Sample_Size   -0.0017       0.3791    -0.0020        0.5215
============================================================

Заключение и изводи от анализа

  • Системен пропуск:
    • Всички агенции подцениха победителя (Прогресивна България) с над 10% средно отклонение.
    • Всички агенции надцениха установените играчи (ГЕРБ, ДПС, Възраждане) в различна степен.
  • Методология:
    • Необходима е ревизия на моделите за разпределение на гласовете на нерешилите избиратели.
    • Необходима е критична ревизия на източниците и структурирането на извадката.
  • Информационна хигиена:
    • Ефирът е пълен с демографски, психографски и поведенчески разпределения на вота, към които трябва да се подходи със скептицизъм, имайки предвид мащаба на анализираните отклонения.
    • Анализи на по-малки кохорти (напр. от типа “коя партия е донор на гласоподаватели”) носят риск да са значително по-неточни.
  • Забележка:
    • Нормализирането на толкова голям набор от изследвания изисква задълбочена работа.
    • Този анализ е по-скоро отправна точка за по-задълбочени бъдещи мета-анализи, целящи по добро измерване на електоралните нагласи.
    • Най-належащата задача би била ръчното преглеждане (QA) и коригиране на входящите данни.