Spaces:
Runtime error
Runtime error
import pandas as pd | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import seaborn as sns | |
from datetime import datetime, timedelta | |
import logging | |
logger = logging.getLogger(__name__) | |
class JiraVisualizer: | |
""" | |
Клас для створення візуалізацій даних Jira | |
""" | |
def __init__(self, df): | |
""" | |
Ініціалізація візуалізатора. | |
Args: | |
df (pandas.DataFrame): DataFrame з даними Jira | |
""" | |
self.df = df | |
self._setup_plot_style() | |
def _setup_plot_style(self): | |
""" | |
Налаштування стилю візуалізацій. | |
""" | |
plt.style.use('ggplot') | |
sns.set(style="whitegrid") | |
# Налаштування для українських символів | |
plt.rcParams['font.family'] = 'DejaVu Sans' | |
def plot_status_counts(self): | |
""" | |
Створення діаграми розподілу тікетів за статусами. | |
Returns: | |
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки | |
""" | |
try: | |
if 'Status' not in self.df.columns: | |
logger.warning("Колонка 'Status' відсутня") | |
return None | |
status_counts = self.df['Status'].value_counts() | |
# Створення діаграми | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
# Спроба впорядкувати статуси логічно | |
try: | |
status_order = ['To Do', 'In Progress', 'In Review', 'Done', 'Closed'] | |
available_statuses = [s for s in status_order if s in status_counts.index] | |
other_statuses = [s for s in status_counts.index if s not in status_order] | |
ordered_statuses = available_statuses + other_statuses | |
status_counts = status_counts.reindex(ordered_statuses) | |
except: | |
pass | |
bars = sns.barplot(x=status_counts.index, y=status_counts.values, ax=ax) | |
# Додаємо підписи значень над стовпцями | |
for i, v in enumerate(status_counts.values): | |
ax.text(i, v + 0.5, str(v), ha='center') | |
ax.set_title('Розподіл тікетів за статусами') | |
ax.set_xlabel('Статус') | |
ax.set_ylabel('Кількість') | |
plt.xticks(rotation=45) | |
plt.tight_layout() | |
logger.info("Діаграма статусів успішно створена") | |
return fig | |
except Exception as e: | |
logger.error(f"Помилка при створенні діаграми статусів: {e}") | |
return None | |
def plot_priority_counts(self): | |
""" | |
Створення діаграми розподілу тікетів за пріоритетами. | |
Returns: | |
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки | |
""" | |
try: | |
if 'Priority' not in self.df.columns: | |
logger.warning("Колонка 'Priority' відсутня") | |
return None | |
priority_counts = self.df['Priority'].value_counts() | |
# Створення діаграми | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
# Спроба впорядкувати пріоритети логічно | |
try: | |
priority_order = ['Highest', 'High', 'Medium', 'Low', 'Lowest'] | |
available_priorities = [p for p in priority_order if p in priority_counts.index] | |
other_priorities = [p for p in priority_counts.index if p not in priority_order] | |
ordered_priorities = available_priorities + other_priorities | |
priority_counts = priority_counts.reindex(ordered_priorities) | |
except: | |
pass | |
# Кольори для різних пріоритетів | |
colors = ['#FF5555', '#FF9C5A', '#FFCC5A', '#5AFF96', '#5AC8FF'] | |
if len(priority_counts) <= len(colors): | |
bars = sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax, palette=colors[:len(priority_counts)]) | |
else: | |
bars = sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax) | |
# Додаємо підписи значень над стовпцями | |
for i, v in enumerate(priority_counts.values): | |
ax.text(i, v + 0.5, str(v), ha='center') | |
ax.set_title('Розподіл тікетів за пріоритетами') | |
ax.set_xlabel('Пріоритет') | |
ax.set_ylabel('Кількість') | |
plt.xticks(rotation=45) | |
plt.tight_layout() | |
logger.info("Діаграма пріоритетів успішно створена") | |
return fig | |
except Exception as e: | |
logger.error(f"Помилка при створенні діаграми пріоритетів: {e}") | |
return None | |
def plot_type_counts(self): | |
""" | |
Створення діаграми розподілу тікетів за типами. | |
Returns: | |
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки | |
""" | |
try: | |
if 'Issue Type' not in self.df.columns: | |
logger.warning("Колонка 'Issue Type' відсутня") | |
return None | |
type_counts = self.df['Issue Type'].value_counts() | |
# Створення діаграми | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
bars = sns.barplot(x=type_counts.index, y=type_counts.values, ax=ax) | |
# Додаємо підписи значень над стовпцями | |
for i, v in enumerate(type_counts.values): | |
ax.text(i, v + 0.5, str(v), ha='center') | |
ax.set_title('Розподіл тікетів за типами') | |
ax.set_xlabel('Тип') | |
ax.set_ylabel('Кількість') | |
plt.xticks(rotation=45) | |
plt.tight_layout() | |
logger.info("Діаграма типів успішно створена") | |
return fig | |
except Exception as e: | |
logger.error(f"Помилка при створенні діаграми типів: {e}") | |
return None | |
def plot_created_timeline(self): | |
""" | |
Створення часової діаграми створення тікетів. | |
Returns: | |
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки | |
""" | |
try: | |
if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']): | |
logger.warning("Колонка 'Created' відсутня або не містить дат") | |
return None | |
# Додаємо колонку з датою створення (без часу) | |
if 'Created_Date' not in self.df.columns: | |
self.df['Created_Date'] = self.df['Created'].dt.date | |
# Кількість створених тікетів за датами | |
created_by_date = self.df['Created_Date'].value_counts().sort_index() | |
# Створення діаграми | |
fig, ax = plt.subplots(figsize=(12, 6)) | |
created_by_date.plot(kind='line', marker='o', ax=ax) | |
ax.set_title('Кількість створених тікетів за датами') | |
ax.set_xlabel('Дата') | |
ax.set_ylabel('Кількість') | |
ax.grid(True) | |
plt.tight_layout() | |
logger.info("Часова діаграма успішно створена") | |
return fig | |
except Exception as e: | |
logger.error(f"Помилка при створенні часової діаграми: {e}") | |
return None | |
def plot_inactive_issues(self, days=14): | |
""" | |
Створення діаграми неактивних тікетів. | |
Args: | |
days (int): Кількість днів неактивності | |
Returns: | |
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки | |
""" | |
try: | |
if 'Updated' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Updated']): | |
logger.warning("Колонка 'Updated' відсутня або не містить дат") | |
return None | |
# Визначення неактивних тікетів | |
cutoff_date = datetime.now() - timedelta(days=days) | |
inactive_issues = self.df[self.df['Updated'] < cutoff_date] | |
if len(inactive_issues) == 0: | |
logger.warning("Немає неактивних тікетів для візуалізації") | |
return None | |
# Розподіл неактивних тікетів за статусами | |
if 'Status' in inactive_issues.columns: | |
inactive_by_status = inactive_issues['Status'].value_counts() | |
# Створення діаграми | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
bars = sns.barplot(x=inactive_by_status.index, y=inactive_by_status.values, ax=ax) | |
# Додаємо підписи значень над стовпцями | |
for i, v in enumerate(inactive_by_status.values): | |
ax.text(i, v + 0.5, str(v), ha='center') | |
ax.set_title(f'Розподіл неактивних тікетів за статусами (>{days} днів)') | |
ax.set_xlabel('Статус') | |
ax.set_ylabel('Кількість') | |
plt.xticks(rotation=45) | |
plt.tight_layout() | |
logger.info("Діаграма неактивних тікетів успішно створена") | |
return fig | |
else: | |
logger.warning("Колонка 'Status' відсутня для неактивних тікетів") | |
return None | |
except Exception as e: | |
logger.error(f"Помилка при створенні діаграми неактивних тікетів: {e}") | |
return None | |
def plot_status_timeline(self, timeline_df=None): | |
""" | |
Створення діаграми зміни статусів з часом. | |
Args: | |
timeline_df (pandas.DataFrame): DataFrame з часовими даними або None для автоматичного генерування | |
Returns: | |
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки | |
""" | |
try: | |
if timeline_df is None: | |
if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']): | |
logger.warning("Колонка 'Created' відсутня або не містить дат") | |
return None | |
if 'Updated' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Updated']): | |
logger.warning("Колонка 'Updated' відсутня або не містить дат") | |
return None | |
# Визначення часового діапазону | |
min_date = self.df['Created'].min().date() | |
max_date = self.df['Updated'].max().date() | |
# Створення часового ряду для кожного дня | |
date_range = pd.date_range(start=min_date, end=max_date, freq='D') | |
# Збір статистики для кожної дати | |
timeline_data = [] | |
for date in date_range: | |
date_str = date.strftime('%Y-%m-%d') | |
# Тікети, створені до цієї дати | |
created_until = self.df[self.df['Created'].dt.date <= date.date()] | |
# Статуси тікетів на цю дату | |
status_counts = {} | |
# Для кожного тікета визначаємо його статус на цю дату | |
for _, row in created_until.iterrows(): | |
# Якщо тікет був оновлений після цієї дати, використовуємо його поточний статус | |
if row['Updated'].date() >= date.date(): | |
status = row.get('Status', 'Unknown') | |
status_counts[status] = status_counts.get(status, 0) + 1 | |
# Додаємо запис для цієї дати | |
timeline_data.append({ | |
'Date': date_str, | |
'Total': len(created_until), | |
**status_counts | |
}) | |
# Створення DataFrame | |
timeline_df = pd.DataFrame(timeline_data) | |
# Конвертація Date до datetime | |
timeline_df['Date'] = pd.to_datetime(timeline_df['Date']) | |
else: | |
# Конвертація Date до datetime, якщо потрібно | |
if not pd.api.types.is_datetime64_dtype(timeline_df['Date']): | |
timeline_df['Date'] = pd.to_datetime(timeline_df['Date']) | |
# Отримання статусів (всі колонки, крім Date і Total) | |
status_columns = [col for col in timeline_df.columns if col not in ['Date', 'Total']] | |
if not status_columns: | |
logger.warning("Немає даних про статуси для візуалізації") | |
return None | |
# Створення діаграми | |
fig, ax = plt.subplots(figsize=(14, 8)) | |
# Створення сетплоту для статусів | |
status_data = timeline_df[['Date'] + status_columns].set_index('Date') | |
status_data.plot.area(ax=ax, stacked=True, alpha=0.7) | |
ax.set_title('Зміна статусів тікетів з часом') | |
ax.set_xlabel('Дата') | |
ax.set_ylabel('Кількість тікетів') | |
ax.grid(True) | |
plt.tight_layout() | |
logger.info("Часова діаграма статусів успішно створена") | |
return fig | |
except Exception as e: | |
logger.error(f"Помилка при створенні часової діаграми статусів: {e}") | |
return None | |
def plot_lead_time_by_type(self): | |
""" | |
Створення діаграми часу виконання за типами тікетів. | |
Returns: | |
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки | |
""" | |
try: | |
if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']): | |
logger.warning("Колонка 'Created' відсутня або не містить дат") | |
return None | |
if 'Resolved' not in self.df.columns: | |
logger.warning("Колонка 'Resolved' відсутня") | |
return None | |
if 'Issue Type' not in self.df.columns: | |
logger.warning("Колонка 'Issue Type' відсутня") | |
return None | |
# Конвертація колонки Resolved до datetime, якщо потрібно | |
if not pd.api.types.is_datetime64_dtype(self.df['Resolved']): | |
self.df['Resolved'] = pd.to_datetime(self.df['Resolved'], errors='coerce') | |
# Фільтрація завершених тікетів | |
completed_issues = self.df.dropna(subset=['Resolved']) | |
if len(completed_issues) == 0: | |
logger.warning("Немає завершених тікетів для аналізу") | |
return None | |
# Обчислення Lead Time (в днях) | |
completed_issues['Lead_Time_Days'] = (completed_issues['Resolved'] - completed_issues['Created']).dt.days | |
# Фільтрація некоректних значень | |
valid_lead_time = completed_issues[completed_issues['Lead_Time_Days'] >= 0] | |
if len(valid_lead_time) == 0: | |
logger.warning("Немає валідних даних про час виконання") | |
return None | |
# Обчислення середнього часу виконання за типами | |
lead_time_by_type = valid_lead_time.groupby('Issue Type')['Lead_Time_Days'].mean() | |
# Створення діаграми | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
bars = sns.barplot(x=lead_time_by_type.index, y=lead_time_by_type.values, ax=ax) | |
# Додаємо підписи значень над стовпцями | |
for i, v in enumerate(lead_time_by_type.values): | |
ax.text(i, v + 0.5, f"{v:.1f}", ha='center') | |
ax.set_title('Середній час виконання тікетів за типами (дні)') | |
ax.set_xlabel('Тип') | |
ax.set_ylabel('Дні') | |
plt.xticks(rotation=45) | |
plt.tight_layout() | |
logger.info("Діаграма часу виконання успішно створена") | |
return fig | |
except Exception as e: | |
logger.error(f"Помилка при створенні діаграми часу виконання: {e}") | |
return None | |
def plot_all(self, output_dir=None): | |
""" | |
Створення та збереження всіх діаграм. | |
Args: | |
output_dir (str): Директорія для збереження діаграм. | |
Якщо None, діаграми не зберігаються. | |
Returns: | |
dict: Словник з об'єктами figure для всіх діаграм | |
""" | |
plots = {} | |
# Створення діаграм | |
plots['status'] = self.plot_status_counts() | |
plots['priority'] = self.plot_priority_counts() | |
plots['type'] = self.plot_type_counts() | |
plots['created_timeline'] = self.plot_created_timeline() | |
plots['inactive'] = self.plot_inactive_issues() | |
plots['status_timeline'] = self.plot_status_timeline() | |
plots['lead_time'] = self.plot_lead_time_by_type() | |
# Збереження діаграм, якщо вказана директорія | |
if output_dir: | |
import os | |
from pathlib import Path | |
# Створення директорії, якщо вона не існує | |
output_path = Path(output_dir) | |
output_path.mkdir(exist_ok=True, parents=True) | |
# Збереження кожної діаграми | |
for name, fig in plots.items(): | |
if fig: | |
fig_path = output_path / f"{name}.png" | |
fig.savefig(fig_path, dpi=300) | |
logger.info(f"Діаграма {name} збережена у {fig_path}") | |
return plots |