From fb4a6a5006f78a76f45c29f36a5a9c80a0504d2f Mon Sep 17 00:00:00 2001
From: Robbertjan <robbertjan-1994@hotmail.com>
Date: Sat, 5 Feb 2022 17:01:55 +0100
Subject: [PATCH 1/3] Fixed name of footer bottom line location

---
 stargazer/stargazer.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/stargazer/stargazer.py b/stargazer/stargazer.py
index 72ecc4d..1306f5d 100644
--- a/stargazer/stargazer.py
+++ b/stargazer/stargazer.py
@@ -25,7 +25,7 @@ class LineLocation(Enum):
     BODY_TOP = 'bt'
     BODY_BOTTOM = 'bb'
     FOOTER_TOP = 'ft'
-    FOOTER_BOOTM = 'fb'
+    FOOTER_BOTTOM = 'fb'
 
 
 class Stargazer:
@@ -538,7 +538,7 @@ def generate_footer(self):
             if getattr(self, f'show_{attr}'):
                 footer += self.generate_stat(stat, self._stats_labels[attr])
 
-        footer += self.generate_custom_lines(LineLocation.FOOTER_BOOTM)
+        footer += self.generate_custom_lines(LineLocation.FOOTER_BOTTOM)
         footer += '<tr><td colspan="' + str(self.num_models + 1) + '" style="border-bottom: 1px solid black"></td></tr>'
         if self.show_notes:
             footer += self.generate_notes()
@@ -769,7 +769,7 @@ def generate_footer(self, only_tabular=False):
             if getattr(self, f'show_{attr}'):
                 footer += self.generate_stat(stat, self._stats_labels[attr])
 
-        footer += self.generate_custom_lines(LineLocation.FOOTER_BOOTM)
+        footer += self.generate_custom_lines(LineLocation.FOOTER_BOTTOM)
         footer += '\\hline\n\\hline \\\\[-1.8ex]\n'
         if self.show_notes:
             footer += self.generate_notes()

From 533c8c871a392d83e0ac333a4f028b46a9b47b34 Mon Sep 17 00:00:00 2001
From: Robbertjan <robbertjan-1994@hotmail.com>
Date: Sat, 5 Feb 2022 19:43:51 +0100
Subject: [PATCH 2/3] Implemented an excel renderer

---
 stargazer/stargazer.py | 209 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 209 insertions(+)

diff --git a/stargazer/stargazer.py b/stargazer/stargazer.py
index 1306f5d..d64a513 100644
--- a/stargazer/stargazer.py
+++ b/stargazer/stargazer.py
@@ -20,6 +20,7 @@
 from enum import Enum
 import numbers
 import pandas as pd
+import xlsxwriter
 
 class LineLocation(Enum):
     BODY_TOP = 'bt'
