Загрузка данных
import pandas as pd
import numpy as np
from openpyxl import load_workbook
from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter
# ---------------------------
# 1. Загрузка и предобработка
# ---------------------------
file_path = 'По группам продуктов ЗП (27.07.2026 23_49).xlsx' # укажите свой путь
df_raw = pd.read_excel(file_path, sheet_name='ДАННЫЕ', header=None, skiprows=3)
header_row_idx = df_raw[df_raw[0] == 'Номер кассовой смены'].index[0]
df = pd.read_excel(file_path, sheet_name='ДАННЫЕ', header=header_row_idx)
df.columns = df.columns.str.strip()
df = df[df['Продукт'].notna() & (df['Продукт'] != '')]
df['Кол-во проданного'] = pd.to_numeric(df['Кол-во проданного'], errors='coerce')
df['Выручка'] = pd.to_numeric(df['Выручка'], errors='coerce')
df = df[(df['Кол-во проданного'] > 0) & (df['Выручка'] > 0)]
# Нормализация названий заведений
df['Место реализации'] = df['Место реализации'].replace({
'Королев Ц6А': 'Королёв Ц6А',
'Королёв П7': 'Королёв П7',
'Балашиха': 'Балашиха'
})
# Дни недели
df['Дата'] = pd.to_datetime(df['Смена']).dt.date
df['День недели'] = pd.to_datetime(df['Смена']).dt.day_name()
days_map = {
'Monday': 'понедельник', 'Tuesday': 'вторник', 'Wednesday': 'среда',
'Thursday': 'четверг', 'Friday': 'пятница', 'Saturday': 'суббота', 'Sunday': 'воскресенье'
}
df['День недели'] = df['День недели'].map(days_map)
week_order = ['понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота', 'воскресенье']
df['День недели'] = pd.Categorical(df['День недели'], categories=week_order, ordered=True)
# Группировка по заведению, продукту, дню
df_agg = df.groupby(['Место реализации', 'Продукт', 'Дата', 'День недели'], as_index=False).agg({
'Кол-во проданного': 'sum',
'Выручка': 'sum'
})
# -------------------------------------------------
# 2. ABC-анализ для каждого заведения (по продуктам)
# -------------------------------------------------
def abc_analysis(group):
group = group.sort_values('Выручка', ascending=False)
total = group['Выручка'].sum()
group['cum_sum'] = group['Выручка'].cumsum()
group['cum_percent'] = group['cum_sum'] / total * 100
def assign(pct):
if pct <= 80:
return 'A'
elif pct <= 95:
return 'B'
else:
return 'C'
group['ABC'] = group['cum_percent'].apply(assign)
return group
abc_results = {}
for place in df_agg['Место реализации'].unique():
place_data = df_agg[df_agg['Место реализации'] == place]
product_sales = place_data.groupby('Продукт', as_index=False)['Выручка'].sum()
product_sales = abc_analysis(product_sales)
abc_results[place] = product_sales[['Продукт', 'ABC']]
# Добавляем категорию в основной датафрейм
df_agg = df_agg.merge(
pd.concat([df.assign(Заведение=place) for place, df in abc_results.items()], ignore_index=True),
left_on=['Место реализации', 'Продукт'],
right_on=['Заведение', 'Продукт'],
how='left'
).drop(columns=['Заведение'])
# ------------------------------------------------
# 3. Подготовка данных для Excel-таблицы (по дням)
# ------------------------------------------------
# Создаём сводную таблицу: строки - продукт, столбцы - день, значения - кол-во и сумма
def make_pivot(place):
data = df_agg[df_agg['Место реализации'] == place]
# Добавим категорию ABC для каждого продукта (одна на продукт)
product_abc = data[['Продукт', 'ABC']].drop_duplicates()
# Свод по количеству
pivot_qty = data.pivot(index='Продукт', columns='День недели', values='Кол-во проданного').fillna(0)
pivot_qty = pivot_qty.reindex(columns=week_order, fill_value=0)
# Свод по выручке
pivot_rev = data.pivot(index='Продукт', columns='День недели', values='Выручка').fillna(0)
pivot_rev = pivot_rev.reindex(columns=week_order, fill_value=0)
# Объединяем: для каждого дня две колонки (кол-во, сумма)
# Создаём мультииндекс для столбцов
cols = pd.MultiIndex.from_product([week_order, ['Кол-во', 'Сумма']])
final = pd.DataFrame(index=pivot_qty.index, columns=cols)
for day in week_order:
final[(day, 'Кол-во')] = pivot_qty[day]
final[(day, 'Сумма')] = pivot_rev[day]
# Добавляем столбец с категорией ABC (для цвета)
final['ABC'] = product_abc.set_index('Продукт')['ABC']
# Итоговая строка
total_row = {}
for day in week_order:
total_row[(day, 'Кол-во')] = final[(day, 'Кол-во')].sum()
total_row[(day, 'Сумма')] = final[(day, 'Сумма')].sum()
total_row['ABC'] = 'ИТОГО'
final.loc['ИТОГО'] = total_row
return final
# Создаём словарь листов
sheets = {}
for place in df_agg['Место реализации'].unique():
sheets[place] = make_pivot(place)
# Также создадим сводный лист с долей ABC по заведениям
abc_share = df_agg.groupby(['Место реализации', 'ABC']).size().unstack(fill_value=0)
abc_share_pct = abc_share.div(abc_share.sum(axis=1), axis=0) * 100
abc_share_pct = abc_share_pct.round(1)
abc_share_pct['Итого'] = abc_share.sum(axis=1)
sheets['Сводка_ABC'] = abc_share_pct
# ------------------------------------------------
# 4. Запись в Excel с форматированием (openpyxl)
# ------------------------------------------------
output_file = 'ABC_sales_dashboard.xlsx'
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
for sheet_name, df_sheet in sheets.items():
df_sheet.to_excel(writer, sheet_name=sheet_name, startrow=1)
# Теперь стилизуем через openpyxl
wb = load_workbook(output_file)
# Цвета для категорий
color_map = {
'A': PatternFill(start_color='2E8B57', end_color='2E8B57', fill_type='solid'), # зелёный
'B': PatternFill(start_color='DAA520', end_color='DAA520', fill_type='solid'), # жёлтый
'C': PatternFill(start_color='CD5C5C', end_color='CD5C5C', fill_type='solid'), # красный
'ИТОГО': PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid') # синий для итогов
}
# Стиль границ
thin_border = Border(
left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin')
)
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
# Пропускаем сводку (там другая структура)
if sheet_name == 'Сводка_ABC':
continue
# Удалим первую пустую строку (заголовок не нужен)
ws.delete_rows(1)
# Добавим заголовок заведения
ws.insert_rows(1)
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(ws[1]))
ws.cell(row=1, column=1, value=f'Заведение: {sheet_name} (продажи по дням недели)')
ws.cell(row=1, column=1).font = Font(size=16, bold=True)
ws.cell(row=1, column=1).alignment = Alignment(horizontal='center')
# Определим, где находится столбец ABC (последний)
abc_col_idx = None
for col in range(1, ws.max_column + 1):
if ws.cell(row=2, column=col).value == 'ABC':
abc_col_idx = col
break
# Теперь пройдём по строкам, начиная с 3 (первая строка - заголовок, вторая - подзаголовки дней)
for row in range(3, ws.max_row + 1):
# Проверим, что в ячейке ABC есть значение
abc_val = ws.cell(row=row, column=abc_col_idx).value
if abc_val is None:
continue
# Для ИТОГО используем синий цвет
if abc_val == 'ИТОГО':
fill = color_map['ИТОГО']
font = Font(bold=True, color='FFFFFF')
else:
fill = color_map.get(abc_val, PatternFill())
font = Font(bold=False)
# Окрашиваем все ячейки суммы (чётные столбцы, начиная с 2-го дня)
# Структура: столбцы: день1-кол-во, день1-сумма, день2-кол-во, день2-сумма, ..., ABC
# Индексы колонок: 1-й день кол-во = 2, сумма = 3, и т.д. (т.к. 1-й столбец - Продукт)
for col in range(2, abc_col_idx): # до столбца ABC
# Проверим, что это столбец с суммой (чётный порядковый номер в паре)
# Определим, является ли колонка "Сумма" по заголовку
header_val = ws.cell(row=2, column=col).value
if header_val == 'Сумма':
cell = ws.cell(row=row, column=col)
if cell.value and cell.value != 0:
cell.fill = fill
cell.font = font
cell.border = thin_border
# Также можно применить числовой формат
cell.number_format = '#,##0.00'
else:
# Для кол-ва просто границы
ws.cell(row=row, column=col).border = thin_border
# Окрасим ячейку с ABC (для информации)
ws.cell(row=row, column=abc_col_idx).fill = fill
ws.cell(row=row, column=abc_col_idx).font = font
ws.cell(row=row, column=abc_col_idx).border = thin_border
# Также оформим заголовки дней (строка 2) и названия продуктов (столбец 1)
for col in range(1, ws.max_column + 1):
ws.cell(row=2, column=col).font = Font(bold=True)
ws.cell(row=2, column=col).alignment = Alignment(horizontal='center')
ws.cell(row=2, column=col).border = thin_border
for row in range(3, ws.max_row + 1):
ws.cell(row=row, column=1).font = Font(bold=True)
ws.cell(row=row, column=1).border = thin_border
ws.cell(row=row, column=1).alignment = Alignment(horizontal='left')
# Автоширина колонок
for col in range(1, ws.max_column + 1):
col_letter = get_column_letter(col)
max_len = 0
for row in range(1, ws.max_row + 1):
val = ws.cell(row=row, column=col).value
if val:
max_len = max(max_len, len(str(val)))
ws.column_dimensions[col_letter].width = max_len + 2
# Заморозка панели (чтобы заголовки и продукты всегда были видны)
ws.freeze_panes = 'B3' # замораживаем строку 2 и столбец 1
# Оформление сводного листа "Сводка_ABC"
ws_sum = wb['Сводка_ABC']
# Удалим первую пустую строку
ws_sum.delete_rows(1)
# Добавим заголовок
ws_sum.insert_rows(1)
ws_sum.merge_cells(start_row=1, start_column=1, end_row=1, end_column=ws_sum.max_column)
ws_sum.cell(row=1, column=1, value='Сравнение долей ABC-категорий по заведениям')
ws_sum.cell(row=1, column=1).font = Font(size=16, bold=True)
ws_sum.cell(row=1, column=1).alignment = Alignment(horizontal='center')
# Оформим заголовки (строка 2)
for col in range(1, ws_sum.max_column + 1):
ws_sum.cell(row=2, column=col).font = Font(bold=True)
ws_sum.cell(row=2, column=col).alignment = Alignment(horizontal='center')
ws_sum.cell(row=2, column=col).border = thin_border
# Данные
for row in range(3, ws_sum.max_row + 1):
for col in range(1, ws_sum.max_column + 1):
ws_sum.cell(row=row, column=col).border = thin_border
if col == 1: # название заведения
continue
# Если это столбец с процентом A/B/C, окрасим соответственно
header = ws_sum.cell(row=2, column=col).value
if header in ['A', 'B', 'C']:
val = ws_sum.cell(row=row, column=col).value
if val:
ws_sum.cell(row=row, column=col).fill = color_map[header]
ws_sum.cell(row=row, column=col).number_format = '0.0"%"'
# Автоширина
for col in range(1, ws_sum.max_column + 1):
col_letter = get_column_letter(col)
max_len = 0
for row in range(1, ws_sum.max_row + 1):
val = ws_sum.cell(row=row, column=col).value
if val:
max_len = max(max_len, len(str(val)))
ws_sum.column_dimensions[col_letter].width = max_len + 2
wb.save(output_file)
print(f'Готово! Создан файл: {output_file}')
print(f'Листы: {", ".join(wb.sheetnames)}')