import matplotlib.pyplot as plt import matplotlib.animation as animation import numpy as np def minutes_to_time(minutes, start_time="00:00"): """Преобразует минуты от старта в строку времени ЧЧ:ММ""" start_hour, start_min = map(int, start_time.split(':')) total_minutes = start_hour * 60 + start_min + minutes hour = (total_minutes // 60) % 24 minute = total_minutes % 60 return f"{hour:02d}:{minute:02d}" def plot_queue_dynamics(queue_history, business_queue_history=None, start_time="00:00"): """ Два отдельных графика для очередей с временной шкалой ЧЧ:ММ """ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) # Создаем метки времени для каждого часа total_minutes = len(queue_history) hours = range(0, total_minutes, 60) # каждый час hour_labels = [minutes_to_time(m, start_time) for m in hours] # График 1: Очередь моделей ax1.plot(range(total_minutes), queue_history, 'b-', linewidth=1.5) ax1.set_xticks(hours) ax1.set_xticklabels(hour_labels, rotation=45) ax1.set_xlabel('Время') ax1.set_ylabel('Размер очереди') ax1.set_title('Очередь моделей') ax1.grid(True, alpha=0.3) # График 2: Очередь бизнес-правил if business_queue_history and len(business_queue_history) > 0: ax2.plot(range(total_minutes), business_queue_history, 'orange', linewidth=1.5) ax2.set_xticks(hours) ax2.set_xticklabels(hour_labels, rotation=45) ax2.set_xlabel('Время') ax2.set_ylabel('Размер очереди') ax2.set_title('Очередь бизнес-правил') ax2.grid(True, alpha=0.3) else: ax2.text(0.5, 0.5, 'Нет данных', ha='center', va='center', transform=ax2.transAxes) ax2.set_title('Очередь бизнес-правил') ax2.set_xlabel('Время') plt.tight_layout() return plt def plot_specialist_load(specialist_busy_history, specialists_count, start_time="00:00"): """График загрузки специалистов с временной шкалой ЧЧ:ММ""" load_percent = [busy / specialists_count * 100 for busy in specialist_busy_history] fig, ax = plt.subplots(figsize=(10, 4)) total_minutes = len(load_percent) hours = range(0, total_minutes, 60) # каждый час hour_labels = [minutes_to_time(m, start_time) for m in hours] ax.plot(range(total_minutes), load_percent, 'g-', linewidth=1.5) ax.axhline(y=100, color='r', linestyle='--', alpha=0.5, label='Максимум') ax.axhline(y=80, color='b', linestyle='--', alpha=0.5, label='Цель 80%') ax.set_xticks(hours) ax.set_xticklabels(hour_labels, rotation=45) ax.set_xlabel('Время') ax.set_ylabel('Загрузка (%)') ax.set_title('Загрузка специалистов') ax.legend() ax.grid(True, alpha=0.3) ax.set_ylim(0, 110) plt.tight_layout() return plt def plot_inflow(minute_counts, start_time="00:00"): """ График входящего потока заявок с заливкой под кривой """ fig, ax = plt.subplots(figsize=(14, 5)) total_minutes = len(minute_counts) minutes = range(total_minutes) # Заливка под кривой (area plot) ax.fill_between(minutes, minute_counts, alpha=0.3, color='blue', label='Общий поток') # Основной график (линия поверх заливки) ax.plot(minutes, minute_counts, 'b-', linewidth=1.5, alpha=0.8) # Скользящее среднее window = 30 if total_minutes > window: smoothed = np.convolve(minute_counts, np.ones(window) / window, mode='valid') ax.plot(range(window - 1, total_minutes), smoothed, 'r-', linewidth=2.5, label=f'Среднее за 30 мин') # Можно добавить заливку и для среднего (опционально) # ax.fill_between(range(window - 1, total_minutes), smoothed, alpha=0.2, color='red') # Метки времени hours = range(0, total_minutes, 60) hour_labels = [minutes_to_time(m, start_time) for m in hours] ax.set_xticks(hours) ax.set_xticklabels(hour_labels, rotation=45) ax.set_xlabel('Время') ax.set_ylabel('Количество заявок') ax.set_title('Входящий поток заявок') ax.legend() ax.grid(True, alpha=0.3) # Добавим горизонтальную линию среднего mean_value = np.mean(minute_counts) ax.axhline(y=mean_value, color='gray', linestyle='--', alpha=0.7, label=f'Среднее: {mean_value:.1f}') plt.tight_layout() return plt def minutes_to_time(minutes, start_time="00:00"): """Преобразует минуты от старта в строку времени ЧЧ:ММ""" start_hour, start_min = map(int, start_time.split(':')) total_minutes = start_hour * 60 + start_min + minutes hour = (total_minutes // 60) % 24 minute = total_minutes % 60 return f"{hour:02d}:{minute:02d}" def plot_detailed_decisions(batch_stats, second_model_name="XGBoost", start_time="00:00"): """ Набор графиков для каждого типа решений отдельно с временной шкалой ЧЧ:ММ """ if not batch_stats: return None fig, axes = plt.subplots(3, 2, figsize=(14, 10)) times = [stat['time'] for stat in batch_stats] # минуты total_minutes = max(times) if times else 0 # Метки времени каждый час hours = range(0, total_minutes + 60, 60) hour_labels = [minutes_to_time(m, start_time) for m in hours] # 1. Бизнес-правила (ручной разбор) axes[0, 0].plot(times, [stat['business_manual'] for stat in batch_stats], 'r-', linewidth=1.5) axes[0, 0].fill_between(times, 0, [stat['business_manual'] for stat in batch_stats], alpha=0.2, color='red') axes[0, 0].set_title('Ручной разбор: бизнес-правила', fontweight='bold') axes[0, 0].set_xticks(hours) axes[0, 0].set_xticklabels(hour_labels, rotation=45) axes[0, 0].set_xlabel('Время') axes[0, 0].set_ylabel('Заявок') axes[0, 0].grid(True, alpha=0.3) # 2. Бизнес-правила (авто отказ) axes[0, 1].plot(times, [stat['business_auto'] for stat in batch_stats], 'darkred', linewidth=1.5) axes[0, 1].fill_between(times, 0, [stat['business_auto'] for stat in batch_stats], alpha=0.2, color='darkred') axes[0, 1].set_title('Авто отказ: бизнес-правила', fontweight='bold') axes[0, 1].set_xticks(hours) axes[0, 1].set_xticklabels(hour_labels, rotation=45) axes[0, 1].set_xlabel('Время') axes[0, 1].set_ylabel('Заявок') axes[0, 1].grid(True, alpha=0.3) # 3. LR уверенные решения axes[1, 0].plot(times, [stat['lr_confident'] for stat in batch_stats], 'blue', linewidth=1.5) axes[1, 0].fill_between(times, 0, [stat['lr_confident'] for stat in batch_stats], alpha=0.2, color='blue') axes[1, 0].set_title('Уверенные решения: Logistic Regression', fontweight='bold') axes[1, 0].set_xticks(hours) axes[1, 0].set_xticklabels(hour_labels, rotation=45) axes[1, 0].set_xlabel('Время') axes[1, 0].set_ylabel('Заявок') axes[1, 0].grid(True, alpha=0.3) # 4. Вторая модель уверенные решения axes[1, 1].plot(times, [stat['second_confident'] for stat in batch_stats], 'green', linewidth=1.5) axes[1, 1].fill_between(times, 0, [stat['second_confident'] for stat in batch_stats], alpha=0.2, color='green') axes[1, 1].set_title(f'Уверенные решения: {second_model_name}', fontweight='bold') axes[1, 1].set_xticks(hours) axes[1, 1].set_xticklabels(hour_labels, rotation=45) axes[1, 1].set_xlabel('Время') axes[1, 1].set_ylabel('Заявок') axes[1, 1].grid(True, alpha=0.3) # 5. Ручной разбор от моделей axes[2, 0].plot(times, [stat['second_uncertain'] for stat in batch_stats], 'orange', linewidth=1.5) axes[2, 0].fill_between(times, 0, [stat['second_uncertain'] for stat in batch_stats], alpha=0.2, color='orange') axes[2, 0].set_title('Ручной разбор: модели неуверенны', fontweight='bold') axes[2, 0].set_xticks(hours) axes[2, 0].set_xticklabels(hour_labels, rotation=45) axes[2, 0].set_xlabel('Время') axes[2, 0].set_ylabel('Заявок') axes[2, 0].grid(True, alpha=0.3) # 6. Сравнительный график axes[2, 1].plot(times, [stat['business_manual'] for stat in batch_stats], 'r-', linewidth=1.5, label='Бизнес-правила', alpha=0.7) axes[2, 1].plot(times, [stat['second_uncertain'] for stat in batch_stats], 'orange', linewidth=1.5, label='Модели неуверенны', alpha=0.7) axes[2, 1].set_title('Сравнение источников ручного разбора', fontweight='bold') axes[2, 1].set_xticks(hours) axes[2, 1].set_xticklabels(hour_labels, rotation=45) axes[2, 1].set_xlabel('Время') axes[2, 1].set_ylabel('Заявок') axes[2, 1].legend() axes[2, 1].grid(True, alpha=0.3) plt.suptitle('Детальный анализ решений', fontsize=14, fontweight='bold') plt.tight_layout() return plt def plot_parameters_history(pid_history, second_model_name="XGBoost", start_time="00:00"): """График изменения параметров регулятора""" if pid_history is None or pid_history.empty: return None fig, axes = plt.subplots(3, 1, figsize=(12, 12)) total_minutes = len(pid_history) times = range(total_minutes) # Метки времени hours = range(0, total_minutes, 60) hour_labels = [minutes_to_time(m, start_time) for m in hours] # 1. Отступы LR axes[0].plot(times, pid_history['lr_low'], 'g-', linewidth=2, label='LR Low') axes[0].plot(times, pid_history['lr_high'], 'r-', linewidth=2, label='LR High') axes[0].set_ylabel('Отступ') axes[0].set_title('Отступы Logistic Regression') axes[0].legend() axes[0].grid(True, alpha=0.3) axes[0].set_xticks(hours) axes[0].set_xticklabels(hour_labels, rotation=45) # 2. Отступы второй модели (с именем из параметра) axes[1].plot(times, pid_history['second_low'], 'g-', linewidth=2, label=f'{second_model_name} Low') axes[1].plot(times, pid_history['second_high'], 'r-', linewidth=2, label=f'{second_model_name} High') axes[1].set_ylabel('Отступ') axes[1].set_title(f'Отступы {second_model_name}') axes[1].legend() axes[1].grid(True, alpha=0.3) axes[1].set_xticks(hours) axes[1].set_xticklabels(hour_labels, rotation=45) # 3. Ошибка загрузки и выход регулятора axes[2].plot(times, pid_history['error_load'], 'b-', label='Error load', alpha=0.7, linewidth=1.5) axes[2].plot(times, pid_history['output'], 'r-', label='Output', linewidth=2, alpha=0.7) axes[2].axhline(y=0, color='black', linestyle='-', linewidth=0.5) axes[2].set_xlabel('Время') axes[2].set_ylabel('Значение') axes[2].set_title('Ошибка загрузки и выход регулятора') axes[2].legend() axes[2].grid(True, alpha=0.3) axes[2].set_xticks(hours) axes[2].set_xticklabels(hour_labels, rotation=45) plt.tight_layout() return plt # def plot_summary(processor): # """Сводный дашборд""" # fig, axes = plt.subplots(2, 3, figsize=(15, 10)) # # stats = processor.stats # # # 1. Динамика очередей # axes[0, 0].plot(stats['queue_history'], 'b-', linewidth=1.5, label='Очередь моделей') # if 'business_queue_history' in stats: # axes[0, 0].plot(stats['business_queue_history'], 'orange', linewidth=1.5, label='Очередь бизнес-правил') # axes[0, 0].set_title('Динамика очередей') # axes[0, 0].set_xlabel('Минута') # axes[0, 0].set_ylabel('Заявок') # axes[0, 0].legend() # axes[0, 0].grid(True, alpha=0.3) # # # 2. Загрузка специалистов (модели) # load = [b / processor.specialists_count * 100 for b in stats['specialist_busy']] # axes[0, 1].plot(load, 'g-', linewidth=1.5, label='Основные специалисты') # axes[0, 1].axhline(y=100, color='r', linestyle='--', alpha=0.5, label='Максимум') # if hasattr(processor, 'target_load'): # axes[0, 1].axhline(y=processor.target_load * 100, color='b', linestyle='--', # alpha=0.5, label=f'Цель {processor.target_load * 100:.0f}%') # axes[0, 1].set_title('Загрузка специалистов (модели)') # axes[0, 1].set_xlabel('Минута') # axes[0, 1].set_ylabel('%') # axes[0, 1].legend() # axes[0, 1].grid(True, alpha=0.3) # # # 3. Загрузка экспертов # if 'business_specialist_busy' in stats and stats['business_specialist_busy']: # business_load = [b / processor.business_specialists_count * 100 for b in stats['business_specialist_busy']] # axes[1, 0].plot(business_load, 'orange', linewidth=1.5, label='Эксперты') # axes[1, 0].axhline(y=100, color='r', linestyle='--', alpha=0.5, label='Максимум') # axes[1, 0].set_title('Загрузка экспертов (бизнес-правила)') # axes[1, 0].set_xlabel('Минута') # axes[1, 0].set_ylabel('%') # axes[1, 0].legend() # axes[1, 0].grid(True, alpha=0.3) # else: # axes[1, 0].text(0.5, 0.5, 'Нет данных по экспертам', ha='center', va='center') # axes[1, 0].set_title('Загрузка экспертов') # # # 4. Распределение решений # sizes = [ # stats['auto_approved'], # stats['auto_declined'], # stats['manual_processed'], # stats.get('business_manual_processed', 0) # ] # labels = ['Одобрено авто', 'Отказ авто', 'Ручной (модели)', 'Ручной (бизнес)'] # colors = ['#2ecc71', '#e74c3c', '#3498db', '#f39c12'] # # if sum(sizes) > 0: # wedges, texts, autotexts = axes[1, 1].pie(sizes, labels=labels, autopct='%1.1f%%', # colors=colors, startangle=90) # for autotext in autotexts: # autotext.set_color('white') # autotext.set_fontweight('bold') # axes[1, 1].set_title('Итоговые решения') # # # 5. Ключевые метрики (освободилось место) # total = stats['total_processed'] # if total > 0: # avg_wait = np.mean(stats['wait_times']) if stats['wait_times'] else 0 # avg_business_wait = np.mean(stats.get('business_wait_times', [0])) if stats.get('business_wait_times') else 0 # # metrics_text = f""" # Всего заявок: {total:,} # Одобрено авто: {stats['auto_approved']:,} ({stats['auto_approved'] / total * 100:.1f}%) # Отказ авто: {stats['auto_declined']:,} ({stats['auto_declined'] / total * 100:.1f}%) # # Ручной разбор (модели): {stats['manual_processed']:,} ({stats['manual_processed'] / total * 100:.1f}%) # Ручной разбор (бизнес): {stats.get('business_manual_processed', 0):,} # # Среднее время ожидания (модели): {avg_wait:.1f} мин # Среднее время ожидания (бизнес): {avg_business_wait:.1f} мин # # Средняя загрузка специалистов: {np.mean(load):.1f}% # """ # else: # metrics_text = "Нет данных" # # axes[0, 2].text(0.1, 0.5, metrics_text, transform=axes[0, 2].transAxes, # fontsize=10, verticalalignment='center', fontfamily='monospace') # axes[0, 2].axis('off') # axes[0, 2].set_title('Ключевые метрики') # # # 6. Пустой график или можно что-то еще # axes[1, 2].axis('off') # # plt.suptitle('Сводная статистика симуляции', fontsize=14, fontweight='bold') # plt.tight_layout() # return plt