modelflowib 2.76__tar.gz → 2.78__tar.gz
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.
- {modelflowib-2.76 → modelflowib-2.78}/PKG-INFO +1 -1
- {modelflowib-2.76 → modelflowib-2.78}/modelclass.py +51 -31
- {modelflowib-2.76 → modelflowib-2.78}/modelconstruct_estimation.py +74 -7
- {modelflowib-2.76 → modelflowib-2.78}/modelestimator_new.py +133 -1
- {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/PKG-INFO +1 -1
- {modelflowib-2.76 → modelflowib-2.78}/modelhtml.py +22 -5
- {modelflowib-2.76 → modelflowib-2.78}/modeljupytermagic.py +5 -1
- {modelflowib-2.76 → modelflowib-2.78}/modelnormalize.py +31 -11
- {modelflowib-2.76 → modelflowib-2.78}/modelsolver_ng.py +3652 -3652
- {modelflowib-2.76 → modelflowib-2.78}/pyproject.toml +1 -1
- {modelflowib-2.76 → modelflowib-2.78}/README.md +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/license.md +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelBLfunk.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/model_Excel.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/model_cvx.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/model_dynare.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/model_financial_stability.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/model_latex.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/model_latex_class.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/model_parquet_mixin.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelconstruct.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modeldash.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modeldashboot.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modeldashsidebar.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modeldekom.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modeldiff.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modeldisplay.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelestimation.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/SOURCES.txt +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/dependency_links.txt +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/requires.txt +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/top_level.txt +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelgrab.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelgrabgdx.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelgrabwf2.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelhelp.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelinvert.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modeljupyter.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelmacrograb.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelmanipulation.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelmf.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelnet.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelnewton.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelpattern.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelreport.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modeluserfunk.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelvis.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelwidget.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/modelwidget_input.py +0 -0
- {modelflowib-2.76 → modelflowib-2.78}/setup.cfg +0 -0
|
@@ -3188,7 +3188,7 @@ class Modify_Mixin():
|
|
|
3188
3188
|
newnormalisation = [l.split() for l in lines]
|
|
3189
3189
|
vars_before_normalization = {beforeendo.upper() for beforeendo,afterendo in newnormalisation }
|
|
3190
3190
|
frml_normalize_strip = {(beforeendo.upper(),afterendo.upper()) : self.allvar[beforeendo]['frml'].replace('$','').split(' ',2) for beforeendo,afterendo in newnormalisation }
|
|
3191
|
-
frml_normalize_gross = {(beforeendo,afterendo) : (fname ,normal(expression,the_endo = afterendo,endo_lhs=False,
|
|
3191
|
+
frml_normalize_gross = {(beforeendo,afterendo) : (fname ,normal(expression,the_endo = afterendo,endo_lhs=False,add_add_factor=False)) for (beforeendo,afterendo),(frml,fname,expression) in frml_normalize_strip.items() }
|
|
3192
3192
|
frmldict_normalized = {afterendo : f'FRML {fname} {nexpression.normalized}$'
|
|
3193
3193
|
for (beforeendo,afterendo),(fname,nexpression) in frml_normalize_gross.items() }
|
|
3194
3194
|
# breakpoint()
|
|
@@ -3282,8 +3282,10 @@ class Modify_Mixin():
|
|
|
3282
3282
|
|
|
3283
3283
|
Parameters
|
|
3284
3284
|
----------
|
|
3285
|
-
updateeq :
|
|
3286
|
-
new equations seperated by newline
|
|
3285
|
+
updateeq : str or Makemodel
|
|
3286
|
+
new equations seperated by newline, or a Makemodel instance whose
|
|
3287
|
+
normalized equations (including per-equation add factors and
|
|
3288
|
+
fitted equations, when present) are used as the update.
|
|
3287
3289
|
newfunks : TYPE, optional
|
|
3288
3290
|
Additional userspecified functions. The default is [].
|
|
3289
3291
|
calc_add : bool, optional
|
|
@@ -3297,32 +3299,50 @@ class Modify_Mixin():
|
|
|
3297
3299
|
a dataframe with calculated add factors. Origin is the original models lastdf.
|
|
3298
3300
|
|
|
3299
3301
|
'''
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
updatefunks=list(set(self.funks+newfunks) ) #
|
|
3303
|
-
|
|
3304
|
-
newmodelname = newname if newname else self.name+' Updated'
|
|
3305
|
-
|
|
3306
|
-
dfvars = set(self.lastdf.columns)
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
frmldict_calc_add = {nexpression.endo_var: f'{frml}
|
|
3323
|
-
frml,fname,nexpression in frml2_normal
|
|
3324
|
-
|
|
3325
|
-
|
|
3302
|
+
from modelconstruct_estimation import Makemodel # local to avoid circular import
|
|
3303
|
+
|
|
3304
|
+
updatefunks=list(set(self.funks+newfunks) ) #
|
|
3305
|
+
|
|
3306
|
+
newmodelname = newname if newname else self.name+' Updated'
|
|
3307
|
+
|
|
3308
|
+
dfvars = set(self.lastdf.columns)
|
|
3309
|
+
new_var_description = self.var_description
|
|
3310
|
+
|
|
3311
|
+
if isinstance(updateeq, Makemodel):
|
|
3312
|
+
# the Makemodel has already expanded, optionally estimated and normalized the
|
|
3313
|
+
# equations, and decided add factor/fixable/fitted per equation from its flags
|
|
3314
|
+
updatefunks = list(set(updatefunks + updateeq.funks))
|
|
3315
|
+
new_var_description = {**self.var_description, **updateeq.var_description}
|
|
3316
|
+
frml2_normal = [['FRML', fname, nexpression]
|
|
3317
|
+
for fname, (parts, nexpression)
|
|
3318
|
+
in zip(updateeq.normal_output_frmlnames, updateeq.normal)]
|
|
3319
|
+
frmldict_update = {nexpression.endo_var: f'{frml} {fname} {nexpression.normalized}$' for
|
|
3320
|
+
frml,fname,nexpression in frml2_normal}
|
|
3321
|
+
frmldict_update = {**frmldict_update,
|
|
3322
|
+
**{f'{nexpression.endo_var}_FITTED': f'FRML <FIT> {nexpression.fitted}$'
|
|
3323
|
+
for frml,fname,nexpression in frml2_normal if len(nexpression.fitted)}}
|
|
3324
|
+
frmldict_calc_add = {nexpression.endo_var: f'{frml} <CALC_ADD_FACTOR> {nexpression.calc_add_factor}$' for
|
|
3325
|
+
frml,fname,nexpression in frml2_normal
|
|
3326
|
+
if len(nexpression.calc_add_factor) and nexpression.endo_var in self.endogene|self.exogene|dfvars }
|
|
3327
|
+
else:
|
|
3328
|
+
updatemodel = mp.tofrml(updateeq) # create the moedl with the usual processing of frml's
|
|
3329
|
+
while ' ' in updatemodel:
|
|
3330
|
+
updatemodel = updatemodel.replace(' ', ' ')
|
|
3331
|
+
frml2 = updatemodel.split('$')
|
|
3332
|
+
frml2_strip = [mp.split_frml(f+'$') for f in frml2 if len(f)>2]
|
|
3333
|
+
frml2_normal = [[frml,fname,
|
|
3334
|
+
normal(expression[:-1],do_preprocess=do_preprocess,add_add_factor=add_add_factor,
|
|
3335
|
+
make_fixable = pt.kw_frml_name(fname.upper(),'FIXABLE'))]
|
|
3336
|
+
for allfrml,frml,fname,expression in frml2_strip]
|
|
3337
|
+
frmldict_update = {nexpression.endo_var: f'{frml} {fname} {nexpression.normalized}$' for
|
|
3338
|
+
frml,fname,nexpression in frml2_normal}
|
|
3339
|
+
|
|
3340
|
+
|
|
3341
|
+
if add_add_factor:
|
|
3342
|
+
frmldict_calc_add = {nexpression.endo_var: f'{frml} {fname} {nexpression.calc_add_factor}$' for
|
|
3343
|
+
frml,fname,nexpression in frml2_normal if nexpression.endo_var in self.endogene|self.exogene|dfvars }
|
|
3344
|
+
else:
|
|
3345
|
+
frmldict_calc_add={}
|
|
3326
3346
|
# breakpoint()
|
|
3327
3347
|
|
|
3328
3348
|
frmldict = {k: v['frml'] for (k,v) in self.allvar.items() if k in self.endogene } # frml's in the existing model
|
|
@@ -3331,8 +3351,8 @@ class Modify_Mixin():
|
|
|
3331
3351
|
newfrml = '\n'.join([f for f in newfrmldict.values()])
|
|
3332
3352
|
newmodel = self.__class__(newfrml,modelname = f'updated {self.name}',
|
|
3333
3353
|
funks=updatefunks,
|
|
3334
|
-
var_description=
|
|
3335
|
-
model_description = self.model_description,
|
|
3354
|
+
var_description=new_var_description,
|
|
3355
|
+
model_description = self.model_description,
|
|
3336
3356
|
var_groups = self.var_groups ,
|
|
3337
3357
|
reports = self.reports ,
|
|
3338
3358
|
substitution = self.substitution,
|
|
@@ -982,6 +982,16 @@ from IPython.display import display, Markdown, Math
|
|
|
982
982
|
import re
|
|
983
983
|
from IPython.display import display, Markdown, Math
|
|
984
984
|
|
|
985
|
+
class Markdown(Markdown):
|
|
986
|
+
"""IPython Markdown, but with the markdown source as the text/plain fallback.
|
|
987
|
+
|
|
988
|
+
The stock class reprs as '<IPython.core.display.Markdown object>', and that
|
|
989
|
+
string is what latex/pdf exporters show, as they can not render the
|
|
990
|
+
text/markdown mime type and fall back to text/plain. With the source as
|
|
991
|
+
repr, exported documents show the model text instead."""
|
|
992
|
+
def __repr__(self):
|
|
993
|
+
return str(self.data)
|
|
994
|
+
|
|
985
995
|
def display_model(cell: str, spec: str = "markdown"):
|
|
986
996
|
"""
|
|
987
997
|
Display a model specification.
|
|
@@ -1771,15 +1781,34 @@ def _estimation_record_to_markdown(record: dict) -> str:
|
|
|
1771
1781
|
|
|
1772
1782
|
coefs = _estimator_coefficients_for_markdown(est)
|
|
1773
1783
|
if coefs:
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1784
|
+
try:
|
|
1785
|
+
tvals = est.tvalues.to_dict()
|
|
1786
|
+
except Exception:
|
|
1787
|
+
tvals = {}
|
|
1788
|
+
# NaN != NaN — only add the column when at least one t-stat exists.
|
|
1789
|
+
has_t = any(v == v for v in tvals.values())
|
|
1790
|
+
if has_t:
|
|
1791
|
+
lines.extend([
|
|
1792
|
+
"",
|
|
1793
|
+
"| Parameter | Estimate | t-stat |",
|
|
1794
|
+
"|:--|--:|--:|",
|
|
1795
|
+
])
|
|
1796
|
+
else:
|
|
1797
|
+
lines.extend([
|
|
1798
|
+
"",
|
|
1799
|
+
"| Parameter | Estimate |",
|
|
1800
|
+
"|:--|--:|",
|
|
1801
|
+
])
|
|
1779
1802
|
for key in sorted(coefs, key=lambda x: (str(x).split("__")[0], int(str(x).split("__")[-1]) if str(x).split("__")[-1].lstrip("-").isdigit() else str(x))):
|
|
1780
|
-
|
|
1781
|
-
f"| {_markdown_escape_cell(key)}
|
|
1803
|
+
row = (
|
|
1804
|
+
f"| {_markdown_escape_cell(key)} "
|
|
1805
|
+
f"| {_markdown_escape_cell(_markdown_format_number(coefs[key]))} |"
|
|
1782
1806
|
)
|
|
1807
|
+
if has_t:
|
|
1808
|
+
t = tvals.get(key)
|
|
1809
|
+
t_cell = f"{t:.3f}" if t is not None and t == t else ""
|
|
1810
|
+
row += f" {_markdown_escape_cell(t_cell)} |"
|
|
1811
|
+
lines.append(row)
|
|
1783
1812
|
|
|
1784
1813
|
return "\n".join(lines) + "\n"
|
|
1785
1814
|
|
|
@@ -2419,6 +2448,31 @@ class Makemodel(BaseExplode):
|
|
|
2419
2448
|
"""
|
|
2420
2449
|
return MakeModelReport(self, title=title, plot_format=plot_format, report_all=report_all)
|
|
2421
2450
|
|
|
2451
|
+
@property
|
|
2452
|
+
def tvalues(self):
|
|
2453
|
+
"""t-statistics for all estimated equations as a DataFrame.
|
|
2454
|
+
|
|
2455
|
+
Rows are parameter tokens (``C__1``, ``C__2``, ...), one column per
|
|
2456
|
+
estimated equation named by its endogenous variable. NaN where a
|
|
2457
|
+
parameter is not in that equation or the backend provides no
|
|
2458
|
+
t-statistic (fixed/derived parameters, failed covariance, EViews
|
|
2459
|
+
``NA`` rows).
|
|
2460
|
+
|
|
2461
|
+
Per-equation access: ``rec['estimator_object'].tvalues`` on the
|
|
2462
|
+
entries of :attr:`estimation_records`.
|
|
2463
|
+
"""
|
|
2464
|
+
import pandas as pd
|
|
2465
|
+
cols = {}
|
|
2466
|
+
for rec in self.estimation_records:
|
|
2467
|
+
est = rec.get('estimator_object')
|
|
2468
|
+
try:
|
|
2469
|
+
ser = est.tvalues
|
|
2470
|
+
except Exception:
|
|
2471
|
+
continue
|
|
2472
|
+
name = getattr(est, 'endo_var', '') or rec.get('frmlname', '')
|
|
2473
|
+
cols[name] = ser
|
|
2474
|
+
return pd.DataFrame(cols)
|
|
2475
|
+
|
|
2422
2476
|
@property
|
|
2423
2477
|
def markdown_with_estimation(self) -> str:
|
|
2424
2478
|
"""Original Markdown input with compact estimation tables inserted.
|
|
@@ -2806,6 +2860,19 @@ class Listmodels(BaseExplode):
|
|
|
2806
2860
|
"""
|
|
2807
2861
|
return MakeModelReport(self, title=title, plot_format=plot_format, report_all=report_all)
|
|
2808
2862
|
|
|
2863
|
+
@property
|
|
2864
|
+
def tvalues(self):
|
|
2865
|
+
"""t-statistics of all member Makemodels, combined column-wise.
|
|
2866
|
+
|
|
2867
|
+
See :attr:`Makemodel.tvalues` for the layout.
|
|
2868
|
+
"""
|
|
2869
|
+
import pandas as pd
|
|
2870
|
+
frames = [mex.tvalues for mex in self.makemodels]
|
|
2871
|
+
frames = [f for f in frames if not f.empty]
|
|
2872
|
+
if not frames:
|
|
2873
|
+
return pd.DataFrame()
|
|
2874
|
+
return pd.concat(frames, axis=1)
|
|
2875
|
+
|
|
2809
2876
|
@property
|
|
2810
2877
|
def markdown_with_estimation(self) -> str:
|
|
2811
2878
|
"""Concatenate member Makemodel markdown-with-estimation strings."""
|
|
@@ -99,6 +99,7 @@ from lmfit import Parameters, minimize
|
|
|
99
99
|
from matplotlib.gridspec import GridSpec
|
|
100
100
|
|
|
101
101
|
from modelclass import model
|
|
102
|
+
from modelmanipulation import check_syntax
|
|
102
103
|
import modelnormalize as nz
|
|
103
104
|
|
|
104
105
|
|
|
@@ -904,6 +905,13 @@ class Eq_parent:
|
|
|
904
905
|
raw = self.org_eq.upper()
|
|
905
906
|
eq_body, constraint_text = _split_st_clause(raw)
|
|
906
907
|
|
|
908
|
+
# 2a) Syntax-check the equation text BEFORE parameter-token
|
|
909
|
+
# normalization. Typos like ``C(1)2020`` (missing ``*D``) or a
|
|
910
|
+
# missing ``+`` between terms are clean SyntaxErrors here, but
|
|
911
|
+
# after normalization they can merge into single valid
|
|
912
|
+
# identifiers and slip through to give degenerate fits.
|
|
913
|
+
self._check_eq_syntax(eq_body)
|
|
914
|
+
|
|
907
915
|
# 2) Normalize parameter tokens in both equation body AND constraints
|
|
908
916
|
# so names align downstream (e.g. ``C(1)`` -> ``C__1``).
|
|
909
917
|
eq = _normalize_param_tokens(eq_body, self.est_param, self.param_names)
|
|
@@ -946,6 +954,7 @@ class Eq_parent:
|
|
|
946
954
|
# Subclasses that need a custom dataframe layout (e.g. mfcalc-built
|
|
947
955
|
# regressors for OLS) handle that themselves.
|
|
948
956
|
if self.input_df is not None:
|
|
957
|
+
self._check_unknown_variables()
|
|
949
958
|
self.eq_var_df = (
|
|
950
959
|
self.mdummy.insertModelVar(self.input_df)
|
|
951
960
|
.loc[:, self.varname_all]
|
|
@@ -953,7 +962,49 @@ class Eq_parent:
|
|
|
953
962
|
)
|
|
954
963
|
self.estimation_df = self.eq_var_df
|
|
955
964
|
|
|
956
|
-
|
|
965
|
+
@staticmethod
|
|
966
|
+
def _check_eq_syntax(eq_body: str) -> None:
|
|
967
|
+
"""Raise ``SyntaxError`` (with a pinpointed location) if the equation
|
|
968
|
+
text does not parse as Python expressions.
|
|
969
|
+
|
|
970
|
+
Each side of the first ``=`` is checked separately — the full
|
|
971
|
+
statement would be an invalid Python assignment whenever the LHS is
|
|
972
|
+
a transformation like ``DLOG(...)``. Whitespace is preserved, which
|
|
973
|
+
is what makes missing-operator typos detectable.
|
|
974
|
+
"""
|
|
975
|
+
checkable = eq_body.replace("@ABS(", "ABS(")
|
|
976
|
+
sides = [side.strip() for side in checkable.split("=", 1)]
|
|
977
|
+
check_syntax([side for side in sides if side])
|
|
978
|
+
|
|
979
|
+
def _check_unknown_variables(self) -> None:
|
|
980
|
+
"""Print a diagnosis and raise for variables with no data column.
|
|
981
|
+
|
|
982
|
+
Every variable in the equation that is not a parameter placeholder
|
|
983
|
+
(active prefix or ``param_names``) or a helper slot must exist in
|
|
984
|
+
``input_df``. Anything else would be inserted as an all-NaN column
|
|
985
|
+
by ``insertModelVar`` and silently degrade or break the fit —
|
|
986
|
+
typically a misspelled variable name or a forgotten ``param_names``
|
|
987
|
+
entry (``ALFA(1)`` with ``'alfa'`` not declared reads as a lead of
|
|
988
|
+
an unknown variable ALFA).
|
|
989
|
+
"""
|
|
990
|
+
helpers = {"ACTUAL", "FITTED", "RESIDUALS"}
|
|
991
|
+
known = set(self.input_df.columns) | set(self.c_params) | helpers
|
|
992
|
+
unknown = sorted(set(self.varname_all) - known)
|
|
993
|
+
if unknown:
|
|
994
|
+
# Print the diagnosis, then raise a bare SyntaxError — same
|
|
995
|
+
# style as check_syntax. IPython renders SyntaxError without
|
|
996
|
+
# the frame stack, so the notebook shows the message above
|
|
997
|
+
# instead of a long construction call stack.
|
|
998
|
+
print(
|
|
999
|
+
f"{type(self).__name__}({self.endo_var}): the equation "
|
|
1000
|
+
f"references variables with no column in input_df:\n"
|
|
1001
|
+
f" {unknown}\n"
|
|
1002
|
+
f"Equation: {self.org_eq_clean}\n"
|
|
1003
|
+
"Check for misspelled variable names or missing param_names "
|
|
1004
|
+
"entries — such variables would otherwise enter the "
|
|
1005
|
+
"estimation as all-NaN columns."
|
|
1006
|
+
)
|
|
1007
|
+
raise SyntaxError("Unknown variables in equation - see above") from None
|
|
957
1008
|
|
|
958
1009
|
@property
|
|
959
1010
|
def mdummy(self):
|
|
@@ -1079,6 +1130,10 @@ class EstimatorBackend(Eq_parent, ABC):
|
|
|
1079
1130
|
Default is a no-op. Override to reject equation forms the backend
|
|
1080
1131
|
cannot handle.
|
|
1081
1132
|
|
|
1133
|
+
:meth:`_tvalues_dict`
|
|
1134
|
+
Default returns ``{}`` (no t-statistics). Override to return
|
|
1135
|
+
``{<prefix>__n: t}`` so the :attr:`tvalues` Series is populated.
|
|
1136
|
+
|
|
1082
1137
|
Standard attributes set during ``__post_init__``
|
|
1083
1138
|
------------------------------------------------
|
|
1084
1139
|
regression_model : Any
|
|
@@ -1230,6 +1285,29 @@ class EstimatorBackend(Eq_parent, ABC):
|
|
|
1230
1285
|
break
|
|
1231
1286
|
return mapped
|
|
1232
1287
|
|
|
1288
|
+
# ---- t-statistics ------------------------------------------------------
|
|
1289
|
+
|
|
1290
|
+
def _tvalues_dict(self) -> Dict[str, float]:
|
|
1291
|
+
"""Backend-specific t-statistics keyed by ``{est_param}__n`` token.
|
|
1292
|
+
|
|
1293
|
+
Default: no t-statistics available. Backends override where the
|
|
1294
|
+
fitted result exposes (or allows computing) them.
|
|
1295
|
+
"""
|
|
1296
|
+
return {}
|
|
1297
|
+
|
|
1298
|
+
@cached_property
|
|
1299
|
+
def tvalues(self) -> pd.Series:
|
|
1300
|
+
"""t-statistics as a Series keyed like :attr:`coef_ser`.
|
|
1301
|
+
|
|
1302
|
+
Parameters without an available t-statistic (fixed, derived, or the
|
|
1303
|
+
backend could not provide one) are NaN.
|
|
1304
|
+
"""
|
|
1305
|
+
tvals = self._tvalues_dict()
|
|
1306
|
+
return pd.Series(
|
|
1307
|
+
{p: tvals.get(p, float("nan")) for p in self.c_params},
|
|
1308
|
+
name=self.caption, dtype=float,
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1233
1311
|
# ---- A/F and residuals (computed once, cached) -------------------------
|
|
1234
1312
|
|
|
1235
1313
|
@cached_property
|
|
@@ -1486,6 +1564,13 @@ class Estimate_ols(EstimatorBackend):
|
|
|
1486
1564
|
"<caption><h3>OLS Regression Results</h3></caption>",
|
|
1487
1565
|
)
|
|
1488
1566
|
|
|
1567
|
+
def _tvalues_dict(self) -> Dict[str, float]:
|
|
1568
|
+
"""statsmodels exposes t-statistics directly, keyed by regressor
|
|
1569
|
+
column name — map those back to the canonical parameter tokens."""
|
|
1570
|
+
return self._map_statsmodels_params(
|
|
1571
|
+
self.regression_model.tvalues.to_dict()
|
|
1572
|
+
)
|
|
1573
|
+
|
|
1489
1574
|
# ---- override estimation_smpl: OLS reports its EFFECTIVE sample --------
|
|
1490
1575
|
|
|
1491
1576
|
@cached_property
|
|
@@ -1610,6 +1695,33 @@ class Estimate_nls_lmfit(EstimatorBackend):
|
|
|
1610
1695
|
"<h2>Fit Result</h2>", "<h3>NLS Regression Results</h3>"
|
|
1611
1696
|
)
|
|
1612
1697
|
|
|
1698
|
+
def _tvalues_dict(self) -> Dict[str, float]:
|
|
1699
|
+
"""lmfit provides standard errors (from the covariance matrix when
|
|
1700
|
+
``calc_covar=True``); t = value / stderr. Fixed parameters, derived
|
|
1701
|
+
(``expr``) parameters without propagated errors, and fits where the
|
|
1702
|
+
covariance could not be estimated have ``stderr`` None (or NaN) and
|
|
1703
|
+
are omitted (→ NaN in :attr:`tvalues`)."""
|
|
1704
|
+
res = self.regression_model
|
|
1705
|
+
out: Dict[str, float] = {}
|
|
1706
|
+
for name, p in res.params.items():
|
|
1707
|
+
# stderr is None when lmfit produced no uncertainties, and can
|
|
1708
|
+
# be NaN when the covariance matrix is ill-conditioned.
|
|
1709
|
+
if p.stderr and p.stderr == p.stderr:
|
|
1710
|
+
out[name] = p.value / p.stderr
|
|
1711
|
+
if not out and any(p.vary for p in res.params.values()):
|
|
1712
|
+
print(
|
|
1713
|
+
f"[{type(self).__name__}({self.endo_var})] WARNING: lmfit "
|
|
1714
|
+
f"produced no parameter uncertainties "
|
|
1715
|
+
f"(errorbars={getattr(res, 'errorbars', None)}), so no "
|
|
1716
|
+
"t-values are available. Common causes: singular or "
|
|
1717
|
+
"ill-conditioned Jacobian (collinear or huge-scale "
|
|
1718
|
+
"regressors), parameters stuck at a bound, or a fit that "
|
|
1719
|
+
"did not converge. Inspect "
|
|
1720
|
+
"est.regression_model.params.pretty_print() and "
|
|
1721
|
+
"est.regression_model.message."
|
|
1722
|
+
)
|
|
1723
|
+
return out
|
|
1724
|
+
|
|
1613
1725
|
|
|
1614
1726
|
# =============================================================================
|
|
1615
1727
|
# Estimate_nls_eviews — nonlinear least squares via EViews (py2eviews)
|
|
@@ -1702,6 +1814,26 @@ class Estimate_nls_eviews(EstimatorBackend):
|
|
|
1702
1814
|
def _render_summary_html(self) -> str:
|
|
1703
1815
|
return eviews_output_to_html(self.regression_model)
|
|
1704
1816
|
|
|
1817
|
+
def _tvalues_dict(self) -> Dict[str, float]:
|
|
1818
|
+
"""Parse the t-Statistic column from the EViews spool text.
|
|
1819
|
+
|
|
1820
|
+
Only the coefficient vector is read back from EViews numerically;
|
|
1821
|
+
the coefficient table (Coefficient, Std. Error, t-Statistic, Prob.)
|
|
1822
|
+
exists only as text in the spool. Rows where EViews prints ``NA``
|
|
1823
|
+
simply do not match and stay NaN in :attr:`tvalues`.
|
|
1824
|
+
"""
|
|
1825
|
+
if not isinstance(self.regression_model, str):
|
|
1826
|
+
return {}
|
|
1827
|
+
num = r"(-?[\d.]+(?:[eE][+-]?\d+)?)"
|
|
1828
|
+
out: Dict[str, float] = {}
|
|
1829
|
+
for m in re.finditer(
|
|
1830
|
+
rf"^\s*C\((\d+)\)\s+{num}\s+{num}\s+{num}",
|
|
1831
|
+
self.regression_model,
|
|
1832
|
+
flags=re.MULTILINE,
|
|
1833
|
+
):
|
|
1834
|
+
out[f"{self.est_param}__{m.group(1)}"] = float(m.group(4))
|
|
1835
|
+
return out
|
|
1836
|
+
|
|
1705
1837
|
|
|
1706
1838
|
# =============================================================================
|
|
1707
1839
|
# Estimate_nls — factory class dispatching on solver
|
|
@@ -118,6 +118,7 @@ def _mmr_md_to_html(
|
|
|
118
118
|
|
|
119
119
|
in_p = in_ul = in_ol = in_bll = in_fence = in_table = False
|
|
120
120
|
tbl_hdr_done = False
|
|
121
|
+
pending_est = False
|
|
121
122
|
|
|
122
123
|
def ep():
|
|
123
124
|
nonlocal in_p
|
|
@@ -148,9 +149,24 @@ def _mmr_md_to_html(
|
|
|
148
149
|
def end_all():
|
|
149
150
|
ep(); eul(); eol(); ebll(); etbl()
|
|
150
151
|
|
|
152
|
+
def flush_est():
|
|
153
|
+
# Emit the panel for the last estimated equation once all of its
|
|
154
|
+
# '>>' continuation lines have been rendered, so the equation is not
|
|
155
|
+
# split around its estimation output.
|
|
156
|
+
nonlocal pending_est, rec_i, panel_i
|
|
157
|
+
if pending_est:
|
|
158
|
+
pending_est = False
|
|
159
|
+
if rec_i < len(est_records):
|
|
160
|
+
ebll()
|
|
161
|
+
out.append(_mmr_est_panel(est_records[rec_i], panel_i, plot_format))
|
|
162
|
+
panel_i += 1; rec_i += 1
|
|
163
|
+
|
|
151
164
|
for raw in lines:
|
|
152
165
|
s = raw.strip()
|
|
153
166
|
|
|
167
|
+
if pending_est and not s.startswith('>>'):
|
|
168
|
+
flush_est()
|
|
169
|
+
|
|
154
170
|
# code fence (```)
|
|
155
171
|
if s.startswith('```'):
|
|
156
172
|
if in_fence:
|
|
@@ -192,20 +208,20 @@ def _mmr_md_to_html(
|
|
|
192
208
|
end_all()
|
|
193
209
|
out.append('<pre class="mmr-bll"><code>'); in_bll = True
|
|
194
210
|
out.append(_mmr_annotate_bll(content, var_desc))
|
|
211
|
+
# The estimator tag may sit on a continuation line.
|
|
212
|
+
if _mmr_has_estimator_tag(raw):
|
|
213
|
+
pending_est = True
|
|
195
214
|
continue
|
|
196
215
|
|
|
197
216
|
# BLL equation >
|
|
198
217
|
if s.startswith('>'):
|
|
199
218
|
content = s[1:].strip()
|
|
200
|
-
has_est = _mmr_has_estimator_tag(raw)
|
|
201
219
|
if not in_bll:
|
|
202
220
|
end_all()
|
|
203
221
|
out.append('<pre class="mmr-bll"><code>'); in_bll = True
|
|
204
222
|
out.append(_mmr_annotate_bll(content, var_desc))
|
|
205
|
-
if
|
|
206
|
-
|
|
207
|
-
out.append(_mmr_est_panel(est_records[rec_i], panel_i, plot_format))
|
|
208
|
-
panel_i += 1; rec_i += 1
|
|
223
|
+
if _mmr_has_estimator_tag(raw):
|
|
224
|
+
pending_est = True
|
|
209
225
|
continue
|
|
210
226
|
|
|
211
227
|
# markdown table row
|
|
@@ -262,6 +278,7 @@ def _mmr_md_to_html(
|
|
|
262
278
|
|
|
263
279
|
if in_fence:
|
|
264
280
|
out.append('</code></pre>')
|
|
281
|
+
flush_est()
|
|
265
282
|
end_all()
|
|
266
283
|
|
|
267
284
|
# leftover estimation records not matched to any source line
|
|
@@ -27,6 +27,10 @@ from modelclass import model
|
|
|
27
27
|
from modelmanipulation import explode
|
|
28
28
|
from model_latex_class import a_latex_model
|
|
29
29
|
from modelconstruct_estimation import Makemodel, display_model
|
|
30
|
+
# Markdown subclass whose text/plain fallback is the markdown source itself,
|
|
31
|
+
# so latex/pdf exports show the model text instead of
|
|
32
|
+
# '<IPython.core.display.Markdown object>'. Shadows the IPython import above.
|
|
33
|
+
from modelconstruct_estimation import Markdown
|
|
30
34
|
from modelhelp import debug_var
|
|
31
35
|
from modelreport import LatexRepo
|
|
32
36
|
|
|
@@ -589,7 +593,7 @@ try:
|
|
|
589
593
|
if dict_name in user_ns:
|
|
590
594
|
segs = list(user_ns[dict_name].keys())
|
|
591
595
|
print(f"Model `{name}` built from segments: {', '.join(segs)}")
|
|
592
|
-
print(f"✅ Created
|
|
596
|
+
print(f"✅ Created Makemodel: {name}")
|
|
593
597
|
print(emodel)
|
|
594
598
|
|
|
595
599
|
return emodel
|
|
@@ -129,8 +129,19 @@ def funk_find_arg(funk_match, streng):
|
|
|
129
129
|
return streng[:start], match[1:index], match[index + 1:]
|
|
130
130
|
assert 1==2,f'Parantese mismatch in {streng}'
|
|
131
131
|
|
|
132
|
+
def funk_check_arg(funk,arg,udtryk,udtryk_up):
|
|
133
|
+
'''Guards against variables named like a function.
|
|
134
|
+
|
|
135
|
+
A lagged variable with a function name, fx DIFF(-1), matches DIFF( and would
|
|
136
|
+
silently be swallowed as a function call. If the argument is a pure signed
|
|
137
|
+
integer it must be such a lag, so raise an informative error instead.'''
|
|
138
|
+
if re.fullmatch(r'[+-]?\d+',arg.replace(' ','')):
|
|
139
|
+
raise Exception(f"A variable can not be named {funk}, as {funk}( is a function in the business language.\n"
|
|
140
|
+
f"{funk}({arg}) looks like a lagged variable {funk}. Rename the variable.\n"
|
|
141
|
+
f"Original expression: {udtryk}\n"
|
|
142
|
+
f"Processed so far : {udtryk_up}")
|
|
143
|
+
|
|
132
144
|
|
|
133
|
-
|
|
134
145
|
def preprocess(udtryk,funks=[]):
|
|
135
146
|
'''
|
|
136
147
|
test processing expanding dlog,diff,movavg,pct,logit functions
|
|
@@ -146,16 +157,21 @@ def preprocess(udtryk,funks=[]):
|
|
|
146
157
|
|
|
147
158
|
'''
|
|
148
159
|
udtryk_up = udtryk.upper().replace(' ','')
|
|
160
|
+
udtryk_up = funk_replace('PCT','PCT_GROWTH',udtryk_up)
|
|
161
|
+
|
|
149
162
|
while dlog_match := funk_in('DLOG',udtryk_up):
|
|
150
163
|
fordlog,dlogudtryk_up,efterdlog=funk_find_arg(dlog_match,udtryk_up)
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
funk_check_arg('DLOG',dlogudtryk_up,udtryk,udtryk_up)
|
|
165
|
+
udtryk_up=fordlog+'DIFF(LOG('+dlogudtryk_up+'))'+efterdlog
|
|
166
|
+
|
|
153
167
|
while logit_match := funk_in('LOGIT',udtryk_up):
|
|
154
168
|
forlogit,logitudtryk_up,efterlogit=funk_find_arg(logit_match,udtryk_up)
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
funk_check_arg('LOGIT',logitudtryk_up,udtryk,udtryk_up)
|
|
170
|
+
udtryk_up=forlogit+'(LOG('+logitudtryk_up+'/(1.0 -'+logitudtryk_up+')))'+efterlogit
|
|
171
|
+
|
|
157
172
|
while movavg_match := funk_in('MOVAVG', udtryk_up):
|
|
158
173
|
forkaede,kaedeudtryk,efterkaede=funk_find_arg(movavg_match,udtryk_up)
|
|
174
|
+
funk_check_arg('MOVAVG',kaedeudtryk,udtryk,udtryk_up)
|
|
159
175
|
arg=kaedeudtryk.split(',',1)
|
|
160
176
|
avg='(('
|
|
161
177
|
term=arg[0]
|
|
@@ -168,19 +184,23 @@ def preprocess(udtryk,funks=[]):
|
|
|
168
184
|
|
|
169
185
|
while pct_match := funk_in('PCT_GROWTH',udtryk_up):
|
|
170
186
|
forpc,pcudtryk,efterpc=funk_find_arg(pct_match,udtryk_up)
|
|
171
|
-
|
|
187
|
+
funk_check_arg('PCT_GROWTH',pcudtryk,udtryk,udtryk_up)
|
|
188
|
+
udtryk_up=f'{forpc} (100 * ( ({pcudtryk}) / ({lagone(pcudtryk,funks=funks)}) -1)) {efterpc}'
|
|
172
189
|
|
|
173
190
|
while pct_match := funk_in('PCY',udtryk_up):
|
|
174
191
|
forpc,pcudtryk,efterpc=funk_find_arg(pct_match,udtryk_up)
|
|
175
|
-
|
|
176
|
-
|
|
192
|
+
funk_check_arg('PCY',pcudtryk,udtryk,udtryk_up)
|
|
193
|
+
udtryk_up=f'{forpc} (100 * ( ({pcudtryk}) / ({lagone(pcudtryk,funks=funks,laglead=-4)}) -1)) {efterpc}'
|
|
194
|
+
|
|
177
195
|
while diff_match := funk_in('DIFF' , udtryk_up):
|
|
178
196
|
fordif,difudtryk_up,efterdif=funk_find_arg(diff_match,udtryk_up)
|
|
179
|
-
|
|
180
|
-
|
|
197
|
+
funk_check_arg('DIFF',difudtryk_up,udtryk,udtryk_up)
|
|
198
|
+
udtryk_up=fordif+'(('+difudtryk_up+')-('+lagone(difudtryk_up+'',funks=funks)+'))'+efterdif
|
|
199
|
+
|
|
181
200
|
while diff_match := funk_in('D' , udtryk_up):
|
|
182
201
|
# print(f'{udtryk_up} on D')
|
|
183
202
|
fordif,difudtryk_up,efterdif=funk_find_arg(diff_match,udtryk_up)
|
|
203
|
+
funk_check_arg('D',difudtryk_up,udtryk,udtryk_up)
|
|
184
204
|
difudtryk_up = difudtryk_up.replace(' ','').replace(',0,1','') if difudtryk_up.endswith(',0,1') else difudtryk_up
|
|
185
205
|
udtryk_up=fordif+'(('+difudtryk_up+')-('+lagone(difudtryk_up+'',funks=funks)+'))'+efterdif
|
|
186
206
|
|
|
@@ -407,7 +427,7 @@ if __name__ == '__main__':
|
|
|
407
427
|
normal('a+b = c',add_add_factor=1,make_fitted=1).fprint
|
|
408
428
|
normal('PCT_growth(a) = n(-1)',add_add_factor=0).fprint
|
|
409
429
|
normal('a = movavg(pct(b),2)',add_add_factor=0).fprint
|
|
410
|
-
normal('pct_growth(c) = pct_growth(
|
|
430
|
+
normal('pct_growth(c) = pct_growth(dd)',add_add_factor=0).fprint
|
|
411
431
|
normal('pct_growth(c) = z+pct(b) + pct(e)').fprint
|
|
412
432
|
normal('pct_growth(c) = z+pct(b) + pct(e)').fprint
|
|
413
433
|
normal('a = pct_growth(b)',add_add_factor=0).fprint
|