#####################################################################################################################
# 3. Создание полного календаря с учетом пиковых часов, рабочих дней и праздников
#####################################################################################################################

# Преобразование столбца 'Дата' в формат datetime
combined_df['Дата'] = pd.to_datetime(combined_df['Дата'], format='%d.%m.%Y')



# ///--------------------------------Добавлен блок от 20.09 (1-ое изменение)

# Получение названия дня недели на английском
combined_df['День_недели'] = combined_df['Дата'].dt.day_name()
combined_df['Месяц'] = combined_df['Дата'].dt.month

# Ваш маппинг
day_mapping = {
                'Monday': 'пн',   'Mon': 'пн',
                'Tuesday': 'вт',  'Tue': 'вт',
                'Wednesday': 'ср','Wed': 'ср',
                'Thursday': 'чт', 'Thu': 'чт',
                'Friday': 'пт',   'Fri': 'пт',
                'Saturday': 'сб', 'Sat': 'сб',
                'Sunday': 'вс',   'Sun': 'вс'
              }

# Добавление нового столбца с днем недели на русском языке
combined_df['День_недели'] = combined_df['День_недели'].map(day_mapping)

# В данные, которые мы распарсили с сайта я добавил поля День_недели, Месяц и мапинг дней недели в нужный формат.
# Это необходимо, поскольку мы будем срезать часы, в которые не было пиков в разрезе сорса, месяца и дня недели.

# --------------------------------Добавлен блок от 20.09---------\\\



# Получение диапазона дат
min_date = combined_df['Дата'].min().date()
max_date = combined_df['Дата'].max().date()

# Создание полного списка дат с часовыми интервалами
hourly_dates = pd.date_range(start=min_date, 
                             end=pd.Timestamp(max_date) + pd.Timedelta(hours=23), freq='H') # -- Изменено от 20.09
# # !!! Очень важно добавить 23 часа к макс дате, так как они иначе не включаются в последний день!!!!



full_calendar_df = pd.DataFrame({
    'Дата': hourly_dates.date,
    'Час': hourly_dates.hour
})

# Уникальные источники данных
sources = combined_df['source'].unique()

# Создание полного календаря для каждого источника
expanded_calendar_df = pd.concat([full_calendar_df.assign(source=src) for src in sources], ignore_index=True)

# Инициализация столбца Пик c значением 0
expanded_calendar_df['Пик'] = 0

# Проставляем пики из исходного датафрейма
for _, row in combined_df.iterrows():
    expanded_calendar_df.loc[
        (expanded_calendar_df['Дата'] == row['Дата'].date()) &
        (expanded_calendar_df['Час'] == row['Час']) &
        (expanded_calendar_df['source'] == row['source']),
        'Пик'
    ] = 1

# Добавление столбцов с месяцем и годом
expanded_calendar_df['Дата'] = pd.to_datetime(expanded_calendar_df['Дата'])
expanded_calendar_df['МесяцГод'] = expanded_calendar_df['Дата'].dt.to_period('M')


expanded_calendar_df = expanded_calendar_df.merge(  
    combined_df[['source', 'Дата' ]],   
    on=['Дата', 'source'],   
    how='inner'  
)  


# Создание производственного календаря
russia_holidays = holidays.Russia()
calend = Russia()

# Диапазон дат для производственного календаря
start_year, end_year = 2010, 2040
work_calendar_data = []

# Формирование списка всех дней с указанием выходных и праздников
current_date = date(start_year, 1, 1)
end_date = date(end_year, 12, 31)

while current_date <= end_date:
    is_working_day = calend.is_working_day(current_date)
    is_holiday = current_date in russia_holidays
    holiday_name = russia_holidays.get(current_date) if is_holiday else ''
    
    day_of_week_mapped = current_date.strftime('%a')
    
    
    work_calendar_data.append([
        current_date,
        int(is_working_day),
        int(is_holiday),
        holiday_name,
        day_mapping.get(day_of_week_mapped)
    ])
    
    current_date += timedelta(days=1)

# Создание датафрейма для производственного календаря
work_calendar_df = pd.DataFrame(work_calendar_data, columns=[
    'Дата', 'Рабочий_день', 'Праздник', 'Название_праздника', 'День_недели'
])

# Преобразование дат в формат datetime для объединения с основным календарем
expanded_calendar_df['Дата'] = pd.to_datetime(expanded_calendar_df['Дата'], dayfirst=True)
work_calendar_df['Дата'] = pd.to_datetime(work_calendar_df['Дата'], dayfirst=True)

# Объединение данных по датам
merged_data = expanded_calendar_df.merge(
    work_calendar_df[['Дата', 'День_недели', 'Рабочий_день', 'Праздник']],
    on='Дата',
    how='left'
)

