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.
modelvis.py ADDED
@@ -0,0 +1,1038 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Fri May 12 11:07:02 2017
4
+
5
+ @author: hanseni
6
+
7
+ This module creates functions and classes for visualizing results.
8
+
9
+
10
+ """
11
+
12
+
13
+ import pandas as pd
14
+ import matplotlib.pyplot as plt
15
+ import matplotlib as mpl
16
+ import seaborn as sns
17
+ import fnmatch
18
+ from matplotlib import dates
19
+ import matplotlib.ticker as ticker
20
+ from IPython.display import display
21
+ from dataclasses import dataclass, field
22
+ from typing import Any, List, Dict, Optional
23
+
24
+ from subprocess import run
25
+ from pathlib import Path
26
+ import webbrowser as wb
27
+ import types
28
+
29
+
30
+
31
+ import numpy
32
+
33
+ from modelhelp import cutout,finddec
34
+
35
+ ##%%
36
+ def meltdim(df,dims=['dima','dimb'],source='Latest'):
37
+ ''' Melts a wide dataframe the variable names are split to dimensions acording
38
+ to the list of texts in dims. in variablenames
39
+ the tall dataframe have a variable name for each dimensions
40
+ also values and source are introduced ac column names in the dataframe '''
41
+ splitstring = (r'\|').join(['(?P<'+d+'>[A-Z0-9_]*)' for d in dims])
42
+ melted = pd.melt(df.reset_index().rename(columns={'index':'quarter'}),id_vars='quarter')
43
+ vardf = (melted
44
+ .assign(source=source)
45
+ .assign(varname = lambda df_ :df_.variable.str.replace('__','|',len(dims)-1)) # just to make the next step more easy
46
+ .pipe(lambda df_ : pd.concat([df_ ,df_.varname.str.extract(splitstring,expand=True)],axis=1)))
47
+ return vardf
48
+
49
+
50
+
51
+ class DummyVis:
52
+ def __init__(self, *args, **kwargs):
53
+ pass
54
+
55
+ def __getattr__(self, name):
56
+ # Ignore special methods for Jupyter's display system and other internal methods
57
+ ignored_methods = [
58
+ '_ipython_canary_method_should_not_exist_',
59
+ '_repr_', # covers all _repr_*_ methods
60
+ '__', # covers all special __*__ methods
61
+ ]
62
+ if any(name.startswith(prefix) for prefix in ignored_methods):
63
+ return lambda *args, **kwargs: None
64
+
65
+ # Return a callable that prints a message and returns another DummyVis instance
66
+ def dummy_method(*args, **kwargs):
67
+ # print(f"Attempt to call '{name}' on an uninitialized vis instance.")
68
+ return DummyVis()
69
+
70
+ return dummy_method
71
+
72
+ def __call__(self, *args, **kwargs):
73
+ # Define behavior when the instance is called as a function
74
+ # print("DummyVis instance called as a function.")
75
+ return DummyVis
76
+
77
+ def __repr__(self):
78
+ return "<Try again>"
79
+
80
+
81
+ class vis():
82
+ ''' Visualization class. used as a method on a model instance.
83
+
84
+ The purpose is to select variables acording to a pattern, potential with wildcards
85
+ '''
86
+ def __init__(self, model=None, pat='',names=None,df=None):
87
+ self.model = model
88
+ self.__pat__ = pat
89
+ if type(names) == type(None):
90
+ self.names = self.model.vlist(self.__pat__)
91
+ else:
92
+ self.names = names
93
+
94
+ if not len(self.names):
95
+ raise ValueError(f'The variable specification:"{pat}" did not generate any matches')
96
+
97
+
98
+ if isinstance(df,pd.DataFrame):
99
+ self.thisdf = df
100
+ else:
101
+ try:
102
+ self.thisdf = self.model.lastdf.loc[:,self.names]
103
+ except Exception as e:
104
+ print(e)
105
+ print('No data in the model instance, so only structure information from model.[<something>] ')
106
+
107
+
108
+
109
+
110
+
111
+ return
112
+
113
+
114
+
115
+ def explain(self,**kwargs):
116
+ for var in self.names:
117
+ x = self.model.explain(var,**kwargs)
118
+ return
119
+
120
+ def draw(self,**kwargs):
121
+ for var in self.names:
122
+ x = self.model.draw(var,**kwargs)
123
+
124
+ def dekomp(self,**kwargs):
125
+ self.model.dekomp.cache_clear()
126
+ for var in self.names:
127
+ x = self.model.dekomp(var,**kwargs)
128
+
129
+ def heat(self,*args, **kwargs):
130
+ ''' Displays a heatmap of the resulting dataframe'''
131
+
132
+ name = kwargs.pop('title',self.__pat__)
133
+ a = heatshow(self.thisdf.loc[self.model.current_per,:].T,
134
+ name=name,*args, **kwargs)
135
+ display(a)
136
+ return a
137
+
138
+ def plot(self,*args, **kwargs):
139
+ ''' Displays a plot for each of the columns in the resulting dataframe '''
140
+
141
+ name = kwargs.get('title','Title')
142
+ a = plotshow(self.thisdf.loc[self.model.current_per,:],
143
+ name=name,*args,**kwargs)
144
+ display(a)
145
+ return a
146
+
147
+
148
+ def rplot(self,*args,**kwargs):
149
+ """
150
+ Generates a table display configuration based on specified parameters and data types, including dynamic
151
+ adjustments of display options using both standard and keyword arguments.
152
+
153
+ Parameters:
154
+ pat (str): Pattern or identifier used to select data for the line, defaulting to '#Headline'.
155
+ title (str): Title of the table, passed directly to Options, defaulting to 'Table'.
156
+ datatype (str): Type of data transformation to apply (e.g., 'growth', 'level'), defaulting to 'growth'.
157
+ custom_description (dict): Custom descriptions to augment or override default descriptions, empty by default.
158
+ dec (int): Number of decimal places for numerical output, passed directly to Line configuration, defaulting to 2.
159
+ heading (str): Optional heading line for the table, empty by default.
160
+ name (str): Name for the display, defaults to 'A_small_table'.
161
+ foot (str): Footer text.
162
+ rename (bool): Allows renaming of data columns
163
+ decorate (bool): Decorates row descriptions based on the showtype, defaulting to False.
164
+ width (int): Specifies the width for formatting output in characters, efaulting to 5.
165
+ chunk_size (int): Number of columns per chunk in the display output, defaulting to 0.
166
+ timeslice (List[int]): Time slice for data display, empty by default.
167
+ max_cols (int): Maximum columns when displayed as a string, faulting to the system wide setting.
168
+ last_cols (int): Specifies the number of last columns to include in a display slice, particularly in Latex.
169
+ col_desc (str): text centered on columns
170
+
171
+ Returns:
172
+ DisplayVarTableDef: Configured table definition object ready for rendering, which includes detailed specifications
173
+ such as units and type of transformation based on the datatype.
174
+
175
+
176
+
177
+ """
178
+
179
+
180
+
181
+ return self.model.plot(self.names,*args,**kwargs)
182
+
183
+ def rtable(self,*args,**kwargs):
184
+ """
185
+ Generates a table display configuration based on specified parameters and data types, including dynamic
186
+ adjustments of display options using both standard and keyword arguments.
187
+
188
+ Parameters:
189
+ pat (str): Pattern or identifier used to select data for the line, defaulting to '#Headline'.
190
+ datatype (str): Type of data transformation to apply (e.g., 'growth', 'level'), defaulting to 'growth'.
191
+ title (str): Title of the table, passed directly to Options, defaulting to 'Table'.
192
+ custom_description (dict): Custom descriptions to augment or override default descriptions, empty by default.
193
+ ncol (int):
194
+ Returns:
195
+ DisplayVarTableDef: Configured table definition object ready for rendering, which includes detailed specifications
196
+ such as units and type of transformation based on the datatype.
197
+
198
+
199
+
200
+ """
201
+ return self.model.table(self.names,*args,**kwargs)
202
+
203
+ def plot_alt(self,title='Title',*args, **kwargs):
204
+ ''' Displays a plot for each of the columns in the resulting dataframe '''
205
+
206
+ if hasattr(self.model,'var_description'):
207
+ vtrans = self.model.var_description
208
+ else:
209
+ vtrans = {}
210
+ a = vis_alt(self.model.basedf.loc[self.model.current_per,self.names].rename(columns=vtrans) ,
211
+ self.model.lastdf.loc[self.model.current_per,self.names].rename(columns=vtrans) ,
212
+ title=title,*args,**kwargs)
213
+ return a
214
+
215
+ def box(self):
216
+ ''' Displays a boxplot comparing basedf and lastdf '''
217
+ return compvis(model=self.model,pat=self.__pat__).box()
218
+ def violin(self):
219
+ ''' Displays a violinplot comparing basedf and lastdf '''
220
+ return compvis(model=self.model,pat=self.__pat__).violin()
221
+ def swarm(self):
222
+ ''' Displays a swarmlot comparing basedf and lastdf '''
223
+ return compvis(model=self.model,pat=self.__pat__).swarm()
224
+
225
+
226
+ @property
227
+ def df(self):
228
+ ''' Returns the result of this instance as a dataframe'''
229
+ return self.thisdf.loc[self.model.current_per,:]
230
+
231
+ @property
232
+ def base(self):
233
+ ''' Returns basedf '''
234
+ return vis(model=self.model,df=self.model.basedf.loc[:,self.names],pat=self.__pat__)
235
+
236
+ @property
237
+ def pct(self):
238
+ '''Returns the pct change'''
239
+ return vis(model=self.model,df=self.thisdf.loc[:,self.names].pct_change()*100.,pat=self.__pat__)
240
+ @property
241
+ def growth(self):
242
+ '''Returns the pct growth'''
243
+ return vis(model=self.model,df=self.thisdf.loc[:,self.names].pct_change()*100.,pat=self.__pat__)
244
+
245
+ @property
246
+ def year_pct(self):
247
+ '''Returns the pct change over 4 periods (used for quarterly data) '''
248
+ return vis(model=self.model,df=self.thisdf.loc[:,self.names].pct_change(periods=4)*100..loc[:,self.names],pat=self.__pat__)
249
+
250
+ @property
251
+ def yoy_growth(self):
252
+ '''Returns the pct change over 4 periods (used for quarterly data) '''
253
+ return vis(model=self.model,df=self.thisdf.loc[:,self.names].pct_change(periods=4)*100..loc[:,self.names],pat=self.__pat__)
254
+
255
+ @property
256
+ def qoq_ar(self):
257
+ '''Returns the pct change over 4 periods (used for quarterly data) '''
258
+ df = ((1.+self.thisdf.loc[:,self.names].pct_change().loc[:,self.names])**4-1.)**100.
259
+ return vis(model=self.model,df=df,pat=self.__pat__)
260
+
261
+
262
+ @property
263
+ def frml(self):
264
+ '''Returns formulas '''
265
+ def getfrml(var,l):
266
+ if var in self.model.endogene:
267
+ t=self.model.allvar[var]['frml'].replace('\n',' ').replace(' ',' ')
268
+ return f'{var:<{l}} : {t}'
269
+ else:
270
+ return f'{var:{l}} : Exogenous'
271
+ mlength = max([len(v) for v in self.names])
272
+ out = '\n'.join(getfrml(var,mlength) for var in self.names)
273
+ print(out)
274
+
275
+ @property
276
+ def des(self):
277
+ '''Returns variable descriptions '''
278
+ def getdes(var,l):
279
+ return f'{var:<{l}} : {self.model.var_description[var]}'
280
+ mlength = max([len(v) for v in self.names])
281
+
282
+ out = '\n'.join(getdes(var,mlength) for var in self.names)
283
+ print(out)
284
+
285
+ @property
286
+ def eviews(self):
287
+ '''Returns variable descriptions '''
288
+ nl = '\n'
289
+ def geteviews(var,l):
290
+ if var in self.model.endogene:
291
+ ev = self.model.eviews_dict.get(var,'Not avaible')
292
+ return f'{var:<{l}} : {nl}{ev}'
293
+ else:
294
+ return f'{var:<{l}} : Exogen'
295
+
296
+ mlength = max([len(v) for v in self.names])
297
+ out = '\n \n'.join(geteviews(var,mlength) for var in self.names)
298
+ print(out)
299
+
300
+
301
+ @property
302
+ def dif(self):
303
+ ''' Returns the differens between the basedf and lastdf'''
304
+ difdf = self.thisdf-self.model.basedf.loc[:,self.names]
305
+ return vis(model=self.model,df=difdf,pat=self.__pat__)
306
+
307
+ @property
308
+ def difpctlevel(self):
309
+ ''' Returns the differens between the basedf and lastdf in percent '''
310
+ difdf = ((self.thisdf-self.model.basedf.loc[:,self.names])/ self.model.basedf.loc[:,self.names])*100.
311
+ return vis(model=self.model,df=difdf,pat=self.__pat__)
312
+
313
+ @property
314
+ def difpct(self):
315
+ ''' Returns the differens between the pct changes in basedf and lastdf'''
316
+ difdf = (self.thisdf.pct_change()-self.model.basedf.loc[:,self.names].pct_change())*100.
317
+ return vis(model=self.model,df=difdf,pat=self.__pat__)
318
+ @property
319
+ def difgrowth(self):
320
+ ''' Returns the differens between the pct changes in basedf and lastdf'''
321
+ difdf = (self.thisdf.pct_change()-self.model.basedf.loc[:,self.names].pct_change())*100.
322
+ return vis(model=self.model,df=difdf,pat=self.__pat__)
323
+
324
+ @property
325
+ def endo(self):
326
+ '''Selects only endogenous variables'''
327
+ endonames = [v for v in self.names if v in self.model.endogene]
328
+ thisdf = self.thisdf.loc[:,endonames]
329
+
330
+ return vis(model=self.model,df = thisdf, names= endonames, pat= self.__pat__ )
331
+ @property
332
+ def exo(self):
333
+ '''Selects only exogenous variables'''
334
+ endonames = [v for v in self.names if v in self.model.exogene]
335
+ thisdf = self.thisdf.loc[:,endonames]
336
+
337
+ return vis(model=self.model,df = thisdf, names= endonames, pat= self.__pat__ )
338
+
339
+ @property
340
+ def print(self):
341
+ ''' prints the current result'''
342
+ print('\n',self.thisdf.loc[self.model.current_per,:].to_string())
343
+ return
344
+
345
+ def __repr__(self):
346
+
347
+ # return self.thisdf.loc[self.model.current_per,:].to_string()
348
+
349
+ if self.model.in_notebook():
350
+ # from modelwidget import visshow
351
+ # visshow(self.model,self.__pat__)
352
+ return ''
353
+ else:
354
+ return self.thisdf.loc[self.model.current_per,:].to_string()
355
+
356
+
357
+
358
+ def _repr_html_(self):
359
+ '''Displays a nice summary of the results when called in a Jupyter enviorement'''
360
+
361
+ from modelwidget import visshow
362
+ pat = ' '.join(self.names)
363
+ # visshow(self.model,self.__pat__)
364
+ visshow(self.model,pat)
365
+ return ''
366
+
367
+ # return self.model.ibsstyle(self.thisdf.loc[self.model.current_per,:]).to_html(doctype_html=True)
368
+
369
+
370
+ # @property
371
+ # def show(self):
372
+ # if self.model.in_notebook():
373
+ # display(self.model.ibsstyle(self.thisdf.loc[self.model.current_per,:],transpose=True))
374
+ # else:
375
+ # print(self.thisdf.loc[self.model.current_per,:].to_string())
376
+ @property
377
+ def show(self):
378
+ if self.model.in_notebook():
379
+ pat = ' '.join(self.names)
380
+
381
+ from modelwidget import visshow
382
+ visshow(self.model,pat)
383
+ else:
384
+ print(self.thisdf.loc[self.model.current_per,:].to_string())
385
+
386
+
387
+ def __mul__(self,other):
388
+ ''' Multiply the curent result with other '''
389
+ muldf = self.thisdf * other
390
+ return vis(model=self.model,df=muldf,pat=self.__pat__)
391
+
392
+ def rename(self,other=None):
393
+ ''' rename columns '''
394
+ if type(other) == type(None):
395
+ if hasattr(self.model,'var_description'):
396
+ vtrans = self.model.var_description
397
+ else:
398
+ vtrans = {}
399
+ else:
400
+ vtrans = other
401
+ muldf = self.thisdf.rename(columns=vtrans)
402
+ return vis(model=self.model,df=muldf,pat=self.__pat__)
403
+
404
+ @property
405
+ def endo(self):
406
+ ''' only endogennous variables. columns '''
407
+ endovar = [v for v in self.names if v in self.model.endogene]
408
+ muldf = self.thisdf.loc[:,endovar]
409
+ # print(f'{endovar=}')
410
+ return vis(model=self.model,df=muldf,pat = ' '.join( endovar ))
411
+
412
+ @property
413
+ def endo_nofit(self):
414
+ ''' only endogennous variables. columns '''
415
+ endovar = [v for v in self.names if v in self.model.endogene and not v.endswith('_FITTED')]
416
+ muldf = self.thisdf.loc[:,endovar]
417
+ # print(f'{endovar=}')
418
+ return vis(model=self.model,df=muldf,pat = ' '.join( endovar ))
419
+
420
+
421
+ def mul(self,other):
422
+ ''' Multiply the curent result with other '''
423
+ return self.__mul__(other)
424
+
425
+ @property
426
+ def mul100(self):
427
+ '''Multiply the current result with 1, used to be 100- '''
428
+ raise Exception ('mul100 cnat be used any more')
429
+ return self.__mul__(1.0)
430
+
431
+ def __getattr__(self, name):
432
+ if name.startswith('_'):
433
+ return
434
+
435
+
436
+ allowed = {
437
+ 'names' : 'Variable names',
438
+ 'des' : 'Descriptions',
439
+ 'frml' : 'Normalized equations',
440
+ 'eviews':'Eviews equations',
441
+ 'base' : 'basedf values',
442
+ 'growth' : 'Growth',
443
+ 'difgrowth' : 'Difference in growth',
444
+ 'dif' : 'Diffference in values',
445
+ 'difpctlevel' : 'Diffference in pct in values',
446
+ 'difgrowth' : 'Difgrowth',
447
+ 'yoy_growth' : 'Year on year growth (quarterly data)',
448
+ 'qoq_ar' : 'Quarter to quarter annual rate growth (quarterly data)',
449
+ 'show':'Display widget with summary of variables - default output in jupyter',
450
+ 'df':'Returns DataFrame',
451
+ 'print':'Print DataFrame as string',
452
+ 'plot()':'Plot the results',
453
+ 'rplot()':"Report plot variables - default datatype='growth'",
454
+ 'rtable()':"Report table variables - default datatype='growth'",
455
+ 'rename()':'Rename variables to their descriptions',
456
+ 'endo':'Limit to endogeneous variables',
457
+ 'endo_nofit':'Limit to endogeneous variables not ending in "_FITTED"',
458
+ 'exo':'Limit to exogenous variables',
459
+
460
+ }
461
+ print(f'<{name}> not allowed try one of these:')
462
+ # Find the maximum length of the keys for alignment
463
+ max_key_length = max(len(key) for key in allowed)
464
+
465
+ # Print each key-value pair aligned
466
+ for key, value in allowed.items():
467
+ print(f"{key:<{max_key_length}} : {value}")
468
+
469
+
470
+ class compvis() :
471
+ ''' Class to compare to runs in boxplots'''
472
+ def __init__(self, model=None, pat=None):
473
+ ''' Combines basedf and lastdf to one tall dataframe useful for the Seaborn library'''
474
+ self.model = model
475
+ self.__pat__ = pat
476
+ self.names = self.model.vlist(self.__pat__)
477
+ self.lastdf = self.model.lastdf.loc[self.model.current_per,self.names]
478
+ self.basedf = self.model.basedf.loc[self.model.current_per,self.names]
479
+ self.lastmelt = melt(self.lastdf,source='Scenario')
480
+ self.basemelt = melt(self.basedf ,source='Base')
481
+ self.melted = pd.concat([self.lastmelt,self.basemelt])
482
+ return
483
+
484
+ def box(self,*args, **kwargs):
485
+ '''Displays a boxplot'''
486
+ fig, ax = plt.subplots(figsize=(12,6))
487
+ ax = sns.boxplot(x='time',y='value',data=self.melted,hue='source',ax=ax)
488
+ ax.set_title(self.__pat__)
489
+ def swarm(self,*args, **kwargs):
490
+ '''Displays a swarmplot '''
491
+ fig, ax = plt.subplots(figsize=(12,6))
492
+ ax = sns.swarmplot(x='time',y='value',data=self.melted,hue='source',ax=ax)
493
+ ax.set_title(self.__pat__)
494
+ def violin(self,*args, **kwargs):
495
+ '''Displays a violinplot'''
496
+ fig, ax = plt.subplots(figsize=(12,6))
497
+ ax = sns.violinplot(x='time',y='value',data=self.melted,hue='source',ax=ax)
498
+ ax.set_title(self.__pat__)
499
+
500
+
501
+ class container():
502
+ '''A container, used if to izualize dataframes without a model'''
503
+
504
+ def __init__(self,lastdf,basedf):
505
+ self.lastdf = lastdf
506
+ self.basedf = basedf
507
+
508
+ def smpl(self,start='',end='',df=None):
509
+ ''' Defines the model.current_per which is used for calculation period/index
510
+ when no parameters are issues the current current period is returned \n
511
+ Either none or all parameters have to be provided '''
512
+ if start =='' and end == '':
513
+ pass
514
+ else:
515
+ istart,iend= self.lastdf.index.slice_locs(start,end)
516
+ per=self.lastdf.index[istart:iend]
517
+ self.current_per = per
518
+ return self.current_per
519
+ def vlist(self,pat):
520
+ '''returns a list of variable matching the pattern'''
521
+ if isinstance(pat,list):
522
+ ipat=pat
523
+ else:
524
+ ipat = [pat]
525
+ out = [v for p in ipat for v in sorted(fnmatch.filter(self.lastdf.columns,p.upper()))]
526
+ return out
527
+
528
+
529
+ ##%%
530
+ class varvis():
531
+ ''' Visualization class. used as a method on a model instance.
532
+
533
+ The purpose is to select variables acording to a pattern, potential with wildcards
534
+ '''
535
+ def __init__(self, model=None, var=''):
536
+ # print(f' varvis called {var=}')
537
+ self.model = model
538
+ self.var = var
539
+ if var not in model.allvar:
540
+ raise ValueError(f'The specification:"{var}" did not match a method, property or variable name')
541
+ self.endo = model.allvar[var]['endo']
542
+ def explain(self,**kwargs):
543
+ x = self.model.explain(self.var,**kwargs)
544
+ return x
545
+
546
+ def draw(self,**kwargs):
547
+ x = self.model.draw(self.var,**kwargs)
548
+
549
+ def tracedep(self,down=1,**kwargs):
550
+ '''Trace dependensies of name down to level down'''
551
+ self.model.draw(self.var,down=down,up=0,source=self.var,**kwargs)
552
+
553
+ def tracepre(self,up=1,**kwargs):
554
+ '''Trace dependensies of name down to level down
555
+ - `showdata|sd=True` will include a table of values for each variable
556
+ - `showdata|sd='pattern of variable names'` will include a table of values for each variable matching the pattern (including wildcharts
557
+ - `attshow|ats = True` will include a table of attributions for each variable
558
+ - `growthshow|gs = True` will include a table of growth for each variable
559
+ - `HR = True` will reorient the dependency graph
560
+ - `up = <integer>` will determine how many levels of parents to include
561
+ - `png = True` will display as a png picture
562
+ - `svg = True` will display as a svg picture which can be zoomed
563
+ - `pdf = True` will display as a pdf picture
564
+ - `eps = True` will create a eps file
565
+ - `browser = True` will open a browser with the resulting dependency graph - useful for zooming on a big graph
566
+ - `saveas = <a file name without extension>` will save the picture wit the filename with an added extension reflection the picture type
567
+
568
+
569
+ To allow the use of the display in presentations or publications The resulting file(s) are placed in the graph/subfolder
570
+ '''
571
+ self.model.draw(self.var,down=0,up=up,sink=self.var,**kwargs)
572
+
573
+ def dash(self,port=5001):
574
+ """
575
+ Launch a dashboard to trace dependencies of the specified variable.
576
+
577
+ This method initializes a dashboard to visualize and trace dependencies
578
+ of a specific variable within the model. The dashboard provides insights
579
+ into how the selected variable is influenced by other components at various
580
+ levels of the dependency hierarchy.
581
+
582
+ Parameters:
583
+ ----------
584
+ port : int, optional
585
+ The port on which to launch the dashboard. Default is 5001.
586
+
587
+ Returns:
588
+ -------
589
+ None
590
+ """
591
+ self.model.modeldash(self.var,port=port)
592
+
593
+
594
+
595
+ def get_att(self,start='',end='',dec=None,bare=True,**kwargs):
596
+ '''
597
+ Retrieve and display the attribution for a variable within a specified period.
598
+
599
+ Parameters:
600
+ start (str): Start date of the period (default: '').
601
+ end (str): End date of the period (default: '').
602
+ dec (int): Number of decimal places for formatting (default: None).
603
+ bare (bool): If True, display only the attribution result; if False, display both the difference and attribution results (default: True).
604
+ type (str) : One of 'pct', 'growth', 'level' (default: 'pct')
605
+ **kwargs: Additional keyword arguments for specifying attribution type and other options.
606
+
607
+ Returns:
608
+ None (displays the attribution result)
609
+
610
+ Note:
611
+ - The method retrieves the difference and attribution results using the specified period and attribution type.
612
+ - The `dec` parameter controls the number of decimal places for formatting. If not provided, the default number of decimal places is determined based on the attribution type.
613
+ - The `bare` parameter determines whether to display only the attribution result or both the difference and attribution results.
614
+ - The `type` parameter wether to display the level, pct or growth attribution determines whether to display level, growth or pct attribution
615
+ '''
616
+
617
+ diff = self.model.get_att_diff(self.var,start=start,end=end,**kwargs)
618
+ res = self.model.get_att(self.var,start=start,end=end,**kwargs)
619
+ percent=(kwargs.get('type','pct') in {'growth'}) or \
620
+ ((kwargs.get('type','pct') in {'pct'}) and bare)
621
+ if type(dec) == type(None):
622
+ xdec = 2 if (kwargs.get('type','pct') in {'level','growth'}) else 0
623
+ else:
624
+ xdec = dec
625
+ if bare:
626
+ out = res
627
+ out.index.name= 'Growth percent' if kwargs.get('type','pct') == 'growth' else 'Percent att.' if kwargs.get('type','pct') == 'pct' else 'Level att.'
628
+
629
+ else:
630
+ out = pd.concat([diff,res])
631
+ out.index.name= 'Growth percent' if kwargs.get('type','pct') == 'growth' else 'Level/percent' if kwargs.get('type','pct') == 'pct' else 'Level/level'
632
+
633
+ sout = self.model.ibsstyle(out,percent=percent,dec=xdec )
634
+ display(sout)
635
+
636
+
637
+ def dekomp(self,**kwargs):
638
+ if kwargs.get('lprint','False'):
639
+ self.model.dekomp.cache_clear()
640
+ x = self.model.dekomp(self.var,**kwargs)
641
+ return x
642
+
643
+ def var_des(self,var):
644
+ des = self.model.var_description[var]
645
+ return des if des != var else ''
646
+
647
+ def _showall_old(self,all=1,dif=0,last=0,show_all=True):
648
+ if self.endo:
649
+ des_string = self.model.get_eq_des(self.var,show_all)
650
+ out1,out2 = '',''
651
+ out0 = f'Endogeneous: {self.var}: {self.var_des(self.var)} \nFormular: {self.model.allvar[self.var]["frml"]}\n\n{des_string}\n'
652
+ try:
653
+ if dif:
654
+ out0 = out0+f'\nValues : \n{self.model.get_values(self.var)}\n'
655
+
656
+ if all:
657
+ out0 = out0+f'\nValues : \n{self.model.get_values(self.var)}\n'
658
+ out1 = f'\nInput last run: \n {self.model.get_eq_values(self.var)}\n\nInput base run: \n {self.model.get_eq_values(self.var,last=False)}\n'
659
+ elif last:
660
+ out0 = out0+f'\nValues : \n{self.model.get_values(self.var)}\n'
661
+ out1 = f'\nInput last run: \n {self.model.get_eq_values(self.var)}\n'
662
+ if all or dif:
663
+ out2 = f'\nDifference for input variables: \n {self.model.get_eq_dif(self.var,filter=False)}'
664
+ except Exception as e:
665
+ print(e)
666
+ pass
667
+ out=out0+out1+out2
668
+ else:
669
+ out = f'Exogeneous : {self.var}: {self.var_des(self.var)} \n Values : \n{self.model.get_values(self.var)}\n'
670
+ return out
671
+
672
+ # def ibsstyle(self,df,dec=2):
673
+ # ''' display a dataframe with tooltip'''
674
+
675
+ # tt = pd.DataFrame([[self.model.var_description[v] for c in df.columns] for v in df.index ],index=df.index,columns=df.columns)
676
+ # xdec = f'{dec}'
677
+ # result = df.style.format('{:.'+xdec+'f}').\
678
+ # set_tooltips(tt, props='visibility: hidden; position: absolute; z-index: 1; border: 1px solid #000066;'
679
+ # 'background-color: white; color: #000066; font-size: 0.8em;'
680
+ # 'transform: translate(0px, -24px); padding: 0.6em; border-radius: 0.5em;')
681
+ # return result
682
+
683
+ def _showall(self,all=1,dif=0,last=0,show_all=True):
684
+ from IPython.display import SVG, display, Image, IFrame, HTML, Markdown
685
+ if self.endo:
686
+ des_string = self.model.get_eq_des(self.var,show_all)
687
+ out0,out1,out2 = '','',''
688
+ print(f'Endogeneous: {self.var}: {self.var_des(self.var)}')
689
+ print(f'Formular: {self.model.allvar[self.var]["frml"]}\n\n{des_string}\n')
690
+ try:
691
+ if dif or all or last: print('Values :')
692
+ if dif or all or last: display(HTML(self.model.ibsstyle(self.model.get_values(self.var)).to_html() ))
693
+ if all or last: print('Input last run:')
694
+ if all or last: display(self.model.ibsstyle(self.model.get_eq_values(self.var)))
695
+ if all : print('Input base run:')
696
+ if all : display(self.model.ibsstyle(self.model.get_eq_values(self.var,last=False)))
697
+ if all or dif: print('Difference for input variables')
698
+ if all or dif: display(self.model.ibsstyle(self.model.get_eq_dif(self.var,filter=False)))
699
+ except Exception as e:
700
+ print(e)
701
+ pass
702
+ out=out0+out1+out2
703
+ else:
704
+ out = f'Exogeneous : {self.var}: {self.var_des(self.var)} \n Values : \n{self.model.get_values(self.var)}\n'
705
+ return out
706
+
707
+
708
+ @property
709
+ def show(self):
710
+ out = self._showall(all=1)
711
+ print(out)
712
+ return
713
+
714
+ @property
715
+ def showdif(self):
716
+ out = self._showall(all=0,dif=1)
717
+ print(out)
718
+ return
719
+ @property
720
+ def frml(self):
721
+ out = self._showall(all=0,dif=0)
722
+ print(out)
723
+ return
724
+ @property
725
+ def eviews(self):
726
+ out = self.model.eviews_dict.get(self.var,'Not avaiable')
727
+ print(out)
728
+ return
729
+
730
+
731
+ def __repr__(self):
732
+
733
+ out = self._showall(all=0,last=1)
734
+ return out
735
+
736
+ def __getattr__(self, name):
737
+ if name.startswith('_'):
738
+ return
739
+ allowed = {
740
+ 'show' : 'show frml and data',
741
+ 'showdif' : 'show oly differences ',
742
+ 'frml' : 'Normalized equations',
743
+ 'eviews':'Eviews equations',
744
+ 'dash' : 'causality dashboard',
745
+ 'get_att()' : 'get impact attrribution ',
746
+ 'tracepre()' : 'trace preceding variables',
747
+ 'tracedep()' : 'trace dependent variables',
748
+ }
749
+ print(f'<{name}> not allowed try one of these:')
750
+ # Find the maximum length of the keys for alignment
751
+ max_key_length = max(len(key) for key in allowed)
752
+
753
+ # Print each key-value pair aligned
754
+ for key, value in allowed.items():
755
+ print(f"{key:<{max_key_length}} : {value}")
756
+
757
+
758
+
759
+
760
+
761
+
762
+
763
+
764
+ def vis_alt(grund,mul,title='Show variables',ttop=None):
765
+ ''' Graph of one of more variables each variable is displayed for 3 banks'''
766
+ avar = grund.columns
767
+ antal=len(avar)
768
+ fig, axes = plt.subplots(nrows=antal, ncols=1,figsize=(15,antal*6)) #,sharex='col' ,sharey='row')
769
+ fig.suptitle(title, fontsize=20)
770
+ ax2 = [axes] if antal == 1 else axes
771
+ for i,(var,ax) in enumerate(zip(avar,ax2)):
772
+ grunddata = grund.loc[:,var]
773
+ muldata = mul.loc[:,var]
774
+ # breakpoint()
775
+ grunddata.plot(ax=ax,legend=False,fontsize=14)
776
+ muldata.plot (ax=ax,legend=False,fontsize=14)
777
+ ax.set_title(var,fontsize=14)
778
+ x_pos = grunddata.index[-1]
779
+ ax.text(x_pos, grunddata.values[-1],'Baseline',fontsize=14)
780
+ ax.text(x_pos, muldata.values[-1] ,'Alternative',fontsize=14)
781
+ ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda value,number: f'{value:,}'))
782
+
783
+ ax.xaxis.set_minor_locator(plt.NullLocator())
784
+ ax.tick_params(axis='x', labelleft=True)
785
+ fig.subplots_adjust(top=ttop if type(ttop) != type(None) else 0.98-(0.2/antal))
786
+
787
+ return fig
788
+
789
+
790
+ def plotshow(df,name='',ppos=-1,kind='line',colrow=2,sharey=False,top=None,
791
+ splitchar='__',savefig='',*args,**kwargs):
792
+ '''
793
+
794
+ Plots a subplot for each column in a datafra.
795
+ ppos determins which split by __ to use
796
+ kind determins which kind of matplotlib chart to use
797
+
798
+ Args:
799
+ df (TYPE): Dataframe .
800
+ name (TYPE, optional): title. Defaults to ''.
801
+ ppos (TYPE, optional): # of position to use if split. Defaults to -1.
802
+ kind (TYPE, optional): matplotlib kind . Defaults to 'line'.
803
+ colrow/ncol (TYPE, optional): columns per row . Defaults to 2.
804
+ sharey (TYPE, optional): Share y axis between plots. Defaults to True.
805
+ splitchar (TYPE, optional): if the name should be split . Defaults to '__'.
806
+ savefig (TYPE, optional): save figure. Defaults to ''.
807
+ xsize (TYPE, optional): x size default to 10
808
+ ysize (TYPE, optional): y size per row, defaults to 2
809
+
810
+ Returns:
811
+ a matplotlib fig.
812
+
813
+ note: ncol can be used instead of colrow to compatible with keep_plot
814
+
815
+ '''
816
+
817
+ xcolrow = kwargs.get('ncol',colrow)
818
+ plt.ioff()
819
+ if splitchar:
820
+ out=df.pipe(lambda df_: df_.rename(columns={v: v.split(splitchar)[ppos] for v in df_.columns}))
821
+ else:
822
+ out=df
823
+ number = out.shape[1]
824
+ row=-((-number)//xcolrow)
825
+ # breakpoint()
826
+
827
+ axes=out.plot(kind=kind,subplots=True,layout=(row,xcolrow),figsize = (kwargs.get('xsize',10), row*kwargs.get('ysize',2)),
828
+ use_index=True,title=name,sharey=sharey)
829
+ for ax in axes.flatten():
830
+ pass
831
+ # dec=finddec(dfatt)
832
+ # ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda value,number: f'{value:,}'))
833
+
834
+ ax.xaxis.set_minor_locator(plt.NullLocator())
835
+ ax.tick_params(axis='x', labelleft=True)
836
+ if out.index.dtype == 'int64':
837
+ fmtr = ticker.StrMethodFormatter('{x:.0f}')
838
+ ax.xaxis.set_major_formatter(fmtr)
839
+
840
+ fig = axes.flatten()[0].get_figure()
841
+ fig.set_constrained_layout(True)
842
+ fig.suptitle(name,fontsize=20)
843
+ # fig.tight_layout()
844
+
845
+ # top = (row*(2-0.1)-0.2)/(row*(2-0.1))
846
+ # print(top)
847
+ # fig.subplots_adjust(top=top if type(top) != type(None) else 0.98-(0.2/row))
848
+ if savefig:
849
+ fig.savefig(savefig)
850
+ # plt.subplot_tool()
851
+ plt.ion()
852
+ plt.close('all')
853
+ return fig
854
+
855
+ def melt(df,source='Latest'):
856
+ ''' melts a wide dataframe to a tall dataframe , appends a soruce column '''
857
+ melted = pd.melt(df.reset_index().rename(columns={'index':'time'}),id_vars='time').assign(source=source)
858
+ return melted
859
+
860
+ def heatshow(df,name='',cmap="Reds",mul=1.,annot=False,size=(11.69,8.27),dec=0,cbar=True,linewidths=.5):
861
+ ''' A heatmap of a dataframe problems in 3.12'''
862
+ xx=(df.astype('float'))*mul
863
+ # fig, ax = plt.subplots(figsize=(11,8))
864
+ fig, ax = plt.subplots(figsize=size) #A4
865
+ sns.heatmap(xx,cmap=cmap,ax=ax,fmt="."+str(dec)+"f",annot=annot,annot_kws ={"ha": 'center'},linewidths=linewidths,cbar=cbar)
866
+ ax.set_title(name, fontsize=20)
867
+
868
+ ax.set_yticklabels(ax.yaxis.get_majorticklabels(), ha = 'left',rotation=0)
869
+ yax = ax.get_yaxis()
870
+
871
+ yticklabels = ax.get_yticklabels()
872
+ widths = [label.get_window_extent().width for label in yticklabels]
873
+ pad = max(e for e in widths)
874
+
875
+ yax.set_tick_params(pad=pad)
876
+ ax.set_xticklabels(ax.xaxis.get_majorticklabels(), va = 'top' ,rotation=70.)
877
+ fig.subplots_adjust(bottom=0.15)
878
+ #ax.tick_paraOms(axis='y',direction='out', length=3, width=2, colors='b',labelleft=True)
879
+ plt.close('all')
880
+ return fig
881
+
882
+
883
+ ##%%
884
+ def attshow(df,treshold=False,head=5000,tail=0,t=True,annot=False,showsum=False,sort=True,size=(11.69,8.27),title='',
885
+ tshow=True,dec=0,cbar=True,cmap='jet',savefig=''):
886
+ '''Shows heatmap of impacts of exogeneous variables
887
+ :df: Dataframe with impact
888
+ :treshold: Take exogeneous variables with max impact of treshold or larger
889
+ :numhigh: take the numhigh largest impacts
890
+ :t: transpose the heatmap
891
+ :annot: Annotate the heatmap
892
+ :head: take the head largest
893
+ .tail: take the tail smalest
894
+ :showsum: Add a column with the sum
895
+ :sort: Sort the data
896
+ .tshow: Show a longer title
897
+ :cbar: if a colorbar shoud be displayes
898
+ :cmap: the colormap
899
+ :save: Save the chart (in png format) '''
900
+
901
+
902
+ selectmin = df.min().sort_values(ascending=False).tail(tail).index.tolist()
903
+ selectmax = df.max().sort_values(ascending=False).head(head).index.tolist()
904
+ select=selectmax+selectmin
905
+ yy = df[select].pipe(
906
+ lambda df_ : df_[select] if sort else df_[sorted(list(df_.columns))])
907
+ if showsum:
908
+ asum= yy.sum(axis=1)
909
+ asum.name = '_Sum'
910
+ yy = pd.concat([yy,asum],axis=1)
911
+
912
+ yy2 = yy.T if t else yy
913
+ if sort and tshow:
914
+ txt= ' Impact from exogeneous variables. '+(
915
+ str(head)+' highest. ' if head >= 1 else '')+( str(tail)+' smallest. ' if tail >= 1 else '')
916
+ else:
917
+ txt= ''
918
+ f=heatshow(yy2,cmap=cmap,name=title+txt ,annot=annot,mul=1.,size=size,dec=dec,cbar=cbar)
919
+ f.subplots_adjust(bottom=0.16)
920
+ if savefig:
921
+ f.savefig(savefig)
922
+ return yy2
923
+
924
+
925
+
926
+ def attshowone(df,name,pre='',head=5,tail=5):
927
+ ''' shows the contribution to row=name from each column
928
+ the coulumns can optional be selected as starting with pre'''
929
+
930
+ res = df.loc[name,[n for n in df.columns if n.startswith(pre)]].sort_values(ascending=False).pipe(
931
+ lambda df_: df_.head(head).append(df_.tail(tail)))
932
+ ax = res.plot(kind='bar')
933
+ txt= ( str(head)+' highest. ' if head >= 1 else '')+( str(tail)+' smallest. ' if tail >= 1 else '')
934
+ ax.set_title('Contributions to '+name+'. '+txt)
935
+ return ax
936
+
937
+
938
+ def water(serxinput,sort=False,ascending =True,autosum=False,allsort=False,threshold=0.0):
939
+ ''' Creates a dataframe with information for a watrfall diagram
940
+
941
+ :serx: the input serie of values
942
+ :sort: True if the bars except the first and last should be sorted (default = False)
943
+ :allsort: True if all bars should be sorted (default = False)
944
+ :autosum: True if a Total bar are added in the end
945
+ :ascending: True if sortorder = ascending
946
+
947
+ Returns a dataframe with theese columns:
948
+
949
+ :hbegin: Height of the first bar
950
+ :hend: Height of the last bar
951
+ :hpos: Height of positive bars
952
+ :hneg: Height of negative bars
953
+ :start: Ofset at which each bar starts
954
+ :height: Height of each bar (just for information)
955
+ '''
956
+ # get the height of first and last column
957
+
958
+
959
+ total=serxinput.sum()
960
+ serx = cutout(serxinput,threshold)
961
+ if sort or allsort : # sort rows except the first and last
962
+ endslice = None if allsort else -1
963
+ startslice = None if allsort else 1
964
+ i = serx[startslice:endslice].sort_values(ascending =ascending ).index
965
+ if allsort:
966
+ newi =i.tolist()
967
+ else:
968
+ newi =[serx.index.tolist()[0]] + i.tolist() + [serx.index.tolist()[-1]]# Get the head and tail
969
+ ser=serx[newi]
970
+ else:
971
+ ser=serx.copy()
972
+
973
+ if autosum:
974
+ ser['Total'] = total
975
+
976
+ ser = ser.astype('float')
977
+
978
+ hbegin = ser.copy()
979
+ hbegin[1:]=0.0
980
+ hend = ser.copy()
981
+ hend[:-1] = 0.0
982
+
983
+ height = ser
984
+ start = ser.cumsum().shift().fillna(0.0) # the starting point for each bar
985
+ start.iloc[-1] = start.iloc[-1] if allsort and not autosum else 0 # the last bar should start at 0
986
+ end = start + ser
987
+
988
+ hpos= height*(height>=0.0)
989
+ hneg= height*(height<=0.0)
990
+ dfatt = pd.DataFrame({'start':start,'hbegin':hbegin,'hpos':hpos,'hneg':hneg,'hend':hend,'height':height}).loc[ser.index,:]
991
+
992
+ return dfatt
993
+
994
+ def waterplot(basis,sort=True,ascending =True,autosum=False,bartype='bar',threshold=0.0,
995
+ allsort=False,title=f'Attribution ',top=0.9, desdic = {},zero=True, ysize=5,**kwarg):
996
+ att = [(name,water(ser,sort=sort,autosum=autosum,allsort=allsort,threshold=threshold))
997
+ for name,ser in basis.transpose().iterrows()]
998
+ # print(att[0][1])
999
+ fig, axis = plt.subplots(nrows=len(att),ncols=1,figsize=(10,ysize*len(att)),constrained_layout=True)
1000
+ width = 0.5 # the width of the barsser
1001
+ laxis = axis if isinstance(axis,numpy.ndarray) else [axis]
1002
+ for i,((name,dfatt),ax) in enumerate(zip(att,laxis)):
1003
+ _ = dfatt.hpos.plot(ax=ax,kind=bartype,bottom=dfatt.start,stacked=True,
1004
+ color='green',width=width)
1005
+ _ = dfatt.hneg.plot(ax=ax,kind=bartype,bottom=dfatt.start,stacked=True,
1006
+ color='red',width=width)
1007
+ _ = None if allsort else dfatt.hbegin.plot(ax=ax,kind=bartype,bottom=dfatt.start,
1008
+ stacked=True,color=('green' if dfatt.hbegin.iloc[0] > 0 else 'red') if zero else 'blue',width=width)
1009
+ _ = None if allsort and not autosum else dfatt.hend.plot(ax=ax,kind=bartype,bottom=dfatt.start,stacked=True,color='blue',width=width)
1010
+ ax.set_ylabel(name,fontsize='x-large')
1011
+ dec=finddec(dfatt)
1012
+ ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda value,number: f'{value:,.{dec}f}'))
1013
+
1014
+ ax.set_title(desdic.get(name,name))
1015
+ ax.set_xticklabels(dfatt.index.tolist(), rotation = 70,fontsize='x-large')
1016
+ # plt.xticks(rotation=45, horizontalalignment='right',
1017
+ # fontweight='light', fontsize='x-large' )
1018
+ fig.suptitle(title,fontsize=20)
1019
+ if 1:
1020
+ ...
1021
+ # plt.tight_layout()
1022
+ # fig.subplots_adjust(top=top)
1023
+
1024
+ # plt.show()
1025
+ plt.close('all')
1026
+
1027
+
1028
+ return fig
1029
+
1030
+ vis.plot.__doc__ = plotshow.__doc__
1031
+
1032
+
1033
+ if __name__ == '__main__' and 1:
1034
+ basis = pd.DataFrame([[100,100.],[-10.0,-12], [12,-10],[-10,10]],index=['nii','cost','credit','fee'],columns=['ex','ex2'])
1035
+ basis.loc['total'] = basis.sum()
1036
+ waterplot(basis)
1037
+
1038
+