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.
Files changed (50) hide show
  1. {modelflowib-2.76 → modelflowib-2.78}/PKG-INFO +1 -1
  2. {modelflowib-2.76 → modelflowib-2.78}/modelclass.py +51 -31
  3. {modelflowib-2.76 → modelflowib-2.78}/modelconstruct_estimation.py +74 -7
  4. {modelflowib-2.76 → modelflowib-2.78}/modelestimator_new.py +133 -1
  5. {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/PKG-INFO +1 -1
  6. {modelflowib-2.76 → modelflowib-2.78}/modelhtml.py +22 -5
  7. {modelflowib-2.76 → modelflowib-2.78}/modeljupytermagic.py +5 -1
  8. {modelflowib-2.76 → modelflowib-2.78}/modelnormalize.py +31 -11
  9. {modelflowib-2.76 → modelflowib-2.78}/modelsolver_ng.py +3652 -3652
  10. {modelflowib-2.76 → modelflowib-2.78}/pyproject.toml +1 -1
  11. {modelflowib-2.76 → modelflowib-2.78}/README.md +0 -0
  12. {modelflowib-2.76 → modelflowib-2.78}/license.md +0 -0
  13. {modelflowib-2.76 → modelflowib-2.78}/modelBLfunk.py +0 -0
  14. {modelflowib-2.76 → modelflowib-2.78}/model_Excel.py +0 -0
  15. {modelflowib-2.76 → modelflowib-2.78}/model_cvx.py +0 -0
  16. {modelflowib-2.76 → modelflowib-2.78}/model_dynare.py +0 -0
  17. {modelflowib-2.76 → modelflowib-2.78}/model_financial_stability.py +0 -0
  18. {modelflowib-2.76 → modelflowib-2.78}/model_latex.py +0 -0
  19. {modelflowib-2.76 → modelflowib-2.78}/model_latex_class.py +0 -0
  20. {modelflowib-2.76 → modelflowib-2.78}/model_parquet_mixin.py +0 -0
  21. {modelflowib-2.76 → modelflowib-2.78}/modelconstruct.py +0 -0
  22. {modelflowib-2.76 → modelflowib-2.78}/modeldash.py +0 -0
  23. {modelflowib-2.76 → modelflowib-2.78}/modeldashboot.py +0 -0
  24. {modelflowib-2.76 → modelflowib-2.78}/modeldashsidebar.py +0 -0
  25. {modelflowib-2.76 → modelflowib-2.78}/modeldekom.py +0 -0
  26. {modelflowib-2.76 → modelflowib-2.78}/modeldiff.py +0 -0
  27. {modelflowib-2.76 → modelflowib-2.78}/modeldisplay.py +0 -0
  28. {modelflowib-2.76 → modelflowib-2.78}/modelestimation.py +0 -0
  29. {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/SOURCES.txt +0 -0
  30. {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/dependency_links.txt +0 -0
  31. {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/requires.txt +0 -0
  32. {modelflowib-2.76 → modelflowib-2.78}/modelflowib.egg-info/top_level.txt +0 -0
  33. {modelflowib-2.76 → modelflowib-2.78}/modelgrab.py +0 -0
  34. {modelflowib-2.76 → modelflowib-2.78}/modelgrabgdx.py +0 -0
  35. {modelflowib-2.76 → modelflowib-2.78}/modelgrabwf2.py +0 -0
  36. {modelflowib-2.76 → modelflowib-2.78}/modelhelp.py +0 -0
  37. {modelflowib-2.76 → modelflowib-2.78}/modelinvert.py +0 -0
  38. {modelflowib-2.76 → modelflowib-2.78}/modeljupyter.py +0 -0
  39. {modelflowib-2.76 → modelflowib-2.78}/modelmacrograb.py +0 -0
  40. {modelflowib-2.76 → modelflowib-2.78}/modelmanipulation.py +0 -0
  41. {modelflowib-2.76 → modelflowib-2.78}/modelmf.py +0 -0
  42. {modelflowib-2.76 → modelflowib-2.78}/modelnet.py +0 -0
  43. {modelflowib-2.76 → modelflowib-2.78}/modelnewton.py +0 -0
  44. {modelflowib-2.76 → modelflowib-2.78}/modelpattern.py +0 -0
  45. {modelflowib-2.76 → modelflowib-2.78}/modelreport.py +0 -0
  46. {modelflowib-2.76 → modelflowib-2.78}/modeluserfunk.py +0 -0
  47. {modelflowib-2.76 → modelflowib-2.78}/modelvis.py +0 -0
  48. {modelflowib-2.76 → modelflowib-2.78}/modelwidget.py +0 -0
  49. {modelflowib-2.76 → modelflowib-2.78}/modelwidget_input.py +0 -0
  50. {modelflowib-2.76 → modelflowib-2.78}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelflowib
3
- Version: 2.76
3
+ Version: 2.78
4
4
  Summary: A tool to solve and manage dynamic economic and financial models
5
5
  Author-email: Ib Hansen <Ib.Hansen.Iv@gmail.com>
6
6
  License-Expression: MIT
@@ -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,add_factor=False)) for (beforeendo,afterendo),(frml,fname,expression) in frml_normalize_strip.items() }
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 : TYPE
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
- updatemodel = mp.tofrml(updateeq) # create the moedl with the usual processing of frml's
3309
- while ' ' in updatemodel:
3310
- updatemodel = updatemodel.replace(' ', ' ')
3311
- frml2 = updatemodel.split('$')
3312
- frml2_strip = [mp.split_frml(f+'$') for f in frml2 if len(f)>2]
3313
- frml2_normal = [[frml,fname,
3314
- normal(expression[:-1],do_preprocess=do_preprocess,add_add_factor=add_add_factor,
3315
- make_fixable = pt.kw_frml_name(fname.upper(),'FIXABLE'))]
3316
- for allfrml,frml,fname,expression in frml2_strip]
3317
- frmldict_update = {nexpression.endo_var: f'{frml} {fname} {nexpression.normalized}$' for
3318
- frml,fname,nexpression in frml2_normal}
3319
-
3320
-
3321
- if add_add_factor:
3322
- frmldict_calc_add = {nexpression.endo_var: f'{frml} {fname} {nexpression.calc_add_factor}$' for
3323
- frml,fname,nexpression in frml2_normal if nexpression.endo_var in self.endogene|self.exogene|dfvars }
3324
- else:
3325
- frmldict_calc_add={}
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=self.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
- lines.extend([
1775
- "",
1776
- "| Parameter | Estimate |",
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
- lines.append(
1781
- f"| {_markdown_escape_cell(key)} | {_markdown_escape_cell(_markdown_format_number(coefs[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
- # -- variable discovery (cached, derived from mdummy) ---------------------
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelflowib
3
- Version: 2.76
3
+ Version: 2.78
4
4
  Summary: A tool to solve and manage dynamic economic and financial models
5
5
  Author-email: Ib Hansen <Ib.Hansen.Iv@gmail.com>
6
6
  License-Expression: MIT
@@ -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 has_est and rec_i < len(est_records):
206
- ebll()
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 Mexplode model: {name}")
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
- udtryk_up=fordlog+'DIFF(LOG('+dlogudtryk_up+'))'+efterdlog
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
- udtryk_up=forlogit+'(LOG('+logitudtryk_up+'/(1.0 -'+logitudtryk_up+')))'+efterlogit
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
- udtryk_up=f'{forpc} (100 * ( ({pcudtryk}) / ({lagone(pcudtryk,funks=funks)}) -1)) {efterpc}'
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
- udtryk_up=f'{forpc} (100 * ( ({pcudtryk}) / ({lagone(pcudtryk,funks=funks,laglead=-4)}) -1)) {efterpc}'
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
- udtryk_up=fordif+'(('+difudtryk_up+')-('+lagone(difudtryk_up+'',funks=funks)+'))'+efterdif
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(d)',add_add_factor=0).fprint
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