# Фильтруем данные по рабочим дням и праздникам
final_data = merged_data[
    (merged_data['Рабочий_день'] == 1) & 
    (merged_data['Праздник'] == 0) & 
    (merged_data['День_недели'] != 'сб') &
    (merged_data['День_недели'] != 'вс') 
]


# Выбор нужных столбцов и добавление месяца и года
final_data = final_data[['source', 'Дата', 'МесяцГод', 'День_недели', 'Час', 'Пик']]
final_data['Дата'] = pd.to_datetime(final_data['Дата'], dayfirst=True)
final_data['Месяц'] = final_data['Дата'].dt.month # ----------------- изменено от 20.09 (2-ое изменение)
final_data['МесяцГод'] = final_data['Дата'].dt.to_period('M')

#####################################################################################################################
# 4. Фильтрация данных по уникальным часам для каждого источника и сохранение результата
#####################################################################################################################


# ///----------------------- изменено от 20.09 (3-ое изменение)

# Получаем уникальные комбинации часов и источников
unique_hours_per_source = combined_df[[ 'source' , 'Месяц', 'День_недели', 'Час']].drop_duplicates()

# Фильтруем final_data по уникальным комбинациям через merge
final_data = final_data.merge(unique_hours_per_source, on=['source' , 'Месяц', 'День_недели', 'Час'], how='inner')


#------------------------изменено от 20.09-----\\\


# Сохранение результата 
# Важно!!!!!!!  final_data.to_excel передать в БД!!!!!!!!!!!!

final_data.to_excel(f'{output_dir}/parsed_data.xlsx', index=False)


# --------------------------------------------- Часть 2--------------------------------------------------------------#

#####################################################################################################################
# 5. Построение прогноза с помощью модели CatBoost
#####################################################################################################################


# Функция для определения сезона (1 = Нестабильные периоды; 0 = Стабильные)
def get_season(month):
    if month in [6, 7]:
        return 1
    else:
        return 0 

# Функция для генерации тестовых данных и производственного календаря (рабочие дни)
def generate_test_data(first_day_of_month, sources):

    # Преобразуем начало месяца в datetime и вычисляем конец месяца
    first_day_of_month = pd.to_datetime(first_day_of_month)
    last_day_of_month = first_day_of_month + pd.offsets.MonthEnd(1)

    # Генерация всех дней и часов для целевого месяца
    hourly_dates = pd.date_range(start=first_day_of_month, end=last_day_of_month +  pd.Timedelta(hours=23), freq='H')

    test_data = pd.DataFrame({
      			      'Дата': hourly_dates.date,
       			      'Час': hourly_dates.hour
                            })

    # Преобразуем столбец 'Дата' в datetime для правильного объединения
    test_data['Дата'] = pd.to_datetime(test_data['Дата'])

    # Генерация производственного календаря
    russia_holidays = holidays.Russia()
    start_year, end_year = first_day_of_month.year, first_day_of_month.year
    work_calendar_data = []

    # Формирование производственного календаря
    current_date = date(start_year, 1, 1)
    end_date = date(end_year, 12, 31)

    while current_date <= end_date:
        is_working_day = not (current_date.weekday() >= 5 or current_date in russia_holidays)
        is_holiday = current_date in russia_holidays
        holiday_name = russia_holidays.get(current_date) if is_holiday else ''

        day_of_week_mapped = current_date.strftime('%a')
        day_mapping = {'Mon': 'пн', 'Tue': 'вт', 'Wed': 'ср', 'Thu': 'чт', 'Fri': 'пт', 'Sat': 'сб', 'Sun': 'вс'}

        work_calendar_data.append([
            current_date,
            int(is_working_day),
            int(is_holiday),
            holiday_name,
            day_mapping.get(day_of_week_mapped)
        ])

        current_date += timedelta(days=1)

    # Создание датафрейма для производственного календаря
    work_calendar_df = pd.DataFrame(work_calendar_data,
                                    columns=['Дата', 'Рабочий_день', 'Праздник', 'Название_праздника', 'День_недели'])
    work_calendar_df['Дата'] = pd.to_datetime(work_calendar_df['Дата'], dayfirst=True)

    # Присоединяем производственный календарь к тестовым данным
    test_data = test_data.merge(work_calendar_df[['Дата', 'Рабочий_день', 'Праздник', 'День_недели']], on='Дата', how='left')

    # Оставляем только рабочие дни, исключая праздники, субботы и воскресенья
    test_data = test_data[(test_data['Рабочий_день'] == 1) & (test_data['Праздник'] == 0) &
                          (test_data['День_недели'] != 'сб') & (test_data['День_недели'] != 'вс')]

    # Добавляем дополнительные признаки: месяц, год, сезон
    test_data['Месяц'] = test_data['Дата'].dt.month
    test_data['Год'] = test_data['Дата'].dt.year
    test_data['Сезон'] = test_data['Месяц'].apply(get_season)

    
    
    # Дублируем данные для каждого источника
    expanded_test_data = pd.concat([test_data.assign(source=src) for src in sources], ignore_index=True)


    # ///-----------------------изменено от 20.09 (4-ое изменение)
    
    # Получаем уникальные комбинации часов и источников
    unique_hours_per_source = combined_df[[ 'source' , 'Месяц', 'День_недели', 'Час']].drop_duplicates()

    # Фильтруем final_data по уникальным комбинациям через merge
    expanded_test_data = expanded_test_data.merge(unique_hours_per_source,
                                                  on=['source' , 'Месяц', 'День_недели', 'Час'], 
                                                  how='inner')
    
    return expanded_test_data


    # Описание: При формировании модельных данных на будущие периоды, мы должны по аналогии с фактическими данными
    # исключать часы в разрезе source, Месяц, День_недели.

     # -----------------------изменено от 20.09 # ///


