| import plotly.graph_objects as go |
| from plotly.subplots import make_subplots |
| 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 create_animation_frame_plotly(frame_data, specialists_count, second_model_name="XGBoost"): |
| |
| time_ticks = list(range(0, 1441, 180)) |
| time_labels = [minutes_to_time(t, "00:00") for t in time_ticks] |
|
|
| fig = make_subplots( |
| rows=3, cols=2, |
| subplot_titles=('📈 Динамика входящего потока', '⚙️ Загрузка специалистов (%)', |
| '👥 МОНИТОРИНГ РАБОТЫ СПЕЦИАЛИСТОВ', '', |
| '📊 Сводная статистика обработки', '🎯 Оперативные показатели'), |
| specs=[ |
| [{'type': 'scatter'}, {'type': 'scatter'}], |
| [{'type': 'heatmap', 'colspan': 2}, None], |
| [{'type': 'table'}, {'type': 'scatter'}] |
| ], |
| row_heights=[0.25, 0.40, 0.35], |
| vertical_spacing=0.1, |
| ) |
|
|
| |
| inflow_h = frame_data.get('inflow_history', []) |
| load_h = frame_data.get('load_history', []) |
|
|
| fig.add_trace(go.Scatter(y=inflow_h, fill='tozeroy', line=dict(color='#4361ee', width=2)), row=1, col=1) |
| fig.add_trace(go.Scatter(y=[l * 100 for l in load_h], fill='tozeroy', line=dict(color='#4cc9f0', width=2)), row=1, |
| col=2) |
|
|
| for col in [1, 2]: |
| fig.update_xaxes(range=[0, 1440], tickvals=time_ticks, ticktext=time_labels, row=1, col=col) |
| fig.update_yaxes(rangemode="tozero", row=1, col=col) |
|
|
| |
| states = np.array(frame_data['specialist_states']) |
| cols = 20 |
| rows = int(np.ceil(specialists_count / cols)) |
|
|
| |
| z_matrix = np.full((rows, cols), np.nan) |
| for i, val in enumerate(states): |
| r, c = divmod(i, cols) |
| |
| if val == 0: |
| z_matrix[r, c] = 0.1 |
| elif val <= 3: |
| z_matrix[r, c] = 0.4 |
| elif val <= 7: |
| z_matrix[r, c] = 0.7 |
| else: |
| z_matrix[r, c] = 1.0 |
|
|
| |
| colorscale = [ |
| [0.0, '#66ccff'], |
| [0.4, '#4ade80'], |
| [0.7, '#facc15'], |
| [1.0, '#f87171'] |
| ] |
|
|
| fig.add_trace(go.Heatmap( |
| z=z_matrix, colorscale=colorscale, showscale=False, |
| xgap=2, ygap=2, zmin=0, zmax=1, hoverinfo='none' |
| ), row=2, col=1) |
|
|
| |
| free = sum(1 for t in states if t <= 0) |
| legend = (f"Свободно: <b>{free}</b> | <span style='color:#66ccff'>■</span> Свободен " |
| f"<span style='color:#4ade80'>■</span> 1-3м <span style='color:#facc15'>■</span> 4-7м " |
| f"<span style='color:#f87171'>■</span> 8м+") |
| fig.add_annotation(text=legend, xref="paper", yref="paper", x=0.5, y=0.70, showarrow=False, font=dict(size=14)) |
|
|
| |
| cum = frame_data['cumulative'] |
| fig.add_trace(go.Table( |
| header=dict(values=['Параметр', 'Значение'], fill_color='#1e293b', font=dict(color='white', size=15), |
| height=35), |
| cells=dict(values=[ |
| ['✅ Авто-одобрено', '❌ Авто-отказы', '👤 На рассмотрении (Manual)', '<b>ИТОГО ОБРАБОТАНО</b>'], |
| [cum['auto_approved'], cum['auto_declined'], |
| cum['manual_processed'] + cum['business_manual_processed'], f"<b>{cum['total_processed']}</b>"] |
| ], align='left', font=dict(size=14), height=35, fill_color='#f8f9fa') |
| ), row=3, col=1) |
|
|
| |
| q_models = frame_data['queue'] |
| q_business = frame_data.get('business_queue', 0) |
|
|
| |
| avg_w = frame_data.get('avg_wait', 0) |
|
|
| status_card = ( |
| f"<span style='font-size:22px; font-weight:bold;'>МОНИТОРИНГ</span><br><br>" |
| f"<span style='background-color:#dcfce7; color:#166534; padding:8px; border-radius:5px;'>" |
| f"<b>👤 ОЧЕРЕДЬ (СПЕЦ): {q_models}</b></span><br><br>" |
| f"<span style='font-size:18px; color:#666;'>" |
| f"⚙️ Бизнес-правила: {q_business}</span><br><br>" |
| f"🕒 Время: <b>{frame_data['time_str']}</b><br>" |
| f"⏳ Ожидание: <b>{avg_w:.1f} мин</b>" |
| ) |
|
|
| fig.add_trace(go.Scatter(x=[0], y=[0], mode='text', text=[status_card], textfont=dict(size=16)), row=3, col=2) |
|
|
| |
| fig.update_xaxes(visible=False, row=2, col=1); |
| fig.update_yaxes(visible=False, row=2, col=1) |
| fig.update_xaxes(visible=False, row=3, col=2); |
| fig.update_yaxes(visible=False, row=3, col=2) |
|
|
| |
| fig.update_yaxes(range=[0, 60], row=1, col=1) |
| fig.update_yaxes(range=[0, 105], row=1, col=2) |
|
|
| fig.update_layout( |
| height=950, |
| margin=dict(t=80, b=40, l=50, r=50), |
| template="plotly_white", |
| showlegend=False, |
| |
| transition_duration=0, |
| hovermode=False |
| ) |
|
|
| |
| fig.layout.datarevision = frame_data['time'] |
| return fig |
|
|
|
|
| from matplotlib.animation import FFMpegWriter |
|
|
| import matplotlib.pyplot as plt |
| import matplotlib.animation as animation |
| import tempfile |
| import numpy as np |
|
|
| import matplotlib.pyplot as plt |
| import matplotlib.animation as animation |
| import tempfile |
| import numpy as np |
| import os |
|
|
|
|
| |
| def create_simulation_video(frames, specialists_count, second_model_name, fps=24): |
| if not frames: |
| return None |
|
|
| |
| plt.style.use('seaborn-v0_8-whitegrid') |
| fig, axes = plt.subplots(2, 2, figsize=(16, 10), facecolor='#f8f9fa') |
| plt.subplots_adjust(hspace=0.4, wspace=0.25) |
| plt.close() |
|
|
| def update(i): |
| data = frames[i] |
| for ax in axes.flatten(): |
| ax.clear() |
| ax.set_facecolor('white') |
|
|
| |
| y_inflow = data['inflow_history'] |
| axes[0, 0].fill_between(range(len(y_inflow)), y_inflow, color='#4361ee', alpha=0.3) |
| axes[0, 0].plot(range(len(y_inflow)), y_inflow, color='#4361ee', linewidth=2) |
| axes[0, 0].set_xlim(0, 1440) |
| axes[0, 0].set_title("ДИНАМИКА ПОТОКА (заявок/мин)", fontsize=12, fontweight='bold') |
| axes[0, 0].set_xlabel("Минуты симуляции") |
|
|
| |
| y_load = [v * 100 for v in data['load_history']] |
| axes[0, 1].fill_between(range(len(y_load)), y_load, color='#4cc9f0', alpha=0.3) |
| axes[0, 1].plot(range(len(y_load)), y_load, color='#4cc9f0', linewidth=2) |
| axes[0, 1].axhline(y=80, color='#f72585', linestyle='--', alpha=0.6) |
| axes[0, 1].set_xlim(0, 1440) |
| axes[0, 1].set_ylim(0, 110) |
| axes[0, 1].set_title(f"ЗАГРУЖЕННОСТЬ СПЕЦИАЛИСТОВ %: {y_load[-1]:.1f}%", fontsize=12, fontweight='bold') |
|
|
| |
| states = np.array(data['specialist_states']) |
| cols = 20 |
| rows = int(np.ceil(specialists_count / cols)) |
| z = np.zeros((rows, cols)) |
| for idx, val in enumerate(states[:rows * cols]): |
| z[idx // cols, idx % cols] = val |
|
|
| im = axes[1, 0].imshow(z, cmap='RdYlGn_r', aspect='auto', vmin=0, vmax=10) |
| axes[1, 0].set_title(f"МОНИТОРИНГ: {specialists_count} СПЕЦИАЛИСТОВ", fontsize=12, fontweight='bold') |
| axes[1, 0].axis('off') |
|
|
| |
| legend_text = "Цвета: Зеленый (Свободен) → Желтый (3-5 мин) → Красный (8+ мин)" |
| axes[1, 0].text(0.5, -0.1, legend_text, ha='center', transform=axes[1, 0].transAxes, fontsize=10) |
|
|
| |
| ax_stat = axes[1, 1] |
| ax_stat.clear() |
| ax_stat.axis('off') |
|
|
| |
| q_mod_color = '#991b1b' if data['queue'] > 50 else '#166534' |
| q_biz_color = '#991b1b' if data.get('business_queue', 0) > 50 else '#1e293b' |
|
|
| |
| ax_stat.text(0.25, 0.9, "ОЧЕРЕДЬ\n(МОДЕЛИ)", fontsize=10, ha='center', fontweight='bold') |
| ax_stat.text(0.25, 0.78, f"{data['queue']}", fontsize=26, ha='center', fontweight='bold', color=q_mod_color) |
|
|
| ax_stat.text(0.75, 0.9, "ОЧЕРЕДЬ\n(БИЗНЕС ПРАВИЛА)", fontsize=10, ha='center', fontweight='bold') |
| ax_stat.text(0.75, 0.78, f"{data.get('business_queue', 0)}", fontsize=26, ha='center', fontweight='bold', |
| color=q_biz_color) |
|
|
| |
| cum = data['cumulative'] |
| stats_text = ( |
| f"Итоговые показатели к {data['time_str']}\n" |
| f"--------------------------------------\n" |
| f"ОБРАБОТАНО ВСЕГО: {cum['total_processed']}\n" |
| f"Авто-одобрено: {cum['auto_approved']}\n" |
| f"Авто-отказы: {cum['auto_declined']}\n" |
| f"Ручной разбор (модель): {cum['manual_processed']}\n" |
| f"Ручной разбор (бизнес правила): {cum['business_manual_processed']}\n" |
| f"--------------------------------------\n" |
| f"Используемая модель: {second_model_name}" |
| ) |
|
|
| ax_stat.text(0.5, 0.3, stats_text, fontsize=10, fontfamily='monospace', |
| ha='center', va='center', transform=ax_stat.transAxes, |
| bbox=dict(facecolor='#f8f9fa', alpha=1, boxstyle='round,pad=1', edgecolor='#dee2e6')) |
|
|
| return axes.flatten() |
|
|
| ani = animation.FuncAnimation(fig, update, frames=len(frames), interval=1000 / fps) |
| tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') |
|
|
| writer = animation.FFMpegWriter(fps=fps, bitrate=2000, extra_args=['-vcodec', 'libx264', '-pix_fmt', 'yuv420p']) |
| ani.save(tmp_file.name, writer=writer) |
| return tmp_file.name |