twinfit 0.0.1__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.
twinfit-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michelangelo Dondi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
twinfit-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: twinfit
3
+ Version: 0.0.1
4
+ Summary: Bias correction of a fit through a simulator of the data (indirect inference)
5
+ Author: Michelangelo Dondi
6
+ License-Expression: MIT
7
+ Keywords: indirect inference,bias correction,simulation,fitting,systematic errors
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
13
+ Classifier: Topic :: Scientific/Engineering :: Physics
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy>=1.21
18
+ Requires-Dist: scipy>=1.7
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest; extra == "test"
21
+ Dynamic: license-file
22
+
23
+ # twinfit
24
+
25
+ Bias correction of a fit through a simulator of the data.
26
+
27
+ A fit maps a record to an estimate. When the estimate is biased, because the record is short, the fitted model is approximate or
28
+ the errors are not those the fit assumes, a simulator that is right where the fit is wrong can measure the bias. twinfit applies the
29
+ same fit to simulated records and corrects the estimate in one of two ways.
30
+
31
+ - `plugin_correction` measures the bias by simulating at the estimate, and subtracts it.
32
+ - `solved_correction` finds the parameters at which the mean fit of the simulations equals the fit of the data. This is indirect
33
+ inference with the fit as its auxiliary statistic (Gourieroux, Monfort and Renault 1993). The same random draws are used at every
34
+ trial, so the equation is smooth whenever the fit is.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install twinfit
40
+ ```
41
+
42
+ ## Example
43
+
44
+ The maximum likelihood estimate of a Gaussian variance from ten draws is biased low by a factor 9/10. The solved correction removes
45
+ that bias in expectation. A single record keeps its sampling error, so one corrected estimate can still sit far from the truth.
46
+
47
+ ```python
48
+ import numpy as np
49
+ from twinfit import solved_correction
50
+
51
+ def simulate(theta, rng): # one record at the parameters theta
52
+ return np.sqrt(theta[0]) * rng.standard_normal(10)
53
+
54
+ def fit(record): # the fit to correct
55
+ return np.var(record)
56
+
57
+ record = simulate([2.0], np.random.default_rng(1))
58
+ out = solved_correction(fit(record), simulate, fit, n_sim=400)
59
+ print(out.estimate, out.converged)
60
+ ```
61
+
62
+ The result also carries the derivative of the mean fit with respect to the parameters, so that
63
+ `out.delta_covariance(C)` carries a covariance `C` of the raw estimate to the corrected one.
64
+
65
+ ## Status
66
+
67
+ Version 0.0.1 is the first release. Further tools are in preparation.
68
+
69
+ ## Reference
70
+
71
+ C. Gourieroux, A. Monfort and E. Renault (1993). Indirect inference. *Journal of Applied Econometrics* 8, S85-S118.
72
+ doi:10.1002/jae.3950080507
73
+
74
+ ## Licence
75
+
76
+ MIT.
@@ -0,0 +1,54 @@
1
+ # twinfit
2
+
3
+ Bias correction of a fit through a simulator of the data.
4
+
5
+ A fit maps a record to an estimate. When the estimate is biased, because the record is short, the fitted model is approximate or
6
+ the errors are not those the fit assumes, a simulator that is right where the fit is wrong can measure the bias. twinfit applies the
7
+ same fit to simulated records and corrects the estimate in one of two ways.
8
+
9
+ - `plugin_correction` measures the bias by simulating at the estimate, and subtracts it.
10
+ - `solved_correction` finds the parameters at which the mean fit of the simulations equals the fit of the data. This is indirect
11
+ inference with the fit as its auxiliary statistic (Gourieroux, Monfort and Renault 1993). The same random draws are used at every
12
+ trial, so the equation is smooth whenever the fit is.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install twinfit
18
+ ```
19
+
20
+ ## Example
21
+
22
+ The maximum likelihood estimate of a Gaussian variance from ten draws is biased low by a factor 9/10. The solved correction removes
23
+ that bias in expectation. A single record keeps its sampling error, so one corrected estimate can still sit far from the truth.
24
+
25
+ ```python
26
+ import numpy as np
27
+ from twinfit import solved_correction
28
+
29
+ def simulate(theta, rng): # one record at the parameters theta
30
+ return np.sqrt(theta[0]) * rng.standard_normal(10)
31
+
32
+ def fit(record): # the fit to correct
33
+ return np.var(record)
34
+
35
+ record = simulate([2.0], np.random.default_rng(1))
36
+ out = solved_correction(fit(record), simulate, fit, n_sim=400)
37
+ print(out.estimate, out.converged)
38
+ ```
39
+
40
+ The result also carries the derivative of the mean fit with respect to the parameters, so that
41
+ `out.delta_covariance(C)` carries a covariance `C` of the raw estimate to the corrected one.
42
+
43
+ ## Status
44
+
45
+ Version 0.0.1 is the first release. Further tools are in preparation.
46
+
47
+ ## Reference
48
+
49
+ C. Gourieroux, A. Monfort and E. Renault (1993). Indirect inference. *Journal of Applied Econometrics* 8, S85-S118.
50
+ doi:10.1002/jae.3950080507
51
+
52
+ ## Licence
53
+
54
+ MIT.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "twinfit"
7
+ version = "0.0.1"
8
+ description = "Bias correction of a fit through a simulator of the data (indirect inference)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Michelangelo Dondi" }]
13
+ requires-python = ">=3.9"
14
+ dependencies = ["numpy>=1.21", "scipy>=1.7"]
15
+ keywords = ["indirect inference", "bias correction", "simulation", "fitting", "systematic errors"]
16
+ classifiers = [
17
+ "Development Status :: 2 - Pre-Alpha",
18
+ "Intended Audience :: Science/Research",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Scientific/Engineering :: Mathematics",
22
+ "Topic :: Scientific/Engineering :: Physics",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ test = ["pytest"]
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """twinfit: bias correction of a fit through a simulator of the data.
2
+
3
+ The first release provides the plug-in and the solved (indirect inference) corrections, see ``twinfit.indirect``.
4
+ """
5
+ from .indirect import Correction, plugin_correction, solved_correction
6
+
7
+ __all__ = ["Correction", "plugin_correction", "solved_correction", "__version__"]
8
+ __version__ = "0.0.1"
@@ -0,0 +1,125 @@
1
+ """Bias correction of a fit through a simulator of the data.
2
+
3
+ A fit maps a record to an estimate. When the estimate is biased, because the record is short, the fitted model is approximate or the
4
+ errors are not those the fit assumes, a simulator of the record that is right where the fit is wrong can measure that bias. Two
5
+ corrections are provided, both for a user's fit and a user's simulator.
6
+
7
+ ``plugin_correction``
8
+ One step. The bias is measured by simulating at the estimate and fitting the simulations, and it is subtracted.
9
+
10
+ ``solved_correction``
11
+ Indirect inference with the fit as its auxiliary statistic (Gourieroux, Monfort and Renault 1993). The parameters are found at
12
+ which the mean fit of the simulations equals the fit of the data. The same random draws are used at every trial (common random
13
+ numbers), so the equation is smooth in the parameters whenever the fit is.
14
+
15
+ The simulator is a callable ``simulate(theta, rng)`` returning one record, where ``rng`` is a ``numpy.random.Generator``. A
16
+ deterministic simulator, one that ignores ``rng``, gives the binding function itself and needs only ``n_sim=1``. The fit is a
17
+ callable ``fit(record)`` returning the estimate, a scalar or a one-dimensional array of the same length as ``theta``.
18
+
19
+ With ``n_sim`` simulations the solved estimate carries, besides the sampling error of the data, a simulation error that inflates
20
+ its variance by about ``1 + 1/n_sim`` when the simulator draws errors of the data's law (Gourieroux, Monfort and Renault 1993).
21
+
22
+ Reference: C. Gourieroux, A. Monfort and E. Renault (1993), Indirect inference, Journal of Applied Econometrics 8, S85-S118,
23
+ doi:10.1002/jae.3950080507.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass, field
28
+
29
+ import numpy as np
30
+ from scipy import optimize
31
+
32
+ __all__ = ["Correction", "plugin_correction", "solved_correction"]
33
+
34
+
35
+ @dataclass
36
+ class Correction:
37
+ """The result of a correction.
38
+
39
+ estimate: the corrected parameters.
40
+ raw: the estimate that was corrected.
41
+ bias: the mean fit of the simulations minus the parameters they were simulated at, evaluated at the corrected estimate for the
42
+ solved form and at the raw estimate for the plug-in form.
43
+ jacobian: the derivative of the mean fit of the simulations with respect to the parameters at the corrected estimate (solved
44
+ form only, otherwise None). It carries a covariance of the raw estimate to the corrected one, see ``delta_covariance``.
45
+ converged: whether the solver met its tolerance (always True for the plug-in form).
46
+ n_sim: the number of simulations.
47
+ message: the solver's message.
48
+ """
49
+
50
+ estimate: np.ndarray
51
+ raw: np.ndarray
52
+ bias: np.ndarray
53
+ jacobian: np.ndarray | None = None
54
+ converged: bool = True
55
+ n_sim: int = 0
56
+ message: str = field(default="")
57
+
58
+ def delta_covariance(self, raw_covariance):
59
+ """The covariance of the corrected estimate from that of the raw one, through the delta method: J^-1 C J^-T."""
60
+ if self.jacobian is None:
61
+ raise ValueError("delta_covariance needs the solved form's jacobian")
62
+ c = np.atleast_2d(np.asarray(raw_covariance, float))
63
+ jinv = np.linalg.inv(self.jacobian)
64
+ return jinv @ c @ jinv.T
65
+
66
+
67
+ def _seeds(seed, n_sim):
68
+ if n_sim < 1:
69
+ raise ValueError("n_sim must be at least 1")
70
+ return np.random.SeedSequence(seed).spawn(n_sim)
71
+
72
+
73
+ def _mean_fit(theta, simulate, fit, seeds):
74
+ values = [np.atleast_1d(np.asarray(fit(simulate(theta, np.random.default_rng(s))), float)) for s in seeds]
75
+ out = np.mean(values, axis=0)
76
+ if out.shape != np.shape(theta):
77
+ raise ValueError(f"the fit returned shape {out.shape} for parameters of shape {np.shape(theta)}")
78
+ return out
79
+
80
+
81
+ def _as_parameters(estimate):
82
+ theta = np.atleast_1d(np.asarray(estimate, float)).copy()
83
+ if theta.ndim != 1:
84
+ raise ValueError("the estimate must be a scalar or a one-dimensional array")
85
+ return theta
86
+
87
+
88
+ def plugin_correction(estimate, simulate, fit, n_sim=200, seed=0):
89
+ """Subtract the bias measured at the estimate: theta_hat - (mean fit of simulations at theta_hat - theta_hat)."""
90
+ raw = _as_parameters(estimate)
91
+ bias = _mean_fit(raw, simulate, fit, _seeds(seed, n_sim)) - raw
92
+ return Correction(estimate=raw - bias, raw=raw, bias=bias, n_sim=n_sim, message="plug-in")
93
+
94
+
95
+ def _jacobian(theta, simulate, fit, seeds, rel_step):
96
+ k = theta.size
97
+ jac = np.empty((k, k))
98
+ for j in range(k):
99
+ h = rel_step * max(1.0, abs(theta[j]))
100
+ up, down = theta.copy(), theta.copy()
101
+ up[j] += h
102
+ down[j] -= h
103
+ jac[:, j] = (_mean_fit(up, simulate, fit, seeds) - _mean_fit(down, simulate, fit, seeds)) / (2 * h)
104
+ return jac
105
+
106
+
107
+ def solved_correction(estimate, simulate, fit, n_sim=200, seed=0, x0=None, tol=1e-10, rel_step=1e-6, maxfev=2000):
108
+ """Solve mean fit of simulations at theta = estimate for theta, with common random numbers.
109
+
110
+ The search starts at ``x0`` when given and otherwise at the plug-in correction, which is exact when the bias does not depend on
111
+ the parameters.
112
+ """
113
+ raw = _as_parameters(estimate)
114
+ seeds = _seeds(seed, n_sim)
115
+ start = _as_parameters(x0) if x0 is not None else raw - (_mean_fit(raw, simulate, fit, seeds) - raw)
116
+
117
+ def equation(theta):
118
+ return _mean_fit(theta, simulate, fit, seeds) - raw
119
+
120
+ sol = optimize.root(equation, start, method="hybr", tol=tol, options={"maxfev": maxfev})
121
+ theta = np.asarray(sol.x, float)
122
+ jac = _jacobian(theta, simulate, fit, seeds, rel_step)
123
+ bias = _mean_fit(theta, simulate, fit, seeds) - theta
124
+ return Correction(estimate=theta, raw=raw, bias=bias, jacobian=jac, converged=bool(sol.success), n_sim=n_sim,
125
+ message=str(sol.message))
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: twinfit
3
+ Version: 0.0.1
4
+ Summary: Bias correction of a fit through a simulator of the data (indirect inference)
5
+ Author: Michelangelo Dondi
6
+ License-Expression: MIT
7
+ Keywords: indirect inference,bias correction,simulation,fitting,systematic errors
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
13
+ Classifier: Topic :: Scientific/Engineering :: Physics
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy>=1.21
18
+ Requires-Dist: scipy>=1.7
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest; extra == "test"
21
+ Dynamic: license-file
22
+
23
+ # twinfit
24
+
25
+ Bias correction of a fit through a simulator of the data.
26
+
27
+ A fit maps a record to an estimate. When the estimate is biased, because the record is short, the fitted model is approximate or
28
+ the errors are not those the fit assumes, a simulator that is right where the fit is wrong can measure the bias. twinfit applies the
29
+ same fit to simulated records and corrects the estimate in one of two ways.
30
+
31
+ - `plugin_correction` measures the bias by simulating at the estimate, and subtracts it.
32
+ - `solved_correction` finds the parameters at which the mean fit of the simulations equals the fit of the data. This is indirect
33
+ inference with the fit as its auxiliary statistic (Gourieroux, Monfort and Renault 1993). The same random draws are used at every
34
+ trial, so the equation is smooth whenever the fit is.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install twinfit
40
+ ```
41
+
42
+ ## Example
43
+
44
+ The maximum likelihood estimate of a Gaussian variance from ten draws is biased low by a factor 9/10. The solved correction removes
45
+ that bias in expectation. A single record keeps its sampling error, so one corrected estimate can still sit far from the truth.
46
+
47
+ ```python
48
+ import numpy as np
49
+ from twinfit import solved_correction
50
+
51
+ def simulate(theta, rng): # one record at the parameters theta
52
+ return np.sqrt(theta[0]) * rng.standard_normal(10)
53
+
54
+ def fit(record): # the fit to correct
55
+ return np.var(record)
56
+
57
+ record = simulate([2.0], np.random.default_rng(1))
58
+ out = solved_correction(fit(record), simulate, fit, n_sim=400)
59
+ print(out.estimate, out.converged)
60
+ ```
61
+
62
+ The result also carries the derivative of the mean fit with respect to the parameters, so that
63
+ `out.delta_covariance(C)` carries a covariance `C` of the raw estimate to the corrected one.
64
+
65
+ ## Status
66
+
67
+ Version 0.0.1 is the first release. Further tools are in preparation.
68
+
69
+ ## Reference
70
+
71
+ C. Gourieroux, A. Monfort and E. Renault (1993). Indirect inference. *Journal of Applied Econometrics* 8, S85-S118.
72
+ doi:10.1002/jae.3950080507
73
+
74
+ ## Licence
75
+
76
+ MIT.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/twinfit/__init__.py
5
+ src/twinfit/indirect.py
6
+ src/twinfit.egg-info/PKG-INFO
7
+ src/twinfit.egg-info/SOURCES.txt
8
+ src/twinfit.egg-info/dependency_links.txt
9
+ src/twinfit.egg-info/requires.txt
10
+ src/twinfit.egg-info/top_level.txt
11
+ tests/test_indirect.py
@@ -0,0 +1,5 @@
1
+ numpy>=1.21
2
+ scipy>=1.7
3
+
4
+ [test]
5
+ pytest
@@ -0,0 +1 @@
1
+ twinfit
@@ -0,0 +1,73 @@
1
+ """Checks of the two corrections on a case where the answer is known in closed form.
2
+
3
+ The maximum likelihood estimate of a Gaussian variance from n draws, the mean of the squared deviations, has mean (n - 1)/n times the
4
+ variance. With the simulator sqrt(theta) z the mean fit of the simulations is theta times a constant c fixed by the draws, so the
5
+ solved correction is exactly estimate / c and the plug-in one exactly estimate (2 - c), and c itself sits near (n - 1)/n.
6
+ """
7
+ import numpy as np
8
+ import pytest
9
+
10
+ from twinfit import plugin_correction, solved_correction
11
+
12
+ N = 10
13
+
14
+
15
+ def simulate_var(theta, rng):
16
+ return np.sqrt(theta[0]) * rng.standard_normal(N)
17
+
18
+
19
+ def fit_var(x):
20
+ return np.var(x)
21
+
22
+
23
+ def constant_c(n_sim, seed):
24
+ seeds = np.random.SeedSequence(seed).spawn(n_sim)
25
+ return np.mean([np.var(np.random.default_rng(s).standard_normal(N)) for s in seeds])
26
+
27
+
28
+ def test_solved_is_exact_for_a_scale_family():
29
+ est, n_sim, seed = 1.7, 400, 3
30
+ c = constant_c(n_sim, seed)
31
+ out = solved_correction(est, simulate_var, fit_var, n_sim=n_sim, seed=seed)
32
+ assert out.converged
33
+ assert out.estimate[0] == pytest.approx(est / c, rel=1e-9)
34
+ assert out.jacobian[0, 0] == pytest.approx(c, rel=1e-5)
35
+
36
+
37
+ def test_plugin_is_exact_algebra():
38
+ est, n_sim, seed = 1.7, 400, 3
39
+ c = constant_c(n_sim, seed)
40
+ out = plugin_correction(est, simulate_var, fit_var, n_sim=n_sim, seed=seed)
41
+ assert out.estimate[0] == pytest.approx(est * (2 - c), rel=1e-12)
42
+
43
+
44
+ def test_the_constant_is_the_textbook_bias():
45
+ n_sim = 4000
46
+ c = constant_c(n_sim, 11)
47
+ sd = np.sqrt(2 * (N - 1)) / N / np.sqrt(n_sim) # sd of the mean of var(z) over n_sim draws
48
+ assert abs(c - (N - 1) / N) < 3 * sd
49
+
50
+
51
+ def test_vector_parameters_and_delta_covariance():
52
+ def simulate(theta, rng):
53
+ return theta[0] + np.sqrt(theta[1]) * rng.standard_normal(N)
54
+
55
+ def fit(x):
56
+ return np.array([np.mean(x), np.var(x)])
57
+
58
+ out = solved_correction([0.3, 2.0], simulate, fit, n_sim=300, seed=5)
59
+ seeds = np.random.SeedSequence(5).spawn(300)
60
+ zs = [np.random.default_rng(s).standard_normal(N) for s in seeds]
61
+ m, c = np.mean([z.mean() for z in zs]), np.mean([z.var() for z in zs])
62
+ assert out.converged
63
+ # mean fit: (mu + sqrt(v) m, v c), solved for (0.3, 2.0)
64
+ v = 2.0 / c
65
+ assert out.estimate[1] == pytest.approx(v, rel=1e-8)
66
+ assert out.estimate[0] == pytest.approx(0.3 - np.sqrt(v) * m, rel=1e-8, abs=1e-10)
67
+ cov = out.delta_covariance(np.diag([0.1, 0.2]))
68
+ assert cov.shape == (2, 2) and np.all(np.linalg.eigvalsh(cov) > 0)
69
+
70
+
71
+ def test_shape_mismatch_is_refused():
72
+ with pytest.raises(ValueError):
73
+ plugin_correction([1.0, 2.0], simulate_var, fit_var, n_sim=3)