# Функция для прогнозирования на новый месяц
def forecast_for_new_month(generated_test_data, threshold, source):
   
    # Формируем обучающую выборку: train data
    train_data = df[(df['Дата'] < generated_test_data['Дата'].min().replace(day=1)) & (df['source'] == source)]

    # Создание DataFrame для итоговых результатов
    res_df = generated_test_data[generated_test_data['source'] == source].copy()

    # Признаки для тестовых данных
    test_data = generated_test_data[generated_test_data['source'] == source]

    # Обработка данных для train
    train_data = train_data[['Час', 'Месяц', 'Год', 'День_недели', 'Сезон', 'Пик']]  # Добавляем необходимые признаки
    train_data = pd.get_dummies(train_data, columns=['День_недели'])  # One-hot encoding для дней недели

    # Обработка данных для test
    test_data = test_data[['Час', 'Месяц', 'Год', 'День_недели', 'Сезон']]  # Убираем целевую переменную 'Пик'
    test_data = pd.get_dummies(test_data, columns=['День_недели'])  # One-hot encoding для дней недели

    # Выделение признаков и целевой переменной для тренировочных данных
    X_train = train_data.drop('Пик', axis=1)
    y_train = train_data['Пик']

    # Обучение модели CatBoost
    model = CatBoostClassifier(verbose=False)
    model.fit(X_train, y_train)

    # Прогнозирование
    predictions = model.predict(test_data)
    probabilities = model.predict_proba(test_data)[:, 1]

    # В датафрейм с результатами добавляем прогнозы
    res_df['Пик_предсказание'] = predictions
    res_df['Вероятность_пика'] = probabilities.round(3)  # Округление до 3 знаков

    # Нахождение рангов в рамках каждого дня
    res_df['Ранг'] = res_df.groupby('Дата')['Вероятность_пика'].rank(method='first', ascending=False)

    # Установка значения 1 для первых значений в рамках дня, иначе 0
    res_df['Пик_предсказание'] = (res_df['Ранг'] <= threshold).astype(int)

    # Удаление столбца 'Ранг', если он больше не нужен
    res_df = res_df.drop(columns=['Ранг'])

    # Возвращаем только нужные столбцы в нужном порядке
    return res_df[['source', 'Дата', 'Год', 'Месяц', 'День_недели', 'Час', 'Пик_предсказание', 'Вероятность_пика']]


