@@ -1449,6 +1449,7 @@ def plot(self, type: str = 'auto', **kwargs):
14491449 - Staggered SCM → cohort-level ATT comparison
14501450 - Causal Impact → 3-panel (original + pointwise + cumulative)
14511451 - Matching → Love plot (covariate balance)
1452+ - Neural causal estimators → CATE distribution
14521453 - RD → coefplot (use ``rdplot()`` for binned scatter)
14531454 - Other → coefplot
14541455 """
@@ -1488,6 +1489,10 @@ def plot(self, type: str = 'auto', **kwargs):
14881489 from ..matching .match import balanceplot
14891490 return balanceplot (self , ** kwargs )
14901491
1492+ if self .model_info .get ('neural_causal' ):
1493+ from ..neural_causal .plots import neural_causal_plot
1494+ return neural_causal_plot (self , type = 'cate' , ** kwargs )
1495+
14911496 return self ._coefplot (** kwargs )
14921497
14931498 # Explicit type overrides
@@ -1502,6 +1507,9 @@ def plot(self, type: str = 'auto', **kwargs):
15021507 if type == 'balance' :
15031508 from ..matching .match import balanceplot
15041509 return balanceplot (self , ** kwargs )
1510+ if type in ('cate' , 'ite' , 'effects' , 'propensity' , 'overlap' , 'loss' ):
1511+ from ..neural_causal .plots import neural_causal_plot
1512+ return neural_causal_plot (self , type = type , ** kwargs )
15051513 return self ._coefplot (** kwargs )
15061514
15071515 def _is_synth_result (self ) -> bool :
@@ -1560,6 +1568,193 @@ def _coefplot(self, ax=None, figsize=(8, 5), **kwargs):
15601568 # Export
15611569 # ------------------------------------------------------------------
15621570
1571+ def to_markdown (self , path : Optional [str ] = None ,
1572+ digits : int = 4 ) -> str :
1573+ """Render the causal result as Markdown.
1574+
1575+ Neural causal results delegate to the richer neural exporter,
1576+ which includes unit-level effects and training diagnostics.
1577+ Other causal results render the broom-style tidy table plus
1578+ scalar diagnostics.
1579+ """
1580+ if self .model_info .get ('neural_causal' ):
1581+ from ..neural_causal .exports import neural_causal_to_markdown
1582+ return neural_causal_to_markdown (self , path = path , digits = digits )
1583+
1584+ tidy = self .tidy ().round (digits )
1585+ glance = self .glance ().round (digits )
1586+ parts = [
1587+ f"# { self .method } " ,
1588+ "" ,
1589+ "## Estimates" ,
1590+ tidy .to_markdown (index = False ),
1591+ "" ,
1592+ "## Summary" ,
1593+ glance .to_markdown (index = False ),
1594+ ]
1595+ if self .detail is not None and len (self .detail ) > 0 :
1596+ parts .extend ([
1597+ "" ,
1598+ "## Detail" ,
1599+ self .detail .round (digits ).to_markdown (index = False ),
1600+ ])
1601+ text = "\n " .join (parts ) + "\n "
1602+ if path is not None :
1603+ from pathlib import Path
1604+ Path (path ).write_text (text , encoding = "utf-8" )
1605+ return text
1606+
1607+ def to_html (self , path : Optional [str ] = None ,
1608+ digits : int = 4 ) -> str :
1609+ """Render the causal result as an HTML report."""
1610+ if self .model_info .get ('neural_causal' ):
1611+ from ..neural_causal .exports import neural_causal_to_html
1612+ return neural_causal_to_html (self , path = path , digits = digits )
1613+
1614+ tidy = self .tidy ().round (digits )
1615+ glance = self .glance ().round (digits )
1616+ blocks = [
1617+ "<html><body>" ,
1618+ f"<h1>{ _html_escape (self .method )} </h1>" ,
1619+ "<h2>Estimates</h2>" ,
1620+ tidy .to_html (index = False ),
1621+ "<h2>Summary</h2>" ,
1622+ glance .to_html (index = False ),
1623+ ]
1624+ if self .detail is not None and len (self .detail ) > 0 :
1625+ blocks .extend ([
1626+ "<h2>Detail</h2>" ,
1627+ self .detail .round (digits ).to_html (index = False ),
1628+ ])
1629+ blocks .append ("</body></html>" )
1630+ html = "\n " .join (blocks )
1631+ if path is not None :
1632+ from pathlib import Path
1633+ Path (path ).write_text (html , encoding = "utf-8" )
1634+ return html
1635+
1636+ def to_excel (self , path : str , digits : int = 6 ) -> str :
1637+ """Write a multi-sheet Excel workbook for the causal result."""
1638+ if self .model_info .get ('neural_causal' ):
1639+ from ..neural_causal .exports import neural_causal_to_excel
1640+ return neural_causal_to_excel (self , path , digits = digits )
1641+
1642+ with pd .ExcelWriter (path ) as writer :
1643+ self .tidy ().round (digits ).to_excel (
1644+ writer , sheet_name = "Estimates" , index = False
1645+ )
1646+ self .glance ().round (digits ).to_excel (
1647+ writer , sheet_name = "Summary" , index = False
1648+ )
1649+ if self .detail is not None and len (self .detail ) > 0 :
1650+ self .detail .round (digits ).to_excel (
1651+ writer , sheet_name = "Detail" , index = False
1652+ )
1653+ diagnostics = _filter_jsonable_scalars (self .model_info )
1654+ if diagnostics :
1655+ pd .DataFrame ([diagnostics ]).to_excel (
1656+ writer , sheet_name = "Diagnostics" , index = False
1657+ )
1658+ return path
1659+
1660+ def to_word (
1661+ self ,
1662+ path : str ,
1663+ digits : int = 4 ,
1664+ caption : Optional [str ] = None ,
1665+ ) -> str :
1666+ """Write a Word (``.docx``) report for the causal result.
1667+
1668+ Produces a publication-style three-block document:
1669+
1670+ 1. Title (``caption`` or ``"<method> Results"``).
1671+ 2. Estimates table (variables × coefficient/SE/t/p/CI), formatted
1672+ with stars (``* p<0.1, ** p<0.05, *** p<0.01``) and Times New
1673+ Roman 9-10pt typography matching :file:`output/_aer_style.py`.
1674+ 3. Detail table (group/time/ATT or estimator-specific) if
1675+ ``self.detail`` is non-empty.
1676+ 4. Trailing notes paragraph: ``"Standard errors in parentheses.
1677+ Observations: N. Method: <method>."``.
1678+
1679+ Requires ``python-docx``.
1680+ """
1681+ try :
1682+ from docx import Document
1683+ from docx .shared import Pt
1684+ from docx .enum .text import WD_ALIGN_PARAGRAPH
1685+ from docx .enum .table import WD_TABLE_ALIGNMENT
1686+ except ImportError as e : # pragma: no cover
1687+ raise ImportError (
1688+ "python-docx required for to_word(). "
1689+ "Install: pip install python-docx"
1690+ ) from e
1691+ from ..output ._aer_style import (
1692+ apply_word_booktab_rules ,
1693+ style_word_table_typography ,
1694+ add_word_notes_paragraph ,
1695+ )
1696+
1697+ doc = Document ()
1698+ title = caption or f"{ self .method } — { self .estimand } Estimates"
1699+ p = doc .add_paragraph ()
1700+ run = p .add_run (title )
1701+ run .bold = True
1702+ run .font .size = Pt (12 )
1703+ run .font .name = "Times New Roman"
1704+ p .alignment = WD_ALIGN_PARAGRAPH .CENTER
1705+
1706+ def _write_df_to_table (df : pd .DataFrame ) -> None :
1707+ n_rows = len (df ) + 1
1708+ n_cols = len (df .columns ) + 1
1709+ table = doc .add_table (rows = n_rows , cols = n_cols )
1710+ table .alignment = WD_TABLE_ALIGNMENT .CENTER
1711+ table .autofit = True
1712+ table .rows [0 ].cells [0 ].text = ""
1713+ for j , col in enumerate (df .columns , 1 ):
1714+ table .rows [0 ].cells [j ].text = str (col )
1715+ for i , (idx , row ) in enumerate (df .iterrows ()):
1716+ table .rows [i + 1 ].cells [0 ].text = str (idx )
1717+ for j , val in enumerate (row , 1 ):
1718+ if isinstance (val , float ):
1719+ cell_text = f"{ val :.{digits }f} " if np .isfinite (val ) else ""
1720+ elif pd .isna (val ):
1721+ cell_text = ""
1722+ else :
1723+ cell_text = str (val )
1724+ table .rows [i + 1 ].cells [j ].text = cell_text
1725+ style_word_table_typography (
1726+ table , header_rows = (0 ,),
1727+ header_pt = 10 , body_pt = 9 ,
1728+ align_first_col = "left" , align_data_cols = "center" ,
1729+ )
1730+ apply_word_booktab_rules (table , header_top_idx = 0 , header_bot_idx = 0 )
1731+
1732+ # Estimates block — always present
1733+ tidy = self .tidy ().round (digits ).set_index (
1734+ self .tidy ().columns [0 ]
1735+ ) if "term" in self .tidy ().columns or "estimand" in self .tidy ().columns else \
1736+ self .tidy ().round (digits )
1737+ _write_df_to_table (tidy )
1738+
1739+ # Detail block — group/time ATTs, etc.
1740+ if self .detail is not None and len (self .detail ) > 0 :
1741+ doc .add_paragraph ().add_run ("Detail" ).bold = True
1742+ detail_view = self .detail .round (digits )
1743+ if not detail_view .index .is_unique or detail_view .index .equals (
1744+ pd .RangeIndex (len (detail_view ))
1745+ ):
1746+ detail_view = detail_view .reset_index (drop = True )
1747+ _write_df_to_table (detail_view )
1748+
1749+ notes = (
1750+ "Standard errors in parentheses. "
1751+ "* p<0.1, ** p<0.05, *** p<0.01. "
1752+ f"Observations: { self .n_obs :,} . Method: { self .method } ."
1753+ )
1754+ add_word_notes_paragraph (doc , notes )
1755+ doc .save (path )
1756+ return path
1757+
15631758 def to_latex (self , caption : Optional [str ] = None ,
15641759 label : Optional [str ] = None ) -> str :
15651760 """Generate a LaTeX table of the results."""
0 commit comments