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.
modelmf.py ADDED
@@ -0,0 +1,349 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+
4
+ This is a module for extending pandas dataframes with the modelflow toolbox
5
+
6
+ Created on Sat March 2019
7
+
8
+ @author: hanseni
9
+ """
10
+
11
+ import pandas as pd
12
+ import fnmatch
13
+
14
+
15
+
16
+ from modelclass import model
17
+ from modelhelp import debug_var
18
+
19
+
20
+ if not hasattr(pd.DataFrame,'mfcalc'):
21
+
22
+ @pd.api.extensions.register_dataframe_accessor("mfcalc")
23
+ class mfcalc():
24
+ '''
25
+ Used to carry out calculation specified as equations
26
+
27
+ Args:
28
+ eq (TYPE): Equations one on each line. can be started with <start end> to control calculation sample .
29
+ start (TYPE, optional): DESCRIPTION. Defaults to ''.
30
+ end (TYPE, optional): DESCRIPTION. Defaults to ''.
31
+ showeq (TYPE, optional): If True the equations will be printed. Defaults to False.
32
+ **kwargs (TYPE): Here all solve options can be provided.
33
+
34
+ Returns:
35
+ Dataframe.
36
+
37
+ '''
38
+ def __init__(self, pandas_obj):
39
+ # self._validate(pandas_obj)
40
+ self._obj = pandas_obj
41
+ # print(self._obj)
42
+
43
+ def __call__(self,eq,start='',end='',showeq=False, **kwargs):
44
+ '''
45
+ This call performs the calculation
46
+
47
+ Args:
48
+ eq (TYPE): Equations one on each line. can be started with <start end> to control calculation sample .
49
+ start (TYPE, optional): DESCRIPTION. Defaults to ''.
50
+ end (TYPE, optional): DESCRIPTION. Defaults to ''.
51
+ showeq (TYPE, optional): If True the equations will be printed. Defaults to False.
52
+ **kwargs (TYPE): Here all solve options can be provided.
53
+
54
+ Returns:
55
+ Dataframe.
56
+
57
+ '''
58
+
59
+ # print({**kwargs,**{'start':start,'end':end}})
60
+ if (l0 := eq.strip()).startswith('<'):
61
+ timesplit = l0.split('>', 1)
62
+ if len(timesplit) != 2:
63
+ raise Exception(f'Malformed time specification\nOffending: "{l0}"')
64
+
65
+ time_options = timesplit[0].replace('<', '').replace(',', ' ').replace(':', ' ').replace('/', ' ').split()
66
+
67
+ if len(time_options) == 1:
68
+ start = time_options[0]
69
+ end = time_options[0]
70
+ elif len(time_options) == 2:
71
+ start, end = time_options
72
+ else:
73
+ raise Exception(f'Too many times\nOffending: "{l0}"')
74
+
75
+ xeq = timesplit[1]
76
+ blanksmpl = False
77
+
78
+ offending = [line.strip() for line in xeq.splitlines() if line.strip().startswith('<')]
79
+ if offending:
80
+ offending_text = "\n".join(offending)
81
+ raise Exception(
82
+ 'More than one time specification is not allowed\n'
83
+ 'Offending lines:\n'
84
+ f'{offending_text}'
85
+ )
86
+ else:
87
+ xeq = eq
88
+ blanksmpl = True
89
+
90
+ mmodel = model.from_eq(xeq,modelname='MFCalc', **kwargs)
91
+
92
+ res = mmodel(self._obj,**{**{'silent':True},**kwargs,**{'start':start,'end':end,}})
93
+
94
+ # print('jddd')
95
+ if blanksmpl:
96
+ if mmodel.maxlag or mmodel.maxlead:
97
+ print(f'* Take care. Lags or leads in the equations, mfcalc run for {mmodel.current_per[0]} to {mmodel.current_per[-1]}')
98
+ if showeq:
99
+ print(mmodel.equations)
100
+ return res
101
+
102
+ @pd.api.extensions.register_dataframe_accessor("mfupdate")
103
+ class mfupdate():
104
+ '''Extend a dataframe to update with values from another dataframe'''
105
+ def __init__(self, pandas_obj):
106
+ # self._validate(pandas_obj)
107
+ self._obj = pandas_obj
108
+ # print(self._obj)
109
+
110
+ def second(self,df,safe=True):
111
+ this = self._obj.copy(deep=True)
112
+ col=df.columns
113
+ index = df.index
114
+ if safe: # if dooing many experiments, we dont want this to pappen
115
+ assert 0 == len(set(index) - set(this.index)), 'Index in update not in dataframe'
116
+ assert 0 == len(set(col) - set(this.columns)),'Column in update not in dataframe'
117
+ this.loc[index,col] = df.loc[index,col]
118
+ return this
119
+
120
+
121
+ def __call__(self,df):
122
+ return self.second(df)
123
+
124
+ @pd.api.extensions.register_dataframe_accessor("ibloc")
125
+ class ibloc():
126
+ '''Extend a dataframe with a slice method which accepot wildcards in column selection.
127
+
128
+ The method just juse the method vlist from modelclass.model class '''
129
+
130
+
131
+ def __init__(self, pandas_obj):
132
+ # self._validate(pandas_obj)
133
+ self._obj = pandas_obj
134
+ # print(self._obj)
135
+
136
+ def __getitem__(self, select):
137
+ m = model()
138
+ m.lastdf = self._obj
139
+ # print(select)
140
+ vars = m.vlist(select)
141
+ return self._obj.loc[:,vars]
142
+
143
+ def __call__(self):
144
+ ''' Not in use'''
145
+ print('hello from ibloc')
146
+ return
147
+
148
+ def mfquery_0(pat, df):
149
+ '''
150
+ Returns a DataFrame with columns matching the pattern(s), case-insensitive.
151
+ The pattern can be a string or a list of patterns.
152
+ Wildcards: * and ?
153
+
154
+ Args:
155
+ pat (string or list of strings)
156
+ df (DataFrame)
157
+ Returns:
158
+ DataFrame subset
159
+ '''
160
+ if isinstance(pat, list):
161
+ patterns = pat
162
+ else:
163
+ patterns = [pat]
164
+
165
+ # Lowercase lookup for case-insensitive matching
166
+ col_map = {str(c) .lower(): c for c in df.columns}
167
+ lower_cols = list(col_map.keys())
168
+
169
+ seen = set()
170
+ names = []
171
+
172
+ for p in patterns:
173
+ for up in p.lower().split():
174
+ for v in fnmatch.filter(lower_cols, up):
175
+ orig = col_map[v]
176
+ if orig not in seen:
177
+ seen.add(orig)
178
+ names.append(orig)
179
+
180
+ return df.loc[:, names]
181
+
182
+
183
+ @pd.api.extensions.register_dataframe_accessor("mfquery")
184
+ class mfquery:
185
+ def __init__(self, pandas_obj):
186
+ self._obj = pandas_obj
187
+
188
+ def __call__(self, pat):
189
+ return mfquery_0(pat, self._obj)
190
+
191
+
192
+ def f(a):
193
+ return 42
194
+
195
+ from functools import wraps
196
+
197
+
198
+
199
+ def as_df_method_simple(func):
200
+ """Register a function as a method directly on ``pandas.DataFrame``.
201
+
202
+ The decorated function must take a DataFrame as its first argument.
203
+ It becomes callable as ``df.<func_name>(...)`` and remains callable
204
+ as a plain function.
205
+
206
+ Refuses to overwrite pandas built-ins. Methods previously registered
207
+ by this decorator are silently re-registered on module reload, which
208
+ keeps notebook workflows smooth.
209
+
210
+ Raises
211
+ ------
212
+ AttributeError
213
+ If the function's name collides with a pandas built-in attribute.
214
+
215
+ Examples
216
+ --------
217
+ >>> @as_df_method
218
+ ... def top_n(df, col, n=5):
219
+ ... return df.nlargest(n, col)
220
+ >>> df.top_n('Z', n=2)
221
+ """
222
+ # Store the registry on DataFrame itself so it survives module reloads.
223
+ # If we kept it on the decorator function, re-importing would wipe the
224
+ # set and our previously-attached methods would look like built-ins.
225
+ REGISTRY_ATTR = "_as_df_method_registered"
226
+ if not hasattr(pd.DataFrame, REGISTRY_ATTR):
227
+ setattr(pd.DataFrame, REGISTRY_ATTR, set())
228
+ registered = getattr(pd.DataFrame, REGISTRY_ATTR)
229
+
230
+ name = func.__name__
231
+
232
+ # If the name exists on DataFrame but wasn't added by us, it's a
233
+ # pandas built-in (or another extension). Refuse to shadow it.
234
+ if hasattr(pd.DataFrame, name) and name not in registered:
235
+ raise AttributeError(
236
+ f"{name!r} is already an attribute of pandas.DataFrame "
237
+ f"(likely a built-in). Refusing to overwrite it — pick a "
238
+ f"different name."
239
+ )
240
+
241
+ @wraps(func)
242
+ def method(self, *args, **kwargs):
243
+ return func(self, *args, **kwargs)
244
+
245
+ setattr(pd.DataFrame, name, method)
246
+ registered.add(name)
247
+ return func
248
+
249
+
250
+
251
+
252
+ import warnings
253
+
254
+
255
+ def as_df_method(name=None):
256
+ """Register ``func(df, *args, **kwargs)`` as a callable DataFrame method.
257
+
258
+ Usable as ``@as_df_method`` or ``@as_df_method("custom_name")``.
259
+ After registration, ``df.<name>(...)`` calls ``func(df, ...)``. The
260
+ function also remains callable directly.
261
+
262
+ Re-running the decorator (e.g. on notebook reload) updates the
263
+ registered function in place without re-registering the accessor,
264
+ avoiding pandas' "already registered" warning.
265
+
266
+ If ``name`` is already bound on ``pd.DataFrame`` by something other
267
+ than this decorator, a warning is issued and the existing attribute
268
+ is shadowed.
269
+ """
270
+ # Bare usage: @as_df_method (name is actually the function)
271
+ if callable(name):
272
+ func, name = name, None
273
+ return as_df_method(name)(func)
274
+
275
+ def decorator(func):
276
+ accessor_name = name or func.__name__
277
+
278
+ # Registry lives on DataFrame so it survives module reloads in
279
+ # notebooks. Note: this is shared across all modules using this
280
+ # decorator — last writer wins.
281
+ REGISTRY_ATTR = "_as_df_method_registry"
282
+ if not hasattr(pd.DataFrame, REGISTRY_ATTR):
283
+ setattr(pd.DataFrame, REGISTRY_ATTR, {})
284
+ registry = getattr(pd.DataFrame, REGISTRY_ATTR)
285
+
286
+ if hasattr(pd.DataFrame, accessor_name) and accessor_name not in registry:
287
+ warnings.warn(
288
+ f"{accessor_name!r} is already bound on pandas.DataFrame; "
289
+ "shadowing it. Pick another name if this is unintentional.",
290
+ stacklevel=2,
291
+ )
292
+
293
+ # Fast path: already registered. Swap the underlying function on
294
+ # the existing accessor class so reloads pick up code changes
295
+ # without pandas complaining about duplicate registration.
296
+ if accessor_name in registry:
297
+ accessor_cls = registry[accessor_name]
298
+ accessor_cls._func = staticmethod(func)
299
+ accessor_cls.__doc__ = func.__doc__
300
+ return func
301
+
302
+ class _Accessor:
303
+ __doc__ = func.__doc__
304
+ _func = staticmethod(func)
305
+
306
+ def __init__(self, pandas_obj):
307
+ self._obj = pandas_obj
308
+
309
+ def __call__(self, *args, **kwargs):
310
+ # Read _func off the class so reloads are picked up.
311
+ return type(self)._func(self._obj, *args, **kwargs)
312
+
313
+ _Accessor.__name__ = f"{func.__name__}_method"
314
+ _Accessor.__qualname__ = _Accessor.__name__
315
+
316
+ pd.api.extensions.register_dataframe_accessor(accessor_name)(_Accessor)
317
+ registry[accessor_name] = _Accessor
318
+ return func
319
+
320
+ return decorator
321
+
322
+
323
+ if __name__ == '__main__':
324
+ #this is for testing
325
+ df2 = pd.DataFrame({'Z':[1., 22., 33,43] , 'TY':[10.,20.,30.,40.] ,'YD':[10.,20.,30.,40.]},index=[2017,2018,2019,2020])
326
+ df3 = pd.DataFrame({'Z':[1., 22., 33,43] , 'TY':[10.,20.,30.,40.] ,'YD':[10.,20.,30.,40.]},index=[2017,2018,2019,2020])
327
+ df4 = pd.DataFrame({'Z':[ 223., 333] , 'TY':[203.,303.] },index=[2018,2019])
328
+ ftest = '''
329
+ ii = x+z
330
+ c=0.8*yd
331
+ i = ii+iy
332
+ x = f(2)
333
+ y = c + i + x+ i(-1)
334
+ yX = 0.02
335
+ dogplace = y *4 '''
336
+ assert 1==1
337
+ #%%
338
+ df2.mfcalc(ftest,funks=[f],showeq=1)
339
+ df4 = pd.DataFrame({'Z':[ 223., 333] , 'YD':[203.,303.] },index=[2018,2019])
340
+ x = df2.mfupdate(df4)
341
+ #%% test graph
342
+ df2
343
+
344
+
345
+ @as_df_method
346
+ def top_n(df, col, n=5):
347
+ return df.nlargest(n, col)
348
+
349
+ df2.top_n('Z',n=2)
modelnet.py ADDED
@@ -0,0 +1,114 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Wed Oct 15 14:30:44 2014
4
+
5
+ Displays an adjencacy matrix
6
+ .
7
+ @author: ibh
8
+
9
+
10
+ """
11
+ from __future__ import print_function
12
+ import networkx as nx
13
+ from itertools import groupby, chain
14
+ from collections import OrderedDict,Counter
15
+ import numpy as np
16
+ from matplotlib import pyplot, patches
17
+ import matplotlib.pyplot as plt
18
+ import matplotlib as mpl
19
+ import pandas as pd
20
+ import seaborn as sns
21
+ from matplotlib import colors
22
+
23
+
24
+ def draw_adjacency_matrix(G, node_order=None, partitions=None,type=False,title='Structure',size=(10,10)):
25
+ """
26
+ - G is a netorkx graph
27
+ - node_order (optional) is a list of nodes, where each node in G
28
+ appears exactly once
29
+ - partitions is a list of node lists, where each node in G appears
30
+ in exactly one node list
31
+
32
+ - type is a list of saying "simultaneous" or something else, has to have same length as partitions
33
+
34
+ """
35
+ df = nx.to_pandas_adjacency(G,nodelist=node_order).T
36
+
37
+ fig, ax = plt.subplots(figsize=size)
38
+ plt.title(title,fontsize=20)
39
+ cmap1 = colors.ListedColormap(['white','blue'])
40
+ norm1 = colors.BoundaryNorm([0.,0.3,30], cmap1.N)
41
+ sns.heatmap(df,ax=ax,cbar=False,square=True,cmap=cmap1,norm=norm1)
42
+ plt.xlabel('This variable is input to', fontsize=18)
43
+ plt.ylabel('The formular for this variable', fontsize=18)
44
+
45
+ color = 'blue'
46
+ current_idx = 0
47
+ if partitions:
48
+ for i,module in enumerate(partitions):
49
+ if type:
50
+ if len(type[i]) and type[i].startswith('Simultaneous'):
51
+ ax.add_patch(patches.Rectangle((current_idx, current_idx),
52
+ len(module), # Width
53
+ len(module), # Height
54
+ facecolor=[1.,0,0,0.5]))
55
+ elif len(type[i]) and type[i] == 'Recursiv':
56
+ ax.add_patch(patches.Polygon([(current_idx, current_idx),
57
+ (current_idx, current_idx+len(module)),
58
+ (current_idx+len(module), current_idx+len(module))],
59
+ facecolor=[0,1,0,0.5]))
60
+ elif len(type[i]) and type[i] == 'Endogeneous':
61
+ ax.add_patch(patches.Rectangle((current_idx, current_idx),
62
+ len(module), # Width
63
+ len(module), # Height
64
+ facecolor=[1.,0,0,0.5]))
65
+ elif len(type[i]) and type[i] == 'Exogeneous':
66
+ ax.add_patch(patches.Rectangle(( current_idx,0),
67
+ len(module), # Width
68
+ current_idx, # Height
69
+ facecolor=[1.,1,0,0.5]))
70
+ else:
71
+ ax.add_patch(patches.Rectangle((current_idx, current_idx),
72
+ len(module), # Width
73
+ len(module), # Height
74
+ facecolor=[1.,0,0,0.5]))
75
+ current_idx += len(module)
76
+ if 'Recursiv' in type or 'Simultaneous' in type :
77
+ firkant = patches.Rectangle((0,0),1,1,facecolor=[1.,0,0,1], label='Feedback block')
78
+ trekant = patches.Polygon([(0, 0),(0, 1),(1, 3)],facecolor=[0,1,0,1], label='No feedback block')
79
+ ax.legend(handles=[firkant,trekant],loc=1,fontsize=18)
80
+
81
+ if 'Endogeneous' in type or 'Exogeneous' in type :
82
+ firkant = patches.Rectangle((0,0),1,1,facecolor=[1.,0,0,1], label='Endogeneous variables')
83
+ trekant = patches.Polygon([(0, 0),(0, 1),(1, 3)],facecolor=[1,1,0,1], label='Exogeneous variables')
84
+ ax.legend(handles=[firkant,trekant],loc=3,fontsize=18)
85
+ return fig
86
+
87
+ def drawendoexo(model,size=(6.,6.)):
88
+ ''' Draw dependency including exogeneous. Used for illustrating for small models'''
89
+ fig = draw_adjacency_matrix(model.totgraph_nolag,
90
+ list(chain(model.endogene,model.exogene)),[model.endogene,model.exogene],
91
+ ['Endogeneous','Exogeneous'], title = 'Dependencies' ,size=size)
92
+ return fig
93
+
94
+ if __name__ == '__main__' :
95
+ #%%
96
+ ftest = ''' FRMl <> y = c + i + x $
97
+ FRMl <> yd = 0.6 * y + y0 $
98
+ FRMl <> c= 0.6 * yd + 0.2 *yd(-1) $
99
+ FRMl <> i = I0 $
100
+ FRMl <> ii = i *2 $
101
+ FRMl <> x = 2 $
102
+ frml <> dog = y $'''
103
+ import modelclass as mc
104
+ mtest = mc.model(ftest)
105
+ draw_adjacency_matrix(mtest.totgraph_nolag)
106
+ plt.show()
107
+ draw_adjacency_matrix(mtest.endograph,mtest.strongorder,mtest.strongblock,mtest.strongtype)
108
+ plt.show()
109
+ #%%
110
+ mtest.draw('Y')
111
+ mtest.drawmodel()
112
+ #%%
113
+
114
+ drawendoexo(mtest)