Source code for mento.results

import pandas as pd
from typing import Optional, List, Any
from tabulate import tabulate
from docx import Document
from docx.shared import Pt, Cm, RGBColor
import seaborn as sns
import matplotlib.pyplot as plt
from docx.oxml import parse_xml
from docx.oxml.ns import nsdecls
from io import BytesIO
from pandas.io.formats.style import Styler

CUSTOM_COLORS = {
    "blue": "#1f77b4",  # Default Matplotlib blue
    "red": "#d62728",  # Default Matplotlib red
    "dark_gray": "#323232",
    "light_gray": "#e3e3e3",
    "dark_blue": "#073165",
}


# Default Matplotlib and Seaborn settings
[docs] def configure_plot_settings() -> None: """ Configures global settings for Matplotlib and Seaborn plots. """ # Basic style for the plot sns.set_theme(style="whitegrid") sns.set_context("paper", rc={"lines.linewidth": 1.8}) sns.set_style(rc={"axes.facecolor": "#F8F8F8"}) # Matplotlib specific settings plt.rcParams.update( { "text.usetex": True, "font.family": "serif", "font.serif": ["Lato"], "axes.titlesize": 12, "axes.labelsize": 12, "xtick.labelsize": 12, "ytick.labelsize": 12, "legend.fontsize": 12, } )
[docs] class Formatter: """ Class to stlye the results in a Markdown display and a DataFrame output. """ def __init__(self) -> None: # Define colors in the constructor self.green = "#439b00" self.red = "#d43e36" self.yellow = "#efc200" self.mid_value = 0.95 self.max_value = 1
[docs] def DCR(self, DCR: float) -> str: # Determine color based on DCR value if self.mid_value > DCR: color = self.green elif self.mid_value <= DCR <= self.max_value: color = self.yellow else: color = self.red return f"$\\color{{{color}}}{{\\text{{DCR}}={round(DCR,2)}}}$"
[docs] def DCR_value(self, DCR: float) -> str: # Determine color based on DCR value if self.mid_value > DCR: color = self.green elif self.mid_value <= DCR <= 1: color = self.yellow else: color = self.red return f"$\\color{{{color}}}{{{round(DCR,2)}}}$"
[docs] def is_lower(self, value1: float, value2: float) -> str: # Compare two values and return the appropriate formatted output if value1 < value2: return r"$\color{" + self.green + r"}{\, \checkmark}$" # Green checkmark else: return r"$\color{" + self.red + r"}{\, \times}$" # Red cross
[docs] def is_greater(self, value1: float, value2: float) -> str: # Compare two values and return the appropriate formatted output if value1 > value2: return r"$\color{" + self.green + r"}{\, \checkmark}$" # Green checkmark else: return r"$\color{" + self.red + r"}{\, \times}$" # Red cross
# Formatting functions outside the class
[docs] def DCR_value_df(self, DCR: Optional[float]) -> str: # If DCR is None or not a number, return an empty string if DCR is None or not isinstance(DCR, (int, float)): return "" # Determine the color based on DCR value if self.mid_value > DCR: color = self.green elif self.mid_value <= DCR <= self.max_value: # Use self.max_value here for consistency color = self.yellow else: color = self.red return f"color: {color}" # This is a CSS style string
[docs] def apply_DCR_style(self, value: float) -> str: formatted_value = round(value, 2) if isinstance(value, (int, float)) else value return self.DCR_value_df(formatted_value)
[docs] def color_DCR_df(self, df: pd.DataFrame, DCR_columns: list) -> Styler: # Use Styler directly now """ Apply color styling to specified DCR-related columns in the DataFrame. :param df: DataFrame with DCR values to color. :param DCR_columns: List of column names to apply the DCR styling to. :return: A styled DataFrame with colored DCR values. """ return df.style.map(self.apply_DCR_style, subset=DCR_columns).format(precision=2)
[docs] class TablePrinter: """ A class for printing tables with customizable formatting options. Attributes: ----------- title : Optional[str] Optional title displayed above the printed table. Methods: -------- print_table_data(data: List[List[Any]], headers: List[str], tablefmt: str = "fancygrid", numalign: str = "right") -> None Prints table data with customizable formatting. print_table_min_max(data: List[List[Any]], headers: List[str], tablefmt: str = "fancygrid", numalign: str = "right") -> None Prints table data with column alignment for minimum and maximum values. """ def __init__(self, title: Optional[str] = None) -> None: """ Initializes the TablePrinter with an optional title. Parameters: ----------- title : Optional[str], default=None Title to be displayed above the table, if provided. """ self.title = title
[docs] def print_table_data( self, data: dict[str, list[Any]], headers: str, tablefmt: str = "fancygrid", numalign: str = "right", ) -> None: """ Prints table data with customizable formatting options. Parameters: ----------- data : List[List[Any]] The data to be printed in table format, where each inner list represents a row. headers : List[str] Column headers for the table. tablefmt : str, default="fancygrid" Table formatting style supported by tabulate (e.g., "plain", "grid", "fancygrid"). numalign : str, default="right" Number alignment in the table. Common options are "right", "center", or "left". Returns: -------- table: Return the formatted table string """ # if self.title: # print(f"======= {self.title} ======= \n") colalign = ("left", "center", "right", "left") table = tabulate( data, headers=headers, tablefmt=tablefmt, numalign=numalign, colalign=colalign, ) print(table, "\n") return None
[docs] def print_table_min_max( self, data: dict[str, list[Any]], headers: str, tablefmt: str = "fancygrid", numalign: str = "right", ) -> None: """ Prints table data with column alignment for minimum and maximum values. Parameters: ----------- data : List[List[Any]] The data to be printed in table format, where each inner list represents a row. headers : List[str] Column headers for the table. tablefmt : str, default="fancygrid" Table formatting style supported by tabulate (e.g., "plain", "grid", "fancygrid"). numalign : str, default="right" Number alignment in the table. Common options are "right", "center", or "left". Returns: -------- None """ if self.title: print(f"======= {self.title} ======= \n") colalign = ("left", "center", "center", "center", "center", "center", "left") table = tabulate( data, headers=headers, tablefmt=tablefmt, numalign=numalign, colalign=colalign, ) print(table, "\n")
[docs] class DocumentBuilder: """ A class to build and style a Word document, including adding headings and tables from data frames. Attributes: ----------- title : str Title of the document, displayed at the beginning. font_name : str Default font name for the document text. font_size : int Default font size for the document text. doc : Document The Document object representing the Word document being built. Methods: -------- set_document_style() -> None Configures the document's default style with the specified font name and size. add_heading(text: str, level: int) -> None Adds a heading to the document at the specified level. set_col_widths(table: 'docx.table.Table', column_widths: List[Cm]) -> None Sets the width for each column in a given table. add_table(df: pd.DataFrame, column_widths: List[Cm]) -> None Adds a table to the document, with data from a DataFrame and custom column widths. save(filename: str) -> None Saves the document to a specified filename. """ def __init__(self, title: str, font_name: str = "Lato", font_size: int = 9) -> None: """ Initializes the DocumentBuilder with a title, font name, and font size. Parameters: ----------- title : str Title to be displayed in the document. font_name : str, default='Lato' Font name to be used for the document text. font_size : int, default=9 Font size for the document text. """ self.doc = Document() self.title = title self.font_name = font_name self.font_size = font_size self.set_document_style() self.set_page_size() self.set_margins()
[docs] def set_document_style(self) -> None: """ Sets the default style of the document, applying the font name and size. Returns: -------- None """ # Normal text style style = self.doc.styles["Normal"] style.font.name = self.font_name style.font.size = Pt(self.font_size)
[docs] def set_page_size(self) -> None: """ Sets the page size to A4 by default. Returns: -------- None """ section = self.doc.sections[0] section.page_height = Cm(29.7) section.page_width = Cm(21.0)
[docs] def set_margins(self, top: float = 3.5, bottom: float = 2.5, left: float = 1.6, right: float = 1.6) -> None: """ Set page margins in centimeters. Parameters ---------- top, bottom, left, right : float Margins in centimeters. Default is 1 cm for all. """ for section in self.doc.sections: section.top_margin = Cm(top) section.bottom_margin = Cm(bottom) section.left_margin = Cm(left) section.right_margin = Cm(right)
[docs] def add_heading(self, text: str, level: int, font_size: Optional[float] = 10) -> None: """ Adds a heading to the document at the specified level. Parameters: ----------- text : str The text for the heading. level : int The heading level (e.g., 0 for title, 1 for main headings, etc.). font_size : float, optional Font size in points. Default is 10 pt. Returns: -------- None """ heading = self.doc.add_heading(text, level=level) heading.paragraph_format.space_before = Pt(0) # Set font size for all runs in heading for run in heading.runs: run.font.size = Pt(font_size) run.font.name = self.font_name
[docs] def add_text(self, text: str) -> None: """Adds a paragraph to the document""" self.doc.add_paragraph(text)
[docs] def set_col_widths(self, table: Any, column_widths: List[Cm]) -> None: """ Sets the width of each column in the table. Parameters: ----------- table : docx.table.Table The table object whose column widths need to be set. column_widths : List[Cm] List of widths for each column, in centimeters. Returns: -------- None """ for row in table.rows: for idx, width in enumerate(column_widths): row.cells[idx].width = width
[docs] def add_table( self, df: pd.DataFrame, column_widths: List[Cm], font_size: Optional[float] = 9, ) -> None: """ Adds a table to the document, populated with data from a DataFrame. Parameters ---------- df : pd.DataFrame The DataFrame containing the data for the table. column_widths : List[Cm] List of column widths for the table. font_size : float, optional Font size (in points) for table text. Default is 10 pt. Returns ------- docx.table.Table The created table object. """ from docx.shared import Pt # --- Create and style table --- table = self.doc.add_table(rows=df.shape[0] + 1, cols=df.shape[1]) table.style = "Light Shading" self.set_col_widths(table, column_widths) # --- Header row --- for j in range(df.shape[1]): table.cell(0, j).text = str(df.columns[j]) # --- Data rows --- for i in range(df.shape[0]): for j in range(df.shape[1]): value = df.iat[i, j] table.cell(i + 1, j).text = str(value) # --- Format all text --- if font_size is not None: for row in table.rows: for cell in row.cells: for paragraph in cell.paragraphs: for run in paragraph.runs: run.font.size = Pt(font_size) # --- First column not bold --- for row in table.rows[1:]: first_cell = row.cells[0] for run in first_cell.paragraphs[0].runs: run.font.bold = False # --- Add spacer paragraph --- spacer = self.doc.add_paragraph() spacer.paragraph_format.space_after = Pt(0) return table
[docs] def add_table_data(self, df: pd.DataFrame, column_widths=[Cm(12), Cm(2), Cm(2), Cm(2)]) -> None: self.add_table(df, column_widths, font_size=self.font_size)
[docs] def add_table_dcr(self, df: pd.DataFrame) -> None: """ Adds a table using `add_table` and modifies the last row's DCR cell color. - Green if DCR < 1 - Red if DCR >= 1 """ column_widths = [Cm(12), Cm(2), Cm(2), Cm(2)] self.add_table(df, column_widths, font_size=self.font_size) # Get the last table added to the document table = self.doc.tables[-1] # Retrieve the most recent table # Apply color to DCR cell (third column of the last row) last_row_idx = df.shape[0] # Last row index dcr_column_idx = 2 # Third column index (zero-based) dcr_value = float(df.iat[-1, dcr_column_idx]) # Extract DCR value if dcr_value < 1: shading_color = "C6EFCE" # Green font_color = "006100" # Green font else: shading_color = "FFC7CE" # Red font_color = "9C0006" # Red font # Apply shading and font color to the entire last row for cell in table.rows[last_row_idx].cells: # Apply background color cell._element.get_or_add_tcPr().append(parse_xml(f'<w:shd {nsdecls("w")} w:fill="{shading_color}"/>')) # Apply font color for paragraph in cell.paragraphs: for run in paragraph.runs: run.font.color.rgb = RGBColor.from_string(font_color)
[docs] def add_table_min_max(self, df: pd.DataFrame) -> None: column_widths = [Cm(9), Cm(2), Cm(2), Cm(2), Cm(2), Cm(1)] self.add_table(df, column_widths, font_size=self.font_size)
[docs] def add_figure(self, fig: "plt.Figure", width: float = 16) -> None: """ Inserts a matplotlib Figure into the Word document. Parameters ---------- fig : matplotlib.figure.Figure The figure to insert. width : float Width in centimeters (default 12 cm). Height auto-scales. """ buf = BytesIO() fig.savefig(buf, format="png", dpi=200, bbox_inches="tight") plt.close(fig) buf.seek(0) self.doc.add_picture(buf, width=Cm(width)) buf.close() # small space after spacer = self.doc.add_paragraph() spacer.paragraph_format.space_after = Pt(0)
[docs] def add_image(self, image_path: str, width: Optional[Cm] = None) -> None: """ Insert an external image into the document. Parameters ---------- image_path : str Absolute or relative path to the image file. width : docx.shared.Cm, optional Target width. Height scales to keep aspect ratio. """ # paragraph container so image is not glued to previous element paragraph = self.doc.add_paragraph() run = paragraph.add_run() if width is not None: run.add_picture(image_path, width=width) else: run.add_picture(image_path) # small space after spacer = self.doc.add_paragraph() spacer.paragraph_format.space_after = Pt(0)
[docs] def save(self, filename: str) -> None: """ Saves the document to a specified file. Parameters: ----------- filename : str The filename (including path) where the document will be saved. Returns: -------- None """ self.doc.save(filename)
# Examples to run in a Jupyter Notebook # formatter = Formatter() # display(Markdown(formatter.DCR(0.85))) # display(Markdown(formatter.DCR_value(0.85))) # display(Markdown(formatter.is_lower(0.85,1))) # display(Markdown(formatter.is_greater(0.85,1)))