@@ -298,6 +299,9 @@ def render_latex(self, *args, escape=False, **kwargs):
             The LaTeX code.
         """
         return LaTeXRenderer(self, escape=escape).render(*args, **kwargs)
+    
+    def render_excel(self, *args, **kwargs):
+        return ExcelRenderer(self).render(*args, **kwargs)
 
 
 class Renderer:
@@ -832,3 +836,208 @@ def generate_additional_notes(self):
             notes_text += '\\multicolumn{' + str(self.num_models+1) + '}{r}\\textit{' + self._escape(note) + '} \\\\\n'
 
         return notes_text
+
+class ExcelRenderer(Renderer):
+    fmt = 'Excel'
+
+    # Labels for stats in Stargazer._auto_stats:
+    _stats_labels = {'n' : 'Observations',
+                     'r2' : 'R²',
+                     'adj_r2' : 'Adjusted R²',
+                     'residual_std_err' : 'Residual Std. Error',
+                     'f_statistic' : 'F Statistic'}
+    
+    def render(self, filename='workbook.xlsx', ignore_errors=True, fit_to_width=True, cell_height=20, start_row=1, start_col=1, insert_empty_rows=False):
+        with xlsxwriter.Workbook(filename) as wb:
+            ws = wb.add_worksheet()
+            row = self.generate_header(wb, ws, start_row, start_col)
+            row = self.generate_body(wb, ws, row, start_col, insert_empty_rows=insert_empty_rows)
+            row = self.generate_footer(wb, ws, row, start_col)
+
+            if ignore_errors:
+                ws.ignore_errors(
+                    {'number_stored_as_text': f'A1:{self._excel_col(start_col+1+self.num_models)}{row+1}'}
+                )
+            
+            if fit_to_width:
+                self._fit_to_width(ws, start_col)
+
+            if cell_height > 0:
+                for i in range(start_row, row):
+                    ws.set_row(i, cell_height)
+
+    def generate_header(self, wb, ws, row, col):
+
+        if not self.show_header:
+            return
+
+        if self.title_text is not None:
+            ws.write(row, col, self.title_text)
+            row += 1
+
+        if self.dep_var_name is not None:
+            ws.write(row, col, '', wb.add_format({'top': 6, 'bottom': 1}))
+            ws.merge_range(row, col+1, 
+                row, col+self.num_models, 
+                self.dep_var_name + self.dependent_variable, 
+                wb.add_format({'top' : 6, 'bottom': 1, 'align' : 'center', 'valign': 'vcenter', 'italic' : True})
+            )
+            row += 1
+        
+        if self.show_model_nums:
+
+            if self.dep_var_name is not None:
+                format1 = {'top': 0, 'bottom': 1}
+                format2 = {'bottom': 1, 'align': 'center', 'valign': 'vcenter'}
+            else:
+                format1 = {'top': 6, 'bottom': 1}
+                format2 = {'top': 6, 'bottom': 1, 'align': 'center', 'valign': 'vcenter'}
+            
+            ws.write(row, col, '', wb.add_format(format1))
+            ws.write_row(row,col+1,
+                [f'({i})' for i in range(1,self.num_models+1)], 
+                wb.add_format(format2)
+            )
+            row += 1
+
+        return row
+
+    def generate_body(self, wb, ws, row, col, insert_empty_rows=False):
+
+        row = self.generate_custom_lines(LineLocation.BODY_TOP, wb, ws, row, col)
+
+        for cov_name in self.cov_names:
+            row = self.generate_cov_rows(wb, ws, row, col, cov_name)
+            if insert_empty_rows:
+                row += 1
+
+        row = self.generate_custom_lines(LineLocation.BODY_BOTTOM, wb, ws, row, col)
+
+        return row
+
+    def generate_cov_rows(self, wb, ws, row, col, cov_name):
+
+        cov_print_name = cov_name
+        if self.cov_map is not None:
+            cov_print_name = self.cov_map.get(cov_print_name, cov_name)
+
+        ws.write(row, col, cov_print_name, wb.add_format({'align': 'left', 'valign': 'vcenter'}))
+        ws.write(row+1, col, '', wb.add_format({'align': 'left', 'valign': 'vcenter'}))
+
+        for (i, md) in enumerate(self.model_data):  # refactor to generate_cov_rows
+
+            if cov_name in md['cov_names']:
+                cov_text = self._float_format(md['cov_values'][cov_name])
+
+                if self.show_sig:
+                    cov_text += self._format_sig_icon(md['p_values'][cov_name])
+
+                cov_prec_text = '('
+
+                if self.confidence_intervals:
+                    cov_prec_text += self._float_format(md['conf_int_low_values'][cov_name]) + ' , '
+                    cov_prec_text += self._float_format(md['conf_int_high_values'][cov_name])
+                else:
+                    cov_prec_text += self._float_format(md['cov_std_err'][cov_name])
+
+                cov_prec_text += ')'
+
+            else:
+                cov_text = ''
+                cov_prec_text = ''
+
+            ws.write(row, col+1+i, cov_text, wb.add_format({'align': 'center', 'valign': 'vcenter'}))
+            ws.write(row+1, col+1+i, cov_prec_text, wb.add_format({'align': 'center', 'valign': 'vcenter'}))
+        
+        row += 2
+        return row
+
+    def generate_footer(self, wb, ws, row, col):
+
+        if not self.show_footer:
+            return
+
+        for (i,(attr, stat)) in enumerate(Stargazer._auto_stats):
+            if getattr(self, f'show_{attr}'):
+                self._generate_stat(wb, ws, stat, self._stats_labels[attr], row, col, i, len(Stargazer._auto_stats))
+                row +=1
+
+        if self.show_notes:
+            ws.write(row, 1, self.notes_label, wb.add_format({'align': 'left', 'italic' : True, 'top': 6, 'valign': 'vcenter'}))
+            if self.notes_append and self.show_stars:
+                ws.merge_range(row, col+1, row, col+self.num_models, self._generate_p_value_section(), wb.add_format({'align': 'right', 'top': 6, 'valign': 'vcenter'}))
+            row += 1
+
+        return row
+
+    def _format_sig_icon(self, pvalue):
+        return '*' * len(str(self.get_sig_icon(pvalue)))
+
+    def _generate_p_value_section(self):
+        return '; '.join([self._format_sig_icon(sig_level - 0.001)
+                      + '<' + str(sig_level) for sig_level in self.sig_levels])
+
+    def _excel_col(self, col):
+        quot, rem = divmod(col-1,26)
+        return self._excel_col(quot) + chr(rem+ord('A')) if col!=0 else ''
+
+    def generate_custom_lines(self, location, wb, ws, row, col):
+        for custom_row in self.custom_lines[location]:
+            format1 = {'align': 'left', 'valign': 'vcenter'}
+            format2 = {'align': 'center', 'valign': 'vcenter'}
+
+            if 'BOTTOM' in location.name:
+                format1 = {'align': 'left', 'valign': 'vcenter', 'bottom': 1}
+                format2 = {'align': 'center', 'valign': 'vcenter', 'bottom': 1}
+
+                if 'FOOTER' in location.name:
+                    format1['bottom'] = 6
+                    format1['bottom'] = 6
+
+            ws.write(row, col, str(custom_row[0]), wb.add_format(format1))
+            ws.write_row(row, col+1, [str(custom_column) for custom_column in custom_row[1:]], wb.add_format(format2))
+            row += 1
+
+        return row
+
+    def _generate_stat(self, wb, ws, stat, label, row, col, i, n):
+        values = self._generate_stat_values(stat)
+        if not any(values):
+            return ''
+
+        format = {'align': 'left', 'valign': 'vcenter'}
+        if n == 1:
+            format.update({'top': 1, 'bottom': 6})
+        elif i == 0:
+            format.update({'top': 1})
+        elif i == n - 1:
+            format.update({'bottom': 6})
+
+        ws.write(row, col, label, wb.add_format(format))
+
+        format['align'] = 'center'
+        formatter = self._formatters.get(stat, self._float_format)
+        for (j, value) in enumerate(values):
+            if not isinstance(value, str):
+                value = formatter(value)
+            ws.write(row, col+1+j, value, wb.add_format(format))
+
+    def _fit_to_width(self, ws, col, min_width=15, max_width=75):
+
+        data = ws.table
+        str_data = ws.str_table
+
+        string_idxs = [[d[i].string if hasattr(d.get(i), 'string') else None for d in data.values()] for i in range(1,2+self.num_models)]
+        string_idx_mapper = {v:k  for k,v in str_data.string_table.items()}
+
+        df = pd.DataFrame(string_idxs).transpose().stack().reset_index(level=0, drop=True)
+        df = df.map(string_idx_mapper).apply(len)
+
+        width_index = df[df.index==0].max()
+        width_values = df[df.index!=0].max()
+
+        width_index = max(min(width_index*0.9, max_width), min_width)
+        width_values = max(min(width_values*0.9, max_width), min_width)
+
+        ws.set_column(col, col, width_index)
+        ws.set_column(col+1, col+self.num_models, width_values)

From cf9f6c46760b436bd4551735e5ebbb6f5326d4eb Mon Sep 17 00:00:00 2001
From: Robbertjan <robbertjan-1994@hotmail.com>
Date: Sat, 5 Feb 2022 22:13:31 +0100
Subject: [PATCH 3/3] Fix format of header

---
 stargazer/stargazer.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/stargazer/stargazer.py b/stargazer/stargazer.py
index d64a513..2ce9ec7 100644
--- a/stargazer/stargazer.py
+++ b/stargazer/stargazer.py
@@ -876,7 +876,7 @@ def generate_header(self, wb, ws, row, col):
             row += 1
 
         if self.dep_var_name is not None:
-            ws.write(row, col, '', wb.add_format({'top': 6, 'bottom': 1}))
+            ws.write(row, col, '', wb.add_format({'top': 6, 'bottom': 0}))
             ws.merge_range(row, col+1, 
                 row, col+self.num_models, 
                 self.dep_var_name + self.dependent_variable,