Дата генерации: 2025-10-06 15:40:15
Молодой российский бизнесмен предполагает построить ночную дискотеку неподалеку от университета. Существует два варианта: открыть дискотеку со столовой (дневное обслуживание) или без столовой.
| Альтернатива | Выигрыш (благоприятный) | Выигрыш (неблагоприятный) |
|---|---|---|
| Дискотека со столовой | 250000 | -55000 |
| Дискотека без столовой | 175000 | -20000 |
Вычисляется ожидаемая денежная оценка (ОДО) по формуле:
ОДО = P(благоприятный) * Выигрыш_благоприятный + P(неблагоприятный) * Выигрыш_неблагоприятный
Далее выбирается альтернатива с максимальным значением ОДО (критерий объективиста).
Оптимальная альтернатива: Дискотека со столовой (максимальная ОДО = 97500.00 руб.)
results/payoff_table.csvresults/decision_tree.pngresults/decision_report.docxresults/analysis.logУстановите зависимости:
pip install graphviz matplotlib python-docx
apt-get install graphviz
Заметьте: для корректной работы модуля graphviz необходим системный пакет Graphviz (dot). Если он недоступен, скрипт попытается сгенерировать запасной рисунок через matplotlib.
Запустите скрипт:
python decision_tree_analysis.py
На основании критерия максимальной ожидаемой стоимостной ценности рекомендуется открыть: Дискотека со столовой.
Все расчёты произведены в рублях; исходные значения приводились в тыс. рублей и были переведены в рубли.
Ниже приведён полный исходный код скрипта decision_tree_analysis.py.
# decision_tree_analysis.py
# -*- coding: utf-8 -*-
"""
Анализ задачи принятия решений методом дерева решений (Вариант 1)
Автор: автогенерация
Дата: автоматически при запуске
Скрипт:
- создаёт структуру данных для альтернатив
- вычисляет ожидаемую денежную оценку (ОДО)
- сохраняет таблицу выигрышей в CSV (results/payoff_table.csv)
- визуализирует дерево решений (results/decision_tree.png)
- автоматически генерирует README.md (включая код скрипта)
- автоматически генерирует отчет в формате DOCX (results/decision_report.docx) (включая код скрипта)
- выводит пошаговые расчёты в консоль
"""
import os
import csv
import logging
import datetime
import sys
from dataclasses import dataclass
from typing import List, Tuple
# Попытка импортировать graphviz; если не установлен, используем matplotlib как запасной вариант
try:
from graphviz import Digraph # type: ignore
GRAPHVIZ_AVAILABLE = True
except Exception:
GRAPHVIZ_AVAILABLE = False
# matplotlib импорт в качестве запасного рендерера
try:
import matplotlib.pyplot as plt # type: ignore
from matplotlib.patches import FancyBboxPatch, Circle
MATPLOTLIB_AVAILABLE = True
except Exception:
MATPLOTLIB_AVAILABLE = False
# Попытка импортировать python-docx для генерации .docx отчёта
try:
from docx import Document # type: ignore
from docx.shared import Inches, Pt # type: ignore
PYDOCX_AVAILABLE = True
except Exception:
PYDOCX_AVAILABLE = False
# Настройка логирования
LOG_DIR = "results"
os.makedirs(LOG_DIR, exist_ok=True)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(os.path.join(LOG_DIR, "analysis.log"), encoding="utf-8"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def read_own_source() -> Tuple[bool, str]:
"""
Пытается прочитать исходный код текущего файла и вернуть (успех, текст).
Возвращает (False, сообщение об ошибке) при неудаче.
"""
try:
# пытаемся получить путь к текущему файлу
current_file = os.path.abspath(__file__)
except Exception:
# fallback: используем argv[0]
current_file = os.path.abspath(sys.argv[0]) if sys.argv and sys.argv[0] else None
if not current_file or not os.path.exists(current_file):
msg = "Не удалось определить путь к файлу скрипта для включения в отчёт."
logger.warning(msg)
return False, msg
try:
with open(current_file, "r", encoding="utf-8") as f:
src = f.read()
return True, src
except Exception as e:
logger.exception("Ошибка при чтении исходного кода скрипта")
return False, f"Ошибка при чтении исходного кода: {e}"
@dataclass
class Alternative:
"""Класс для представления альтернативы в задаче."""
name: str
payoff_good: float # выигрыш при благоприятном исходе (в рублях)
payoff_bad: float # выигрыш при неблагоприятном исходе (в рублях)
p_good: float # вероятность благоприятного исхода (0..1)
def expected_value(self) -> float:
"""Вычисление ожидаемой денежной оценки (ОДО)."""
ev = self.p_good * self.payoff_good + (1 - self.p_good) * self.payoff_bad
logger.debug(f"ОДО для '{self.name}': p_good={self.p_good}, "
f"payoff_good={self.payoff_good}, payoff_bad={self.payoff_bad} => EV={ev}")
return ev
def payoff_tuple(self) -> Tuple[str, float, float]:
"""Возвращает кортеж для CSV/таблицы."""
return (self.name, self.payoff_good, self.payoff_bad)
class DecisionTreeAnalysis:
"""Класс, выполняющий весь анализ и генерацию результатов."""
def __init__(self, alternatives: List[Alternative], results_dir: str = "results"):
self.alternatives = alternatives
self.results_dir = results_dir
os.makedirs(self.results_dir, exist_ok=True)
logger.info("Инициализация DecisionTreeAnalysis")
logger.debug(f"Папка для результатов: {self.results_dir}")
def save_payoff_table(self, filename: str = "payoff_table.csv") -> str:
"""Сохранение таблицы выигрышей в CSV."""
path = os.path.join(self.results_dir, filename)
logger.info(f"Сохранение таблицы выигрышей в {path}")
try:
with open(path, mode="w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
header = ["Альтернатива", "Выигрыш (благоприятный), руб.", "Выигрыш (неблагоприятный), руб."]
writer.writerow(header)
for alt in self.alternatives:
writer.writerow([alt.name, int(alt.payoff_good), int(alt.payoff_bad)])
logger.debug("Таблица успешно сохранена")
except Exception as e:
logger.exception("Ошибка при сохранении таблицы выигрышей")
raise e
return path
def compute_all_ev(self) -> List[Tuple[Alternative, float]]:
"""Вычисляет ОДО для всех альтернатив и возвращает список (альтернатива, ОДО)."""
results = []
logger.info("Вычисление ОДО для всех альтернатив")
for alt in self.alternatives:
ev = alt.expected_value()
results.append((alt, ev))
logger.info(f"ОДО '{alt.name}' = {ev:.2f} руб.")
return results
def select_best(self) -> Tuple[Alternative, float]:
"""Выбирает альтернативу с максимальным ОДО."""
results = self.compute_all_ev()
best_alt, best_val = max(results, key=lambda x: x[1])
logger.info(f"Оптимальная альтернатива: {best_alt.name} (ОДО = {best_val:.2f} руб.)")
return best_alt, best_val
def generate_readme(self, filename: str = "README.md"):
"""Автоматическая генерация README.md с результатами и инструкциями, включая код скрипта."""
path = os.path.join(self.results_dir, "..", filename)
path = os.path.abspath(path)
logger.info(f"Генерация README.md по пути: {path}")
try:
best_alt, best_val = self.select_best()
evs = self.compute_all_ev()
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
success_src, src_text = read_own_source()
with open(path, "w", encoding="utf-8") as f:
f.write("# Проект: Анализ принятия решений методом дерева решений (Вариант 1)\n\n")
f.write("**Дата генерации:** {}\n\n".format(now))
f.write("## Описание задачи\n")
f.write("Молодой российский бизнесмен предполагает построить ночную дискотеку неподалеку от университета. "
"Существует два варианта: открыть дискотеку со столовой (дневное обслуживание) или без столовой.\n\n")
f.write("## Исходные данные\n")
f.write("- Вероятность благоприятного исхода: {:.2f}\n".format(self.alternatives[0].p_good))
f.write("- Вероятность неблагоприятного исхода: {:.2f}\n\n".format(1 - self.alternatives[0].p_good))
f.write("### Финансовые показатели (в рублях)\n\n")
f.write("| Альтернатива | Выигрыш (благоприятный) | Выигрыш (неблагоприятный) |\n")
f.write("|---|---:|---:|\n")
for alt, ev in evs:
f.write(f"| {alt.name} | {int(alt.payoff_good)} | {int(alt.payoff_bad)} |\n")
f.write("\n")
f.write("## Методика решения\n")
f.write("Вычисляется ожидаемая денежная оценка (ОДО) по формуле:\n\n")
f.write("`ОДО = P(благоприятный) * Выигрыш_благоприятный + P(неблагоприятный) * Выигрыш_неблагоприятный`\n\n")
f.write("Далее выбирается альтернатива с максимальным значением ОДО (критерий объективиста).\n\n")
f.write("## Результаты расчётов\n\n")
for alt, ev in evs:
f.write(f"- ОДО для **{alt.name}** = {ev:.2f} руб.\n")
f.write("\n")
f.write(f"**Оптимальная альтернатива:** **{best_alt.name}** (максимальная ОДО = {best_val:.2f} руб.)\n\n")
f.write("## Файлы с результатами\n")
f.write("- Таблица выигрышей: `results/payoff_table.csv`\n")
f.write("- Изображение дерева решений: `results/decision_tree.png`\n")
f.write("- Отчёт в формате Word: `results/decision_report.docx`\n")
f.write("- Лог выполнения: `results/analysis.log`\n\n")
f.write("## Инструкция по запуску\n")
f.write("1. Установите зависимости:\n")
f.write(" ```bash\n")
f.write(" pip install graphviz matplotlib python-docx\n")
f.write(" apt-get install graphviz\n")
f.write(" ```\n")
f.write(" Заметьте: для корректной работы модуля graphviz необходим системный пакет Graphviz (dot). "
"Если он недоступен, скрипт попытается сгенерировать запасной рисунок через matplotlib.\n\n")
f.write("2. Запустите скрипт:\n")
f.write(" ```bash\n")
f.write(" python decision_tree_analysis.py\n")
f.write(" ```\n\n")
f.write("## Выводы и рекомендации\n")
f.write(f"На основании критерия максимальной ожидаемой стоимостной ценности рекомендуется открыть: **{best_alt.name}**.\n\n")
f.write("## Примечания\n")
f.write("Все расчёты произведены в рублях; исходные значения приводились в тыс. рублей и были переведены в рубли.\n\n")
# Вставляем исходный код скрипта
f.write("## Код скрипта\n\n")
if success_src:
f.write("Ниже приведён полный исходный код скрипта `decision_tree_analysis.py`.\n\n")
f.write("```python\n")
f.write(src_text)
# Убедимся, что файл заканчивается переводом строки
if not src_text.endswith("\n"):
f.write("\n")
f.write("```\n")
else:
f.write("Не удалось прочитать исходный код скрипта: ")
f.write(src_text + "\n")
logger.debug("README.md успешно сгенерирован")
except Exception as e:
logger.exception("Ошибка при генерации README.md")
raise e
return path
def generate_docx_report(self, filename: str = "decision_report.docx") -> str:
"""
Генерация отчёта в формате DOCX с таблицей, результатами расчётов и изображением дерева.
Включает исходный код скрипта как моноширинный текст (если python-docx установлен).
"""
out_path = os.path.join(self.results_dir, filename)
logger.info(f"Генерация DOCX-отчёта: {out_path}")
if not PYDOCX_AVAILABLE:
logger.warning("python-docx не установлен — DOCX отчёт не будет создан")
return out_path
try:
doc = Document()
# --- Заголовок лабораторной ---
doc.add_heading('Лабораторная работа №3', level=1)
doc.add_paragraph('Принятие решений в условиях недостатка информации')
doc.add_paragraph('Вариант 1')
# Поле для имени студента
doc.add_paragraph('Студент: ____________________ (ФИО)')
doc.add_paragraph('') # пустая строка
# Основная часть отчёта
doc.add_heading('Отчёт: Анализ дерева решений (Вариант 1)', level=1)
doc.add_paragraph(f"Дата генерации: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
doc.add_heading('Исходные данные', level=2)
p = doc.add_paragraph()
p.add_run('Вероятность благоприятного исхода: ').bold = True
p.add_run(f"{self.alternatives[0].p_good:.2f}\n")
p.add_run('Вероятность неблагоприятного исхода: ').bold = True
p.add_run(f"{1 - self.alternatives[0].p_good:.2f}\n")
doc.add_heading('Финансовые показатели (в рублях)', level=2)
table = doc.add_table(rows=1, cols=3)
hdr_cells = table.rows[0].cells
hdr_cells[0].text = 'Альтернатива'
hdr_cells[1].text = 'Выигрыш (благоприятный), руб.'
hdr_cells[2].text = 'Выигрыш (неблагоприятный), руб.'
for alt in self.alternatives:
row_cells = table.add_row().cells
row_cells[0].text = alt.name
row_cells[1].text = str(int(alt.payoff_good))
row_cells[2].text = str(int(alt.payoff_bad))
doc.add_heading('Результаты расчётов (ОДО)', level=2)
evs = self.compute_all_ev()
for alt, ev in evs:
doc.add_paragraph(f"ОДО для {alt.name}: {ev:.2f} руб.")
best_alt, best_val = self.select_best()
doc.add_heading('Вывод и рекомендация', level=2)
doc.add_paragraph(f"Оптимальная альтернатива: {best_alt.name} (максимальная ОДО = {best_val:.2f} руб.)")
# Вставка изображения дерева решений, если файл существует
img_path = os.path.join(self.results_dir, "decision_tree.png")
if os.path.exists(img_path):
try:
doc.add_heading('Дерево решений', level=2)
doc.add_picture(img_path, width=Inches(6))
except Exception:
logger.exception("Не удалось вставить изображение в docx (возможно, неподдерживаемый формат)")
doc.add_paragraph("Изображение дерева не удалось вставить в документ. См. файл: results/decision_tree.png")
else:
doc.add_paragraph("Изображение дерева решений отсутствует. Сначала выполните генерацию изображения.")
# Вставляем исходный код скрипта в docx
success_src, src_text = read_own_source()
doc.add_heading('Код скрипта', level=2)
if success_src:
# Добавляем код по строкам, пытаясь применить моноширинный шрифт и небольшой размер
for line in src_text.splitlines():
p = doc.add_paragraph()
run = p.add_run(line)
try:
run.font.name = 'Courier New'
run.font.size = Pt(8)
except Exception:
# игнорируем ошибки стиля, код всё равно будет вставлен
pass
else:
doc.add_paragraph("Не удалось прочитать исходный код скрипта: " + src_text)
# Сохранение файла
doc.save(out_path)
logger.info(f"DOCX-отчёт сохранён: {out_path}")
except Exception as e:
logger.exception("Ошибка при генерации DOCX-отчёта")
raise e
return out_path
def render_decision_tree(self, filename: str = "decision_tree.png"):
"""Построение и сохранение изображения дерева решений."""
out_path = os.path.join(self.results_dir, filename)
logger.info("Построение дерева решений")
try:
if GRAPHVIZ_AVAILABLE:
logger.debug("Используем graphviz для визуализации")
dot = Digraph("DecisionTree", format="png")
dot.attr(rankdir="TB", size="8,6")
# корневой узел решения (квадратный)
dot.node("D", "☐ Решение", shape="square", fontsize="12", style="rounded,filled", fillcolor="#EFEFEF")
# Альтернативы как прямоугольники
for i, alt in enumerate(self.alternatives, start=1):
node_id = f"A{i}"
label = f"{alt.name}"
dot.node(node_id, label, shape="box")
dot.edge("D", node_id)
# Узел случая (каждая альтернатива ведёт к узлу случая с вероятностями)
node_case = f"C{i}"
dot.node(node_case, "○ Состояние", shape="circle")
dot.edge(node_id, node_case, label=f"P(благ)={alt.p_good:.2f}")
# Конечные листья: благоприятный и неблагоприятный
leaf_good = f"O{i}G"
leaf_bad = f"O{i}B"
dot.node(leaf_good, f"+{int(alt.payoff_good/1000)} тыс. руб.\n(благоприятный)")
dot.node(leaf_bad, f"{int(alt.payoff_bad/1000)} тыс. руб.\n(неблагоприятный)")
dot.edge(node_case, leaf_good, label=f"P={alt.p_good:.2f}")
dot.edge(node_case, leaf_bad, label=f"P={1 - alt.p_good:.2f}")
# Сохранение
tmp = os.path.join(self.results_dir, "decision_tree")
dot.render(tmp, cleanup=True)
png_path = tmp + ".png"
if os.path.exists(png_path):
try:
os.replace(png_path, out_path)
except Exception:
import shutil
shutil.copy(png_path, out_path)
logger.info(f"Дерево решений сохранено в {out_path}")
elif MATPLOTLIB_AVAILABLE:
logger.debug("Graphviz недоступен — используем matplotlib для визуализации")
self._render_tree_matplotlib(out_path)
logger.info(f"Дерево решений (matplotlib) сохранено в {out_path}")
else:
logger.warning("Ни graphviz, ни matplotlib недоступны — изображение дерева не создано")
raise RuntimeError("Нет доступных инструментов для отрисовки дерева (graphviz/matplotlib).")
except Exception as e:
logger.exception("Ошибка при построении дерева решений")
raise e
return out_path
def _render_tree_matplotlib(self, out_path: str):
"""Запасной рендер дерева через matplotlib (простой графический вид)."""
fig, ax = plt.subplots(figsize=(8, 6))
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
root_x, root_y = 5, 9
alt_xs = [2 + i * (6 / max(1, (len(self.alternatives) - 1))) for i in range(len(self.alternatives))]
alt_y = 6
leaf_y_good = 3.5
leaf_y_bad = 1.5
root_box = FancyBboxPatch((root_x - 0.9, root_y - 0.4), 1.8, 0.8,
boxstyle="round,pad=0.3", linewidth=1, ec="black", fc="#EFEFEF")
ax.add_patch(root_box)
ax.text(root_x, root_y, "☐ Решение", ha="center", va="center", fontsize=10)
for i, alt in enumerate(self.alternatives):
x = alt_xs[i]
alt_box = FancyBboxPatch((x - 0.9, alt_y - 0.4), 1.8, 0.8,
boxstyle="square,pad=0.3", linewidth=1, ec="black", fc="#FFFFFF")
ax.add_patch(alt_box)
ax.text(x, alt_y, alt.name, ha="center", va="center", fontsize=9)
ax.plot([root_x, x], [root_y - 0.4, alt_y + 0.4], linewidth=1, solid_capstyle='round')
circle = Circle((x, alt_y - 1.2), 0.25, ec="black", fc="#FFFFFF")
ax.add_patch(circle)
ax.text(x, alt_y - 1.2, "○", ha="center", va="center")
ax.text(x, leaf_y_good + 0.2, f"+{int(alt.payoff_good/1000)} тыс.\n(благоприятный)", ha="center", va="center")
ax.text(x, leaf_y_bad + 0.2, f"{int(alt.payoff_bad/1000)} тыс.\n(неблагоприятный)", ha="center", va="center")
ax.plot([x, x], [alt_y - 1.5, leaf_y_good + 0.5], linewidth=1)
ax.plot([x, x], [alt_y - 1.5, leaf_y_bad + 0.5], linewidth=1)
ax.text((root_x + x) / 2, (root_y + alt_y) / 2 + 0.1, f"P(благ)={alt.p_good:.2f}", ha="center")
ax.text(x + 0.2, (leaf_y_good + (alt_y - 1.2)) / 2, f"P={alt.p_good:.2f}", fontsize=8)
ax.text(x + 0.2, (leaf_y_bad + (alt_y - 1.2)) / 2, f"P={1 - alt.p_good:.2f}", fontsize=8)
plt.tight_layout()
plt.savefig(out_path, dpi=200)
plt.close(fig)
def run(self):
"""Основной запуск всех этапов анализа."""
logger.info("Запуск анализа дерева решений")
try:
csv_path = self.save_payoff_table()
img_path = self.render_decision_tree()
readme_path = self.generate_readme()
docx_path = self.generate_docx_report()
# Консольный вывод пошаговых результатов
print("\n===== РЕЗУЛЬТАТЫ АНАЛИЗА =====\n")
evs = self.compute_all_ev()
for alt, ev in evs:
print(f"Альтернатива «{alt.name}»:")
print(f" Выигрыш при благоприятном исходе: {int(alt.payoff_good)} руб.")
print(f" Выигрыш при неблагоприятном исходе: {int(alt.payoff_bad)} руб.")
print(f" Вероятность благоприятного исхода: {alt.p_good:.2f}")
print(f" ОДО = {ev:.2f} руб.\n")
best_alt, best_val = self.select_best()
print(f"=> Оптимальная альтернатива: {best_alt.name} (максимальная ОДО = {best_val:.2f} руб.)\n")
print(f"Файлы с результатами сохранены в папке: {os.path.abspath(self.results_dir)}")
print(f"- Таблица выигрышей: {csv_path}")
print(f"- Изображение дерева решений: {img_path}")
print(f"- README: {readme_path}")
if PYDOCX_AVAILABLE:
print(f"- DOCX-отчёт: {docx_path}")
else:
print("- DOCX-отчёт: не создан (python-docx не установлен)")
logger.info("Анализ завершён успешно")
except Exception as e:
logger.exception("Ошибка во время выполнения анализа")
print("Произошла ошибка. Подробности в логе: results/analysis.log")
raise e
def main():
# Перевод значений: задано в условии в тыс. руб., переведём в рубли (умножим на 1000)
try:
logger.info("Подготовка исходных данных (Вариант 1)")
p_good = 0.5
alts = [
Alternative(name="Дискотека со столовой", payoff_good=250_000, payoff_bad=-55_000, p_good=p_good),
Alternative(name="Дискотека без столовой", payoff_good=175_000, payoff_bad=-20_000, p_good=p_good)
]
analysis = DecisionTreeAnalysis(alternatives=alts, results_dir="results")
analysis.run()
except Exception as exc:
logger.exception("Критическая ошибка в main()")
print("Критическая ошибка. Проверьте файл results/analysis.log для деталей.")
raise exc
if __name__ == "__main__":
main()