Калькулятор расчета метрики для Маркетинга, расчет статистики и аналитики по формулам.
Python:
Это будет довольно объёмный проект, поэтому я организую его в модульную структуру с папками для масштабирования и удобства поддержки. Код будет полностью рабочий, компилируемый в `.exe` для Windows с помощью `PyInstaller`.
---
### Структура проекта
Для масштабирования разобьём код на модули и папки:
```
DigitalMarketingMetrics/
│
├── src/ # Основной код приложения
│ ├── __init__.py # Пустой файл для обозначения модуля
│ ├── main.py # Точка входа (запуск приложения)
│ ├── ui.py # Интерфейс (виджеты, темы, стили)
│ ├── calculations.py # Логика расчётов метрик
│ ├── utils.py # Утилиты (сохранение, загрузка, подсказки)
│ └── charts.py # Визуализация графиков
│
├── assets/ # Ресурсы (иконки, стили)
│ ├── light_theme.json # Настройки светлой темы
│ ├── dark_theme.json # Настройки тёмной темы
│ └── icons/ # Папка для иконок (опционально)
│
└── requirements.txt # Зависимости для компиляции
```
---
### Полный код
#### `requirements.txt`
```
pyinstaller
matplotlib
```
Установите зависимости:
```
pip install -r requirements.txt
```
#### `src/main.py`
```python
import tkinter as tk
from src.ui import AppUI
if __name__ == "__main__":
root = tk.Tk()
app = AppUI(root)
root.mainloop()
```
#### `src/ui.py`
```python
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import json
import os
from src.calculations import calculate_metrics
from src.utils import save_data, load_data, ToolTip
from src.charts import show_chart
class AppUI:
def __init__(self, root):
self.root = root
self.root.title("Digital Marketing Metrics")
self.root.geometry("500x600")
self.root.minsize(400, 600)
self.root.maxsize(800, 10000)
self.entries = {}
self.current_theme = "light"
self.current_lang = "ru"
self.load_themes()
self.setup_ui()
def load_themes(self):
with open("assets/light_theme.json", "r") as f:
self.light_theme = json.load(f)
with open("assets/dark_theme.json", "r") as f:
self.dark_theme = json.load(f)
self.theme = self.light_theme
def apply_theme(self):
style = ttk.Style()
style.theme_use("clam")
for widget, config in self.theme["styles"].items():
style.configure(widget, **config)
if "map" in config:
style.map(widget, **config["map"])
self.root.configure(bg=self.theme["bg"])
self.canvas.configure(bg=self.theme["bg"])
self.result_text.configure(bg=self.theme["styles"]["TEntry"]["fieldbackground"], fg=self.theme["styles"]["TLabel"]["foreground"])
def switch_theme(self):
self.current_theme = "dark" if self.current_theme == "light" else "light"
self.theme = self.dark_theme if self.current_theme == "dark" else self.light_theme
self.apply_theme()
def switch_language(self, event):
self.current_lang = self.lang_var.get()
self.update_texts()
def setup_ui(self):
# Стили
self.apply_theme()
# Основной фрейм с прокруткой
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(fill="both", expand=True)
self.canvas = tk.Canvas(self.main_frame, highlightthickness=0)
self.v_scrollbar = ttk.Scrollbar(self.main_frame, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.v_scrollbar.set)
self.v_scrollbar.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
self.input_frame = ttk.Frame(self.canvas)
self.canvas_window = self.canvas.create_window((0, 0), window=self.input_frame, anchor="nw")
# Верхняя панель
top_frame = ttk.Frame(self.input_frame)
top_frame.grid(row=0, column=0, columnspan=4, pady=5, sticky="ew")
ttk.Button(top_frame, text="Тёмная тема", command=self.switch_theme).pack(side="left", padx=5)
self.lang_var = tk.StringVar(value="ru")
lang_menu = ttk.OptionMenu(top_frame, self.lang_var, "ru", "ru", "en", command=self.switch_language)
lang_menu.pack(side="left", padx=5)
# Формулы
self.formulas = {
"ru": [
("CTR (Кликабельность)", "Процент кликов от общего числа показов.", [
("Количество показов:", "entry_impressions", "Введите количество показов рекламы"),
("Количество кликов:", "entry_clicks", "Введите количество кликов по рекламе")
]),
("CPC (Стоимость за клик)", "Сколько стоит один клик на рекламу.", [
("Затраты на рекламу ($):", "entry_ad_spend", "Введите общие затраты на рекламу"),
("Количество кликов:", "entry_clicks_cpc", "Введите количество кликов")
]),
("CPA (Стоимость за конверсию)", "Стоимость одной конверсии (например, покупки).", [
("Затраты на рекламу ($):", "entry_ad_spend_cpa", "Введите общие затраты на рекламу"),
("Количество конверсий:", "entry_conversions", "Введите количество конверсий")
]),
("ROAS (Возврат затрат)", "Сколько дохода приносит каждый доллар затрат.", [
("Доход от рекламы ($):", "entry_revenue", "Введите доход от рекламы"),
("Затраты на рекламу ($):", "entry_ad_spend_roas", "Введите общие затраты на рекламу")
]),
("CR (Конверсия)", "Процент конверсий от числа кликов.", [
("Количество кликов:", "entry_clicks_cr", "Введите количество кликов"),
("Количество конверсий:", "entry_conversions_cr", "Введите количество конверсий")
]),
("LTV (Пожизненная ценность)", "Общий доход от клиента за всё время.", [
("Средний чек ($):", "entry_avg_order_value", "Введите средний чек клиента"),
("Покупок в год:", "entry_purchases_per_year", "Введите количество покупок в год"),
("Срок жизни клиента (лет):", "entry_customer_lifespan", "Введите срок жизни клиента в годах")
]),
("CPL (Cost Per Lead)", "Стоимость одного лида.", [
("Затраты на рекламу ($):", "entry_ad_spend_cpl", "Введите общие затраты на рекламу"),
("Количество лидов:", "entry_leads", "Введите количество лидов")
]),
("RPM (Revenue Per Mille)", "Доход на тысячу показов.", [
("Доход от рекламы ($):", "entry_revenue_rpm", "Введите доход от рекламы"),
("Количество показов:", "entry_impressions_rpm", "Введите количество показов")
])
],
"en": [
("CTR (Click-Through Rate)", "Percentage of clicks from total impressions.", [
("Impressions:", "entry_impressions", "Enter the number of ad impressions"),
("Clicks:", "entry_clicks", "Enter the number of ad clicks")
]),
("CPC (Cost Per Click)", "Cost of one click on an ad.", [
("Ad Spend ($):", "entry_ad_spend", "Enter total ad spend"),
("Clicks:", "entry_clicks_cpc", "Enter the number of clicks")
]),
("CPA (Cost Per Acquisition)", "Cost of one conversion (e.g., purchase).", [
("Ad Spend ($):", "entry_ad_spend_cpa", "Enter total ad spend"),
("Conversions:", "entry_conversions", "Enter the number of conversions")
]),
("ROAS (Return On Ad Spend)", "Revenue per dollar spent on ads.", [
("Revenue ($):", "entry_revenue", "Enter revenue from ads"),
("Ad Spend ($):", "entry_ad_spend_roas", "Enter total ad spend")
]),
("CR (Conversion Rate)", "Percentage of conversions from clicks.", [
("Clicks:", "entry_clicks_cr", "Enter the number of clicks"),
("Conversions:", "entry_conversions_cr", "Enter the number of conversions")
]),
("LTV (Lifetime Value)", "Total revenue from a customer over time.", [
("Average Order Value ($):", "entry_avg_order_value", "Enter average customer order value"),
("Purchases Per Year:", "entry_purchases_per_year", "Enter number of purchases per year"),
("Customer Lifespan (years):", "entry_customer_lifespan", "Enter customer lifespan in years")
]),
("CPL (Cost Per Lead)", "Cost of one lead.", [
("Ad Spend ($):", "entry_ad_spend_cpl", "Enter total ad spend"),
("Leads:", "entry_leads", "Enter the number of leads")
]),
("RPM (Revenue Per Mille)", "Revenue per thousand impressions.", [
("Revenue ($):", "entry_revenue_rpm", "Enter revenue from ads"),
("Impressions:", "entry_impressions_rpm", "Enter the number of impressions")
])
]
}
row = 1
for formula_title, formula_desc, fields in self.formulas[self.current_lang]:
ttk.Label(self.input_frame, text=formula_title, font=("Helvetica", 12, "bold")).grid(row=row, column=0, columnspan=4, pady=(10, 2), sticky="w")
row += 1
ttk.Label(self.input_frame, text=formula_desc, font=("Helvetica", 9, "italic")).grid(row=row, column=0, columnspan=4, pady=2, sticky="w")
row += 1
ttk.Label(self.input_frame, text=fields[0][0]).grid(row=row, column=0, padx=5, pady=2, sticky="w")
entry1 = ttk.Entry(self.input_frame)
entry1.grid(row=row, column=1, padx=5, pady=2, sticky="ew")
ToolTip(entry1, fields[0][2])
self.entries[fields[0][1]] = entry1
ttk.Label(self.input_frame, text=fields[1][0]).grid(row=row, column=2, padx=5, pady=2, sticky="w")
entry2 = ttk.Entry(self.input_frame)
entry2.grid(row=row, column=3, padx=5, pady=2, sticky="ew")
ToolTip(entry2, fields[1][2])
self.entries[fields[1][1]] = entry2
if len(fields) > 2:
row += 1
ttk.Label(self.input_frame, text=fields[2][0]).grid(row=row, column=0, padx=5, pady=2, sticky="w")
entry3 = ttk.Entry(self.input_frame)
entry3.grid(row=row, column=1, padx=5, pady=2, sticky="ew")
ToolTip(entry3, fields[2][2])
self.entries[fields[2][1]] = entry3
row += 1
ttk.Separator(self.input_frame, orient="horizontal").grid(row=row, column=0, columnspan=4, sticky="ew", pady=5)
row += 1
self.input_frame.grid_columnconfigure(1, weight=1)
self.input_frame.grid_columnconfigure(3, weight=1)
# Кнопки управления
button_frame = ttk.Frame(self.input_frame)
button_frame.grid(row=row, column=0, columnspan=4, pady=15)
ttk.Button(button_frame, text="Рассчитать", command=self.calculate).pack(side="left", padx=5)
ttk.Button(button_frame, text="Очистить", command=self.clear).pack(side="left", padx=5)
# Прокрутка
self.canvas.bind_all("<MouseWheel>", self.scroll_canvas)
self.canvas.bind("<Configure>", self.configure_canvas)
self.input_frame.update_idletasks()
self.canvas.config(scrollregion=self.canvas.bbox("all"))
# Результаты
self.result_frame = ttk.Frame(self.root)
self.result_frame.pack(fill="both", expand=True, padx=10, pady=10)
ttk.Label(self.result_frame, text="Результаты:" if self.current_lang == "ru" else "Results:", font=("Helvetica", 12, "bold")).pack(anchor="w")
self.result_text = scrolledtext.ScrolledText(self.result_frame, height=15, wrap=tk.WORD, font=("Helvetica", 10), bd=0, relief="flat")
self.result_text.pack(fill="both", expand=True)
self.result_frame.configure(height=250)
self.result_frame.pack_propagate(False)
self.result_text.bind("<MouseWheel>", self.scroll_result)
# Нижняя панель
bottom_frame = ttk.Frame(self.result_frame)
bottom_frame.pack(fill="x", pady=5)
ttk.Button(bottom_frame, text="Сохранить в файл" if self.current_lang == "ru" else "Save to File", command=self.save).pack(side="left", padx=5)
ttk.Button(bottom_frame, text="Загрузить из файла" if self.current_lang == "ru" else "Load from File", command=self.load).pack(side="left", padx=5)
self.status_label = ttk.Label(bottom_frame, text="")
self.status_label.pack(side="left", padx=5)
# Адаптивность
self.root.grid_rowconfigure(0, weight=1)
self.root.grid_columnconfigure(0, weight=1)
self.main_frame.grid_rowconfigure(0, weight=1)
self.main_frame.grid_columnconfigure(0, weight=1)
self.result_frame.grid_rowconfigure(1, weight=1)
self.result_frame.grid_columnconfigure(0, weight=1)
def scroll_canvas(self, event):
if event.delta > 0:
self.canvas.yview_scroll(-1, "units")
else:
self.canvas.yview_scroll(1, "units")
def configure_canvas(self, event):
self.canvas.itemconfig(self.canvas_window, width=min(event.width, 800))
self.canvas.config(scrollregion=self.canvas.bbox("all"))
def scroll_result(self, event):
if event.delta > 0:
self.result_text.yview_scroll(-1, "units")
else:
self.result_text.yview_scroll(1, "units")
def calculate(self):
calculate_metrics(self.entries, self.result_text)
show_chart(self.entries, self.root)
from datetime import datetime
self.status_label.config(text=f"Последний расчёт: {datetime.now().strftime('%d.%m.%Y %H:%M')}" if self.current_lang == "ru" else f"Last calculation: {datetime.now().strftime('%d.%m.%Y %H:%M')}")
def clear(self):
for entry in self.entries.values():
entry.delete(0, tk.END)
self.result_text.delete(1.0, tk.END)
self.status_label.config(text="Все поля очищены" if self.current_lang == "ru" else "All fields cleared")
def save(self):
data = {key: entry.get() for key, entry in self.entries.items()}
data["results"] = self.result_text.get(1.0, tk.END).strip()
save_data(data)
self.status_label.config(text="Данные сохранены" if self.current_lang == "ru" else "Data saved")
def load(self):
data = load_data()
if data:
for key, value in data.items():
if key in self.entries:
self.entries[key].delete(0, tk.END)
self.entries[key].insert(0, value)
elif key == "results":
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, value)
self.status_label.config(text="Данные загружены" if self.current_lang == "ru" else "Data loaded")
def update_texts(self):
# Обновление текста интерфейса при смене языка
self.root.destroy()
self.root = tk.Tk()
self.entries = {}
self.setup_ui()
```
#### `src/calculations.py`
```python
def calculate_metrics(entries, result_text):
result_text.delete(1.0, "end")
def get_float_or_none(entry):
value = entry.get().strip()
if value:
try:
val = float(value)
if val < 0:
raise ValueError
return val
except ValueError:
return None
return None
impressions = get_float_or_none(entries["entry_impressions"])
clicks = get_float_or_none(entries["entry_clicks"])
clicks_cpc = get_float_or_none(entries["entry_clicks_cpc"])
clicks_cr = get_float_or_none(entries["entry_clicks_cr"])
conversions = get_float_or_none(entries["entry_conversions"])
conversions_cr = get_float_or_none(entries["entry_conversions_cr"])
ad_spend = get_float_or_none(entries["entry_ad_spend"])
ad_spend_cpa = get_float_or_none(entries["entry_ad_spend_cpa"])
ad_spend_roas = get_float_or_none(entries["entry_ad_spend_roas"])
revenue = get_float_or_none(entries["entry_revenue"])
avg_order_value = get_float_or_none(entries["entry_avg_order_value"])
purchases_per_year = get_float_or_none(entries["entry_purchases_per_year"])
customer_lifespan = get_float_or_none(entries["entry_customer_lifespan"])
ad_spend_cpl = get_float_or_none(entries["entry_ad_spend_cpl"])
leads = get_float_or_none(entries["entry_leads"])
revenue_rpm = get_float_or_none(entries["entry_revenue_rpm"])
impressions_rpm = get_float_or_none(entries["entry_impressions_rpm"])
calculated = False
if impressions is not None and clicks is not None:
if impressions == 0:
result_text.insert("end", "CTR: Ошибка — показы не могут быть 0\n")
elif clicks > impressions:
result_text.insert("end", "CTR: Предупреждение — кликов больше показов\n")
else:
ctr = (clicks / impressions) * 100
result_text.insert("end", f"CTR (Кликабельность): {ctr:.2f}%\n")
calculated = True
if ad_spend is not None and clicks_cpc is not None:
if clicks_cpc == 0:
result_text.insert("end", "CPC: Ошибка — клики не могут быть 0\n")
else:
cpc = ad_spend / clicks_cpc
result_text.insert("end", f"CPC (Стоимость за клик): ${cpc:.2f}\n")
calculated = True
if ad_spend_cpa is not None and conversions is not None:
if conversions == 0:
result_text.insert("end", "CPA: Ошибка — конверсии не могут быть 0\n")
else:
cpa = ad_spend_cpa / conversions
result_text.insert("end", f"CPA (Стоимость за конверсию): ${cpa:.2f}\n")
calculated = True
if revenue is not None and ad_spend_roas is not None:
if ad_spend_roas == 0:
result_text.insert("end", "ROAS: Ошибка — затраты не могут быть 0\n")
else:
roas = revenue / ad_spend_roas
result_text.insert("end", f"ROAS (Возврат затрат): {roas:.2f}x\n")
if roas < 1:
result_text.insert("end", "Предупреждение: ROAS < 1 — кампания убыточна\n")
calculated = True
if clicks_cr is not None and conversions_cr is not None:
if clicks_cr == 0:
result_text.insert("end", "CR: Ошибка — клики не могут быть 0\n")
elif conversions_cr > clicks_cr:
result_text.insert("end", "CR: Предупреждение — конверсий больше кликов\n")
else:
cr = (conversions_cr / clicks_cr) * 100
result_text.insert("end", f"CR (Конверсия): {cr:.2f}%\n")
calculated = True
if avg_order_value is not None and purchases_per_year is not None and customer_lifespan is not None:
ltv = avg_order_value * purchases_per_year * customer_lifespan
result_text.insert("end", f"LTV (Пожизненная ценность): ${ltv:.2f}\n")
calculated = True
if ad_spend_cpl is not None and leads is not None:
if leads == 0:
result_text.insert("end", "CPL: Ошибка — лиды не могут быть 0\n")
else:
cpl = ad_spend_cpl / leads
result_text.insert("end", f"CPL (Стоимость за лид): ${cpl:.2f}\n")
calculated = True
if revenue_rpm is not None and impressions_rpm is not None:
if impressions_rpm == 0:
result_text.insert("end", "RPM: Ошибка — показы не могут быть 0\n")
else:
rpm = (revenue_rpm / impressions_rpm) * 1000
result_text.insert("end", f"RPM (Доход на тысячу показов): ${rpm:.2f}\n")
calculated = True
if not calculated:
result_text.insert("end", "Введите данные хотя бы для одной метрики!\n")
```
#### `src/utils.py`
```python
import tkinter as tk
import json
import os
class ToolTip:
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tooltip = None
self.widget.bind("<Enter>", self.show_tooltip)
self.widget.bind("<Leave>", self.hide_tooltip)
def show_tooltip(self, event):
x, y, _, _ = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 25
self.tooltip = tk.Toplevel(self.widget)
self.tooltip.wm_overrideredirect(True)
self.tooltip.wm_geometry(f"+{x}+{y}")
label = tk.Label(self.tooltip, text=self.text, bg="#ffffe0", fg="black", relief="solid", borderwidth=1)
label.pack()
def hide_tooltip(self, event):
if self.tooltip:
self.tooltip.destroy()
self.tooltip = None
def save_data(data):
with open("metrics_data.json", "w") as f:
json.dump(data, f)
def load_data():
if os.path.exists("metrics_data.json"):
with open("metrics_data.json", "r") as f:
return json.load(f)
return None
```
#### `src/charts.py`
```python
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
def show_chart(entries, root):
metrics = {}
for entry_name, entry in entries.items():
value = entry.get().strip()
if value:
try:
metrics[entry_name] = float(value)
except ValueError:
pass
if not metrics:
return
fig, ax = plt.subplots()
labels = []
values = []
for label, value in metrics.items():
labels.append(label.replace("entry_", ""))
values.append(value)
ax.bar(labels, values)
ax.set_title("Metrics Overview")
plt.xticks(rotation=45)
chart_window = tk.Toplevel(root)
chart_window.title("Metrics Chart")
canvas = FigureCanvasTkAgg(fig, master=chart_window)
canvas.draw()
canvas.get_tk_widget().pack(fill="both", expand=True)
```
#### `assets/light_theme.json`
```json
{
"bg": "#f0f2f5",
"styles": {
"TEntry": {
"fieldbackground": "#ffffff",
"foreground": "#333333",
"borderwidth": 0,
"relief": "flat",
"padding": 5,
"font": ["Helvetica", 10],
"map": {
"fieldbackground": [["focus", "#e8f0fe"]]
}
},
"TButton": {
"background": "#4285f4",
"foreground": "#ffffff",
"borderwidth": 0,
"font": ["Helvetica", 10, "bold"],
"padding": 8,
"map": {
"background": [["active", "#3267d6"]]
}
},
"TLabel": {
"background": "#f0f2f5",
"foreground": "#333333",
"font": ["Helvetica", 10]
},
"TSeparator": {
"background": "#cccccc"
}
}
}
```
#### `assets/dark_theme.json`
```json
{
"bg": "#2d2d2d",
"styles": {
"TEntry": {
"fieldbackground": "#404040",
"foreground": "#ffffff",
"borderwidth": 0,
"relief": "flat",
"padding": 5,
"font": ["Helvetica", 10],
"map": {
"fieldbackground": [["focus", "#505050"]]
}
},
"TButton": {
"background": "#4285f4",
"foreground": "#ffffff",
"borderwidth": 0,
"font": ["Helvetica", 10, "bold"],
"padding": 8,
"map": {
"background": [["active", "#3267d6"]]
}
},
"TLabel": {
"background": "#2d2d2d",
"foreground": "#ffffff",
"font": ["Helvetica", 10]
},
"TSeparator": {
"background": "#666666"
}
}
}
```
---
### Как собрать и запустить
1. **Создайте структуру проекта**:
- Создайте папку `DigitalMarketingMetrics`.
- Внутри создайте папки `src` и `assets`.
- Скопируйте файлы в соответствующие папки как указано выше.
2. **Установите зависимости**:
```
pip install pyinstaller matplotlib
```
3. **Запустите приложение**:
```
cd DigitalMarketingMetrics/src
python main.py
```
4. **Скомпилируйте в .exe**:
```
cd DigitalMarketingMetrics
pyinstaller --onefile --add-data "assets;assets" src/main.py
```
- Флаг `--add-data "assets;assets"` добавляет папку `assets` в сборку.
- Используйте `;` для Windows. Для других ОС замените на `:`.
- Исполняемый файл появится в папке `dist`.
5. **Запустите .exe**:
- Найдите `main.exe` в `dist` и запустите.
---
### Что реализовано
#### Интерфейс:
- Тёмная/светлая тема с переключателем.
- Подсветка активных полей (фокус меняет цвет фона).
- Разделительные линии между формулами.
- Иконки не добавлены (нужны файлы .png, могу предложить позже).
- Анимация кнопки через `map` (цвет при нажатии).
- Статусная строка с датой расчёта и сообщениями.
#### Функциональность:
- Сохранение/загрузка данных в JSON.
- Кнопка "Очистить".
- Подсказки для полей (tooltips).
- Расширенные расчёты (CPL, RPM) с проверкой связности и предупреждениями.
- Графики через `matplotlib` в отдельном окне.
- Мультиязычность (RU/EN).
---
### Пример интерфейса
```
[Тёмная тема] [Язык: RU/EN]
[CTR (Кликабельность) ]
[Процент кликов от общего числа... ]
[Количество показов: [____] ] | [Количество кликов: [____] ]
----------------------------------------
[...и так далее... ]
[Рассчитать] [Очистить]
[Результаты: ] (мин. высота 250px)
[CTR (Кликабельность): 5.00% ]
[CPC (Стоимость за клик): $0.20 ]
[Сохранить в файл] [Загрузить из файла] [Последний расчёт: 08.03.2025 14:30]
```
---
### Примечания
- Для иконок нужно добавить файлы в `assets/icons/` и обновить код.
- Графики простые (столбчатые), можно улучшить (например, цветовая кодировка).
Если всё работает, скажите, что ещё улучшить! Если есть ошибки, сообщите детали, и я исправлю.