# Функция для прогнозирования на исторические месяцы с фактическими данными
def forecast_for_existing_month(first_day_of_month, threshold, source):
    # Фильтрация данных по дате и источнику для train и test выборок
    train_data = df[(df['Дата'] < pd.to_datetime(first_day_of_month)) & (df['source'] == source)]
    test_data = df[(df['Дата'].dt.month == pd.to_datetime(first_day_of_month).month) &
                    (df['Дата'].dt.year == pd.to_datetime(first_day_of_month).year) &
                    (df['source'] == source)]

    # Создание DataFrame res_df на основе исходных данных test_data
    res_df = test_data.copy()

    # Добавляем признаки 'Месяц', 'Год', 'Сезон'
    train_data['Месяц'] = train_data['Дата'].dt.month
    train_data['Год'] = train_data['Дата'].dt.year
    train_data['Сезон'] = train_data['Месяц'].apply(get_season)

    test_data['Месяц'] = test_data['Дата'].dt.month
    test_data['Год'] = test_data['Дата'].dt.year
    test_data['Сезон'] = test_data['Месяц'].apply(get_season)

    # Выбор нужных столбцов
    train_data = train_data[['Час', 'Месяц', 'Год', 'День_недели', 'Сезон', 'Пик']]  # 'День_Года' 'weights'
    train_data = pd.get_dummies(train_data, columns=['День_недели'])

    test_data = test_data[['Час', 'Месяц', 'Год', 'День_недели', 'Сезон', 'Пик']]  # 'День_Года' 'weights'
    test_data = pd.get_dummies(test_data, columns=['День_недели'])

    # Выделение признаков и целевой переменной для тренировочных данных
    X_train = train_data.drop('Пик', axis=1)
    y_train = train_data['Пик']

    # Выделение признаков и целевой переменной для тестовых данных
    X_test = test_data.drop('Пик', axis=1)
    y_test = test_data['Пик']

    # Обучение модели CatBoost
    model = CatBoostClassifier(verbose=False)
    model.fit(X_train, y_train)

    # Прогнозирование
    predictions = model.predict(X_test)
    probabilities = model.predict_proba(X_test)[:, 1]

    # Создание DataFrame для результатов
    res_df['Пик_предсказание'] = predictions
    res_df['Вероятность_пика'] = probabilities

    # Нахождение рангов в рамках каждого дня
    res_df['Ранг'] = res_df.groupby('Дата')['Вероятность_пика'].rank(method='first', ascending=False)

    # Установка значения 1 для первых значений в рамках дня, иначе 0
    res_df['Пик_предсказание'] = (res_df['Ранг'] <= threshold).astype(int)

    # Удаление столбца 'Ранг', если он больше не нужен
    res_df = res_df.drop(columns=['Ранг'])

    return res_df[['source', 'Дата', 'Год', 'Месяц', 'День_недели', 'Час', 'Пик_предсказание', 'Вероятность_пика']]

def process_forecasts(sources, thresholds, flag='historical', current_months_for_prediction=None):
    # Если месяцы для прогноза не заданы, используем первое число текущего месяца
    if current_months_for_prediction is None:
        current_first_day = datetime.today().replace(day=1).strftime('%Y-%m-%d')
        current_months_for_prediction = [current_first_day]

    # Список исторических месяцев с фактическими данными
    historical_months = ['2024-01-01', '2024-02-01', '2024-03-01', '2024-04-01', '2024-05-01',
                         '2024-06-01', '2024-07-01', '2024-08-01', '2023-09-01', '2023-10-01',
                         '2023-11-01', '2023-12-01']

    # Итоговая таблица для результатов
    all_forecasts = pd.DataFrame()

    # Цикл по источникам и месяцам
    for source in sources:
        if flag == 'historical':
            # Обработка исторических месяцев
            for month in historical_months:
                for threshold in thresholds:
                    # Строим прогноз для исторических месяцев
                    forecast_result = forecast_for_existing_month(month, threshold, source)
                    all_forecasts = pd.concat([all_forecasts, forecast_result], ignore_index=True)
        elif flag == 'current':
            # Обработка новых месяцев (с генерацией тестовых данных)
            for month in current_months_for_prediction:
                # Генерация данных для нового месяца
                generated_test_data = generate_test_data(month, sources)
                for threshold in thresholds:
                    # Строим прогноз для сгенерированных данных
                    forecast_result = forecast_for_new_month(generated_test_data, threshold, source)
                    all_forecasts = pd.concat([all_forecasts, forecast_result], ignore_index=True)

    # Сохранение итогового DataFrame в файл
    return all_forecasts


# Вызов функций 
df = pd.read_excel('output/parsed_data.xlsx')
df['Дата'] = pd.to_datetime(df['Дата'], format='%d.%m.%Y')
df['День'] = df['Дата'].dt.day
df['Месяц'] = df['Дата'].dt.month
df['Год'] = df['Дата'].dt.year
df['Сезон'] = df['Месяц'].apply(get_season)
df = df[['source', 'Дата', 'МесяцГод', 'Год', 'Месяц', 'День_недели', 'День',  'Час', 'Сезон', 'Пик']]

# Источники данных и пороги
sources = df['source'].unique()

# Устанавливаем порог для двух наиболее вероятных часов в дне
thresholds = [2]  

# Указание первого дня месяца для прогноза (в данном случае 1 августа 2024 года)
current_months_for_prediction = ['2024-09-01'] # !!!!! Можно не указывать - по дефолту текущий месяц года!!!!!

# Вызов функции для построения прогноза на выбранный период
forecast_df = process_forecasts(
    sources=sources,
    thresholds=thresholds,
    flag='current',  # Прогноз на новые месяцы
    current_months_for_prediction=current_months_for_prediction
)

# Сохранение файла
save_forecast = "forecast_results"
os.makedirs(save_forecast, exist_ok=True)

# Важно!!!!!!!  forecast_df передать в БД!!!!!!!!!!!!
forecast_df.to_excel(f'{save_forecast}/forecast_results_catboost.xlsx', index=False)

Перепиши весь код целиком с учетом этих функций