modelflowib 2.73__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
modeldisplay.py ADDED
@@ -0,0 +1,550 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Mon Apr 8 14:58:26 2024
4
+
5
+ @author: ibhan
6
+
7
+ The `modeldisplay` module facilitates the generation, management, and display of data visualizations and tables
8
+ derived from ModelFlow models. It is designed to support a wide range of output formats including LaTeX documents,
9
+ HTML, and interactive IPyWidgets, making it highly versatile for different reporting and analysis needs in Jupyter
10
+ notebook environments.
11
+
12
+ This module provides a structured and extendible approach to organizing and rendering data analysis results, enabling users to
13
+ easily convert complex model outputs into digestible visual representations and tables. It is especially useful
14
+ for automating the reporting process in data analysis, financial modeling, and research projects, leveraging Python's
15
+ capabilities for data processing and visualization along with advanced document presentation features.
16
+
17
+ Key Features:
18
+ - Dynamic generation of LaTeX and HTML content for integrating data visualizations and tables into reports and
19
+ presentations.
20
+ - Compatibility with IPyWidgets for creating interactive, widget-based displays that enhance the interactivity of
21
+ Jupyter notebooks.
22
+ - Seamless integration with matplotlib for figure generation and pandas for table formatting, providing a comprehensive
23
+ toolkit for data display.
24
+ - Customizable display options and specifications through dataclasses, allowing for tailored presentation styles
25
+ and formats.
26
+
27
+ Classes:
28
+ - `Options`: Configures display options for managing how data and figures are presented, including naming conventions,
29
+ formatting preferences, and title settings.
30
+ - `Line`: Defines line configurations for table displays, supporting various data representation and difference
31
+ calculations to suit different analysis needs.
32
+ - `DisplaySpec`: Groups display options and line configurations, facilitating the management of complex display setups
33
+ in a structured manner.
34
+ - `DisplayDef`: Base class for display definitions, capable of compiling various display components into cohesive
35
+ specifications for rendering.
36
+ - `LatexRepo`: Handles the generation of LaTeX content, compilation into PDFs, and embedding within Jupyter notebooks,
37
+ supporting both static and dynamic content creation.
38
+ - `DisplayVarTableDef`: Specializes in displaying variable tables, automating the creation and formatting of tables
39
+ from ModelFlow model outputs.
40
+ - `DisplayFigWrapDef`: Focuses on wrapping and adjusting matplotlib figures for inclusion in various display formats,
41
+ ensuring figures are presentation-ready.
42
+
43
+ The `modeldisplay` module bridges the gap between analytical modeling and result presentation, offering a streamlined
44
+ workflow for transforming ModelFlow model outputs into high-quality visual and tabular displays suitable for a wide
45
+ range of purposes.
46
+ """
47
+
48
+
49
+
50
+
51
+
52
+ import pandas as pd
53
+ import matplotlib.pyplot as plt
54
+ import matplotlib as mpl
55
+ import seaborn as sns
56
+ import fnmatch
57
+ from matplotlib import dates
58
+ import matplotlib.ticker as ticker
59
+ from IPython.display import display
60
+ from dataclasses import dataclass, field
61
+ from typing import Any, List, Dict, Optional
62
+
63
+ from subprocess import run
64
+ from pathlib import Path
65
+ import webbrowser as wb
66
+
67
+ @dataclass
68
+ class Options:
69
+ """
70
+ Represents configuration options for data display definitions.
71
+
72
+ Attributes:
73
+ name (str) : name for this display. Default is 'display'
74
+ foot (str) : footer if relevant
75
+ rename (bool): If True, allows renaming of data columns. Default is True.
76
+ decorate (bool): If True, decorates row descriptions based on the showtype. Default is True.
77
+ width (int): Specifies the width for formatting output in characters. Default is 20.
78
+ custom_description (Dict): Custom description to augment or override default descriptions. Empty by default.
79
+ title (str): Text for the title. Default is an empty string.
80
+ chunk_size (int): Specifies the number of columns per chunk in the display output. Default is 5.
81
+ timeslice (List): Specifies the time slice for data display. Empty by default.
82
+ """
83
+ name : str = 'display'
84
+ foot : str =''
85
+ rename: bool = True
86
+ decorate: bool = True
87
+ width: int = 20
88
+ custom_description: Dict = field(default_factory=dict)
89
+ title : str = ''
90
+ chunk_size : int = 5
91
+ timeslice : List = field(default_factory=list)
92
+
93
+
94
+
95
+ def __post_init__(self):
96
+ ...
97
+
98
+
99
+
100
+ @dataclass
101
+ class Line:
102
+ """
103
+ A dataclass for representing and validating line configurations for data display.
104
+
105
+ Attributes:
106
+ showtype (str): Specifies the type of data representation. Valid options are 'level', 'growth',
107
+ 'change', 'basedf', 'textline', and 'gdppct'. Default is 'level'.
108
+ diftype (str): Specifies the type of difference calculation to apply. Valid options are 'nodif', 'dif',
109
+ 'difpct', 'basedf', and 'lastdf'. Default is 'nodif'.
110
+ centertext (str): Center text used when showtype is 'textline'. Default is a space.
111
+ rename (bool): If True, allows renaming of data columns. Default is True.
112
+ dec (int): Specifies the number of decimal places to use for numerical output. Default is 2.
113
+ pat (str): Pattern or identifier used to select data for the line. Default is '#Headline'.
114
+ latexfont (str) : Modifier used in lates for instande r'\textbf'
115
+ """
116
+
117
+ showtype: str = 'level'
118
+ diftype: str = 'nodif'
119
+ centertext : str = ''
120
+ rename: bool = False
121
+ dec: int = 2
122
+ pat : str = '#Headline'
123
+ latexfont :str =''
124
+
125
+ def __post_init__(self):
126
+ valid_showtypes = {'level', 'growth', 'change', 'basedf', 'gdppct' ,'textline'}
127
+ valid_diftypes = {'nodif', 'dif', 'difpct', 'basedf', 'lastdf'}
128
+
129
+ if self.showtype not in valid_showtypes:
130
+ raise ValueError(f"showtype must be one of {valid_showtypes}, got {self.showtype}")
131
+
132
+ if self.diftype not in valid_diftypes:
133
+ raise ValueError(f"diftype must be one of {valid_diftypes}, got {self.diftype}")
134
+
135
+
136
+ @dataclass
137
+ class DisplaySpec:
138
+ """
139
+ A dataclass to encapsulate display specifications including options and a list of line configurations.
140
+
141
+ Attributes:
142
+ options (Options): An instance of the Options dataclass specifying configuration options.
143
+ lines (List[Line]): A list of Line instances specifying individual line configurations.
144
+ """
145
+ options: Options = field(default_factory=Options)
146
+ lines: List[Line] = field(default_factory=list)
147
+
148
+
149
+ @dataclass
150
+ class DisplayDef:
151
+ mmodel : Any = None
152
+ spec : Options = field(default_factory=DisplaySpec)
153
+ name : str = ''
154
+
155
+
156
+ def __post_init__(self):
157
+ self.options = self.spec.options
158
+ self.lines = self.spec.lines
159
+ self.name = self.name if self.name else self.options.name
160
+ try:
161
+ self.var_description = self.mmodel.defsub(self.mmodel.var_description | self.options.custom_description )
162
+ except:
163
+ self.var_description = {}
164
+
165
+ try:
166
+ self.timeslice = self.options.timeslice if self.options.timeslice else self.mmodel.current_per
167
+ except:
168
+ self.timeslice = []
169
+
170
+
171
+ def get_rowdes(self,df,showtype,line):
172
+ if self.options.rename or line.rename:
173
+ rowdes = [self.var_description[v] for v in df.index]
174
+ else:
175
+ rowdes = [v for v in df.index]
176
+
177
+ if self.options.decorate :
178
+ match showtype:
179
+ case 'growth':
180
+ rowdes = [f'{des}, % growth' for des in rowdes]
181
+
182
+ case 'gdppct':
183
+ rowdes = [f'{des.split(",")[0].split("mill")[0]}, % of GDP' for des in rowdes]
184
+
185
+
186
+ case _:
187
+ rowdes = rowdes
188
+
189
+ df.index = rowdes
190
+ return df
191
+
192
+ @property
193
+ def df_str(self):
194
+ width = self.options.width
195
+ df = self.df.copy( )
196
+ format_decimal = [ line.dec for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
197
+ for i, dec in enumerate(format_decimal):
198
+ df.iloc[i] = df.iloc[i].apply(lambda x: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip() )
199
+ return df
200
+
201
+ @property
202
+ def df_str_disp(self):
203
+ center = [ (line.showtype == 'textline' and line.centertext !='' ) for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
204
+ center_index = [index+1 for index, value in enumerate(center) if value]
205
+
206
+ width = self.options.width
207
+ rawdata = self.df_str.to_string().split('\n')
208
+ data = center_title_under_years(rawdata,center_index)
209
+ out = '\n'.join(data)
210
+ return out
211
+
212
+ @property
213
+ def print(self):
214
+ if self.options.title:
215
+ print(self.options.title)
216
+ print(self.df_str_disp)
217
+
218
+ @property
219
+ def datawidget(self):
220
+ return self.make_html_style(self.displaydf.loc[:,self.timeslice],self.lines )
221
+
222
+
223
+
224
+
225
+
226
+ def pdf(self,xopen=False,show=True,width=800,height=600):
227
+ repo = LatexRepo(self.latex ,name=self.name)
228
+ return repo.pdf(xopen,show,width,height)
229
+
230
+
231
+ def __floordiv__(self, other):
232
+ if hasattr(other,'latex'):
233
+ # If the other object is an instance of LaTeXHolder, concatenate their LaTeX strings
234
+ return LatexRepo(latex =self.latex + '\n' + other.latex,name=self.name)
235
+ elif isinstance(other, str):
236
+ # If the other object is a string, assume it's a raw LaTeX string and concatenate
237
+ return LatexRepo(latex = self.latex + '\n' + other,name=self.name)
238
+ else:
239
+ # If the other object is neither a LaTeXHolder instance nor a string, raise an error
240
+ raise ValueError("Can only add another LaTeXHolder instance or a raw LaTeX string.")
241
+
242
+
243
+
244
+ def make_html_style(self,df,lines,use_tooltips =True ) :
245
+ out = self.df_str.style.set_table_styles([
246
+ {
247
+ 'selector': '.row_heading, .corner',
248
+ 'props': [
249
+ ('position', 'sticky'),
250
+ ('left', '0'),
251
+ ('z-index', '3'),
252
+ ('background-color', 'white'),
253
+ ('width', '300px'), # Set the width of the row headings
254
+ ('min-width', '200px'), # Ensure the minimum width is respected
255
+ ('max-width', '400px') # Ensure the maximum width is respected
256
+ ]
257
+ },
258
+ {
259
+ 'selector': '.col_heading',
260
+ 'props': [
261
+ ('position', 'sticky'),
262
+ ('top', '0'),
263
+ ('z-index', '2'),
264
+ ('background-color', 'white')
265
+ ]
266
+ },
267
+ {
268
+ 'selector': 'th',
269
+ 'props': [
270
+ ('text-align', 'left') , # Align text to the left
271
+ ('background-color', 'white'), # Ensuring headers are not transparent
272
+ ('z-index', '2') # Headers z-index on par with column headings
273
+ ]
274
+ },
275
+ {
276
+ 'selector': 'td', # Targeting data cells
277
+ 'props': [
278
+ ('z-index', '1'), # Lower z-index than headers
279
+ ]
280
+ },
281
+
282
+
283
+ {
284
+ 'selector': 'caption',
285
+ 'props': [
286
+ ('font-size', '16px'), # Make the font larger
287
+ ('font-weight', 'bold') # Make the font bold
288
+ ]
289
+ }
290
+ ],overwrite=True)
291
+ if use_tooltips:
292
+ tt = pd.DataFrame([[v for v in df.columns ]for t in df.index] ,index=df.index,columns=df.columns)
293
+ try:
294
+ out=out.set_tooltips(tt, props='visibility: hidden; position: absolute; z-index: 1; border: 1px solid #000066;'
295
+ 'background-color: white; color: #000066; font-size: 0.8em;width:100%'
296
+ 'transform: translate(0px, -24px); padding: 0.6em; border-radius: 0.5em;')
297
+ except Exception as e:
298
+ print(f'no tooltips {e}')
299
+
300
+ out= out.set_caption(self.options.title)
301
+
302
+ return out
303
+
304
+ @property
305
+ def latex(self):
306
+ rowlines = [ line for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
307
+ dfs = [self.df_str.iloc[:, i:i+self.options.chunk_size] for i in range(0, self.df_str.shape[1], self.options.chunk_size)]
308
+ outlist = []
309
+ for i,df in enumerate(dfs):
310
+ ncol=len(df.columns)
311
+ newindex = [fr'&\multicolumn{{{ncol}}}'+'{c}{' + f'{line.latexfont}' + '{' + df.index[i]+'}}'
312
+ if line.showtype == 'textline'
313
+ else df.index[i]
314
+ for i, line in enumerate(rowlines)]
315
+
316
+ df.index = newindex
317
+ tabformat = 'l'+'r'*ncol
318
+ outlist = outlist + [df.style.format(lambda x:x) \
319
+ .set_caption(self.options.title + ('' if i == 0 else ' - continued ')) \
320
+ .to_latex(hrules=True, position='ht', column_format=tabformat).replace('%',r'\%').replace('US$',r'US\$') ]
321
+
322
+ # print(outlist)
323
+ out = r' '.join(outlist)
324
+
325
+ out = '\n'.join(l.replace('& ','') if 'multicolum' in l else l for l in out.split('\n'))
326
+ return out
327
+
328
+
329
+ def _repr_html_(self):
330
+ return self.datawidget._repr_html_()
331
+
332
+
333
+
334
+ @dataclass
335
+ class LatexRepo:
336
+ latex: str = ""
337
+ name : str ='latex_test'
338
+
339
+ def set_name(self,name):
340
+ self.name = name
341
+ return self
342
+
343
+ def latexwrap(self):
344
+
345
+ latex_pre = r'''\documentclass{article}
346
+ \usepackage{booktabs}
347
+ \usepackage{caption} % Include the caption package
348
+ \captionsetup{justification=raggedright,singlelinecheck=false}
349
+ \usepackage{graphicx}
350
+ \usepackage{pgf}
351
+
352
+
353
+ \begin{document}
354
+
355
+ '''
356
+
357
+ latex_post = r'''
358
+ \end{document}
359
+ '''
360
+ out = latex_pre + self.latex + latex_post
361
+ return out
362
+
363
+
364
+
365
+ def pdf(self,xopen=False,show=True,width=800,height=600,morelatex = [] ):
366
+ from IPython.display import IFrame,display
367
+
368
+ latex_dir = Path(f'latex/{self.name}')
369
+ latex_dir.mkdir(parents=True, exist_ok=True)
370
+
371
+ latex_file = latex_dir / f'{self.name}.tex'
372
+ pdf_file = latex_dir / f'{self.name}.pdf'
373
+
374
+
375
+ # Now open the file for writing within the newly created directory
376
+ with open(latex_file, 'wt') as f:
377
+ f.write(self.latexwrap()) # Assuming tab.fulllatexwidget is the content you want to write
378
+ xx0 = run(f'latexmk -pdf -dvi- -ps- -f {self.name}.tex' ,cwd = f'{latex_dir}')
379
+ if xx0.returncode:
380
+ wb.open(latex_dir.absolute(), new=1)
381
+
382
+ raise Exception(f'Error creating PDF file, {xx0.returncode}, look in the latex folder')
383
+
384
+ if xopen:
385
+ wb.open(pdf_file , new=2)
386
+
387
+ if show:
388
+ return IFrame(pdf_file, width=width, height=height)
389
+
390
+
391
+ def __floordiv__(self, other):
392
+ if isinstance(other,str):
393
+ other_latex = other
394
+ else:
395
+ if hasattr(other,'latex'):
396
+ other_latex= other.latex
397
+ else:
398
+ raise Exception('Trying to join latex from object without latex content ')
399
+ out = LatexRepo(self.latex + other_latex )
400
+ return out
401
+
402
+
403
+ def _repr_html_(self):
404
+ self.pdf(show=False)
405
+ pdf_file = f"latex/{self.name}/{self.name}.pdf"
406
+ return f'<iframe src="{pdf_file}" width="800" height="600"></iframe>'
407
+
408
+
409
+
410
+ @dataclass
411
+ class DisplayVarTableDef(DisplayDef):
412
+
413
+
414
+ def __post_init__(self):
415
+ super().__post_init__() # Call the parent class's __post_init__
416
+
417
+
418
+
419
+ self.dfs = [self.make_var_df(line) for line in self.lines ]
420
+ self.df = pd.concat( self.dfs )
421
+ self.displaydf = pd.concat( self.dfs )
422
+ return
423
+
424
+
425
+ def make_var_df(self, line):
426
+ showtype = line.showtype
427
+ diftype = line.diftype
428
+
429
+ with self.mmodel.keepswitch(switch=True):
430
+
431
+ # Pre-process for cases that use linevars and linedes
432
+ if showtype in ['textline']:
433
+ linedf = pd.DataFrame(float('nan'), index=self.mmodel.current_per, columns=[line.centertext]).T
434
+
435
+ else:
436
+ def getline(start_ofset= 0,**kvargs):
437
+ locallinedfdict = self.mmodel.keep_get_plotdict_new(pat=line.pat,showtype=showtype,
438
+ diftype = diftype,keep_dim=False)
439
+ if diftype == 'basedf':
440
+ locallinedf = next(iter((locallinedfdict.values()))).T
441
+ else:
442
+ locallinedf = next(iter(reversed(locallinedfdict.values()))).T
443
+
444
+ return locallinedf.loc[:,self.mmodel.current_per]
445
+
446
+ linedf = getline()
447
+ linedf = self.get_rowdes(linedf,showtype,line)
448
+
449
+ return(linedf)
450
+
451
+
452
+ @dataclass
453
+ class DisplayFigWrapDef(DisplayDef):
454
+
455
+ figs : Dict = field(default_factory=dict)
456
+ extensions : List[str] = field(default_factory=lambda: ['svg','pgf'])
457
+
458
+
459
+ def __post_init__(self):
460
+ super().__post_init__() # Call the parent class's __post_init__
461
+
462
+
463
+ self.titledic = {chart: fig.axes[0].get_title() for chart,fig in self.figs.items() }
464
+
465
+ self.newfigs= {chart : fig for chart,fig in self.figs.items() }
466
+ for fig in self.newfigs.values():
467
+ fig.axes[0].set_title('')
468
+
469
+
470
+ self.mmodel.savefigs(figs=self.newfigs, location = './latex',
471
+ experimentname = self.name ,extensions= self.extensions
472
+ ,xopen=False)
473
+ self.charts = list(self.newfigs.keys() )
474
+
475
+
476
+ return
477
+
478
+ def figwrap(self,chart):
479
+ latex_dir = Path(f'../{self.name}')
480
+
481
+ out = r'''
482
+ \begin{figure}[htbp]
483
+ \centering
484
+ \resizebox{\textwidth}{!}{\input{'''
485
+ out = out + fr'{(latex_dir / chart).as_posix()}.pgf'+'}}'
486
+ out = out + fr'''
487
+ \caption{{{self.titledic[chart]}}}
488
+ \end{{figure}}
489
+ '''
490
+ return out
491
+
492
+ @property
493
+ def latex(self):
494
+ latex_dir = Path(f'../{self.name}')
495
+
496
+ out = '\n'.join( self.figwrap(chart) for chart in self.charts)
497
+ return out
498
+
499
+
500
+ def tab_growth(mmodel = None, pat='#Headline',title='Growth',dif=False,custom_description = {},diftext = 'Impact '):
501
+
502
+ if dif==True:
503
+ diftype = 'dif'
504
+ ldiftext = diftext
505
+ else:
506
+ diftype = 'nodif'
507
+ ldiftext = ''
508
+
509
+ tabspec = DisplaySpec(
510
+ options = Options(decorate=False,rename=True,name='A_small_table',
511
+ custom_description=custom_description,title =title,width=5),
512
+ lines = [
513
+ Line(showtype='textline',centertext=f'--- {ldiftext} Percent growth ---'),
514
+ Line(showtype='growth' ,pat=pat,diftype=diftype ) ,
515
+ Line(showtype='textline'),
516
+ ]
517
+ )
518
+ tab = DisplayVarTableDef (mmodel=mmodel, spec = tabspec)
519
+ return tab
520
+
521
+
522
+ def center_title_under_years(data, title_row_index=[1]):
523
+ """
524
+ Center a title (specified by its index in the list) under the years row in a list of strings.
525
+
526
+ :param data: List of strings representing the data.
527
+ :param title_row_index: Index of the title row in the list. Defaults to 1.
528
+ :return: A new list of strings with the centered title.
529
+ """
530
+ # Make a shallow copy of the list to avoid modifying the original list
531
+ adjusted_data = data.copy()
532
+
533
+ # Find the start and end indices of the year values in the first row
534
+ year_row = adjusted_data[0]
535
+ start_index = len(year_row) - len(year_row.lstrip())
536
+ end_index = len(year_row.rstrip())
537
+
538
+ # Calculate the total space available for centering
539
+ total_space = end_index - start_index
540
+
541
+ # Center the title within this space
542
+ for row_index in title_row_index:
543
+ title = adjusted_data[row_index].strip() # Remove leading and trailing spaces
544
+ centered_title = title.center(total_space)
545
+
546
+ # Replace the original title in the list with the centered title
547
+ # Ensuring that the centered title is positioned correctly relative to the entire line
548
+ adjusted_data[row_index] = f"{year_row[:start_index]}{centered_title}{year_row[end_index:]}"
549
+
550
+ return adjusted_data