jump-diffusion-estimation 0.2.0__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.
- jump_diffusion/__init__.py +40 -0
- jump_diffusion/distributions/__init__.py +22 -0
- jump_diffusion/distributions/base.py +152 -0
- jump_diffusion/distributions/kou.py +104 -0
- jump_diffusion/distributions/normal.py +73 -0
- jump_diffusion/distributions/sged.py +113 -0
- jump_diffusion/distributions/skew_normal.py +97 -0
- jump_diffusion/distributions/student_t.py +86 -0
- jump_diffusion/estimation/__init__.py +19 -0
- jump_diffusion/estimation/base_estimator.py +62 -0
- jump_diffusion/estimation/maximum_likelihood.py +1239 -0
- jump_diffusion/models/__init__.py +14 -0
- jump_diffusion/models/base_model.py +93 -0
- jump_diffusion/models/jump_diffusion.py +168 -0
- jump_diffusion/scripts/__init__.py +3 -0
- jump_diffusion/scripts/benchmark.py +10 -0
- jump_diffusion/scripts/validate.py +74 -0
- jump_diffusion/simulation/__init__.py +14 -0
- jump_diffusion/simulation/base_simulator.py +51 -0
- jump_diffusion/simulation/jump_diffusion_simulator.py +264 -0
- jump_diffusion/validation/__init__.py +14 -0
- jump_diffusion/validation/distribution_comparison.py +270 -0
- jump_diffusion/validation/monte_carlo.py +349 -0
- jump_diffusion_estimation-0.2.0.dist-info/METADATA +241 -0
- jump_diffusion_estimation-0.2.0.dist-info/RECORD +29 -0
- jump_diffusion_estimation-0.2.0.dist-info/WHEEL +5 -0
- jump_diffusion_estimation-0.2.0.dist-info/entry_points.txt +3 -0
- jump_diffusion_estimation-0.2.0.dist-info/licenses/LICENSE +25 -0
- jump_diffusion_estimation-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation and Testing Tools
|
|
3
|
+
|
|
4
|
+
This module provides tools for validating estimation methods
|
|
5
|
+
through Monte Carlo experiments and diagnostic tests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .monte_carlo import ValidationExperiment
|
|
9
|
+
from .distribution_comparison import JumpDistributionComparison
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ValidationExperiment",
|
|
13
|
+
"JumpDistributionComparison",
|
|
14
|
+
]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jump-distribution goodness-of-fit comparison.
|
|
3
|
+
|
|
4
|
+
This module fits JumpDiffusionEstimator under several candidate jump-size
|
|
5
|
+
distributions on the same data and compares how well each one fits, via
|
|
6
|
+
information criteria (AIC/BIC) and a Kolmogorov-Smirnov goodness-of-fit
|
|
7
|
+
test whose p-value is obtained by parametric bootstrap, so it accounts for
|
|
8
|
+
the fact that the parameters were estimated from the same data. The KS
|
|
9
|
+
machinery only relies on ``JumpDistribution.rvs`` (through model
|
|
10
|
+
simulation), so it works the same way for any current or future jump
|
|
11
|
+
distribution.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
from scipy.stats import ks_2samp
|
|
20
|
+
|
|
21
|
+
from ..distributions import JumpDistribution
|
|
22
|
+
from ..estimation import JumpDiffusionEstimator
|
|
23
|
+
from ..models.jump_diffusion import JumpDiffusionModel
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JumpDistributionComparison:
|
|
27
|
+
"""
|
|
28
|
+
Fits several jump-size distributions to the same data and compares
|
|
29
|
+
their goodness of fit.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, data: np.ndarray, dt: float):
|
|
33
|
+
"""
|
|
34
|
+
Parameters:
|
|
35
|
+
-----------
|
|
36
|
+
data : np.ndarray
|
|
37
|
+
One-dimensional array of observed increments.
|
|
38
|
+
dt : float
|
|
39
|
+
Time step size between consecutive increments.
|
|
40
|
+
"""
|
|
41
|
+
self.data = data
|
|
42
|
+
self.dt = dt
|
|
43
|
+
self.results: Dict[str, Dict[str, Any]] = {}
|
|
44
|
+
|
|
45
|
+
def _simulate_increments(
|
|
46
|
+
self,
|
|
47
|
+
model: JumpDiffusionModel,
|
|
48
|
+
size: int,
|
|
49
|
+
seed: Optional[int],
|
|
50
|
+
) -> np.ndarray:
|
|
51
|
+
"""Simulate ``size`` increments from ``model`` at this object's dt."""
|
|
52
|
+
_, path, _ = model.simulate(T=size * self.dt, n_steps=size, x0=0.0, seed=seed)
|
|
53
|
+
return np.diff(path)
|
|
54
|
+
|
|
55
|
+
def _parametric_bootstrap_ks_pvalue(
|
|
56
|
+
self,
|
|
57
|
+
jump_distribution: JumpDistribution,
|
|
58
|
+
fitted_model: JumpDiffusionModel,
|
|
59
|
+
ks_observed: float,
|
|
60
|
+
n_obs: int,
|
|
61
|
+
reference_size: int,
|
|
62
|
+
n_bootstrap: int,
|
|
63
|
+
seed: Optional[int],
|
|
64
|
+
estimate_kwargs: Dict[str, Any],
|
|
65
|
+
) -> float:
|
|
66
|
+
"""
|
|
67
|
+
Monte Carlo p-value for the KS goodness-of-fit statistic.
|
|
68
|
+
|
|
69
|
+
A plain two-sample KS p-value is invalid here because the reference
|
|
70
|
+
distribution was estimated from the very data being tested, which
|
|
71
|
+
makes the naive p-value strongly anti-conservative. The parametric
|
|
72
|
+
bootstrap restores validity: under the fitted (null) model we
|
|
73
|
+
simulate ``n_bootstrap`` datasets of the observed size, **re-fit** the
|
|
74
|
+
model on each, and recompute the KS distance of each dataset to its
|
|
75
|
+
own re-fitted model. The fraction of these bootstrap distances that
|
|
76
|
+
equal or exceed the observed one estimates the p-value, with the
|
|
77
|
+
usual ``(1 + count) / (n_successful + 1)`` small-sample correction.
|
|
78
|
+
Re-fitting inside the loop is what makes the test account for
|
|
79
|
+
estimation error.
|
|
80
|
+
|
|
81
|
+
Returns ``nan`` if ``n_bootstrap <= 0`` or no replicate converged.
|
|
82
|
+
"""
|
|
83
|
+
if n_bootstrap <= 0:
|
|
84
|
+
return float("nan")
|
|
85
|
+
|
|
86
|
+
exceed = 0
|
|
87
|
+
n_successful = 0
|
|
88
|
+
for b in range(n_bootstrap):
|
|
89
|
+
data_seed = None if seed is None else seed + 1000 + b
|
|
90
|
+
boot_data = self._simulate_increments(fitted_model, n_obs, data_seed)
|
|
91
|
+
|
|
92
|
+
estimator = JumpDiffusionEstimator(
|
|
93
|
+
boot_data, self.dt, jump_distribution=jump_distribution
|
|
94
|
+
)
|
|
95
|
+
rep_result = estimator.estimate(**estimate_kwargs)
|
|
96
|
+
if not rep_result["convergence"]:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
model_b = JumpDiffusionModel(
|
|
100
|
+
jump_distribution=jump_distribution, **rep_result["parameters"]
|
|
101
|
+
)
|
|
102
|
+
ref_seed = None if seed is None else seed + 100_000 + b
|
|
103
|
+
reference_b = self._simulate_increments(model_b, reference_size, ref_seed)
|
|
104
|
+
ks_b = float(ks_2samp(boot_data, reference_b).statistic)
|
|
105
|
+
if ks_b >= ks_observed:
|
|
106
|
+
exceed += 1
|
|
107
|
+
n_successful += 1
|
|
108
|
+
|
|
109
|
+
if n_successful == 0:
|
|
110
|
+
return float("nan")
|
|
111
|
+
return (1.0 + exceed) / (n_successful + 1.0)
|
|
112
|
+
|
|
113
|
+
def fit(
|
|
114
|
+
self,
|
|
115
|
+
name: str,
|
|
116
|
+
jump_distribution: JumpDistribution,
|
|
117
|
+
seed: Optional[int] = None,
|
|
118
|
+
n_bootstrap: int = 199,
|
|
119
|
+
ks_reference_size: Optional[int] = None,
|
|
120
|
+
**estimate_kwargs: Any,
|
|
121
|
+
) -> Dict[str, Any]:
|
|
122
|
+
r"""
|
|
123
|
+
Fit ``jump_distribution`` to the data and record its goodness of fit.
|
|
124
|
+
|
|
125
|
+
The KS statistic is the distance between the data and the fitted
|
|
126
|
+
model's distribution, the latter approximated by a large simulated
|
|
127
|
+
reference sample so that simulation noise in the statistic itself is
|
|
128
|
+
negligible. Its p-value is obtained by parametric bootstrap (see
|
|
129
|
+
:meth:`_parametric_bootstrap_ks_pvalue`), which is the dominant cost
|
|
130
|
+
of this method.
|
|
131
|
+
|
|
132
|
+
Parameters:
|
|
133
|
+
-----------
|
|
134
|
+
name : str
|
|
135
|
+
Label used to identify this fit in ``results``/``compare()``.
|
|
136
|
+
jump_distribution : JumpDistribution
|
|
137
|
+
Jump-size distribution to fit.
|
|
138
|
+
seed : int, optional
|
|
139
|
+
Base random seed. When given, the reference sample, the plotting
|
|
140
|
+
sample and every bootstrap replicate use distinct derived seeds,
|
|
141
|
+
so the whole ``fit`` is reproducible.
|
|
142
|
+
n_bootstrap : int
|
|
143
|
+
Number of parametric bootstrap replicates for the KS p-value.
|
|
144
|
+
Each re-fits the model, so cost scales linearly; set to ``0`` to
|
|
145
|
+
skip the p-value (it is then reported as ``nan``).
|
|
146
|
+
ks_reference_size : int, optional
|
|
147
|
+
Size of the reference sample used to evaluate the KS statistic.
|
|
148
|
+
Defaults to ``max(20 * n_obs, 20000)``. Larger values give a
|
|
149
|
+
more stable statistic at higher simulation cost.
|
|
150
|
+
\*\*estimate_kwargs : dict
|
|
151
|
+
Additional arguments forwarded to
|
|
152
|
+
``JumpDiffusionEstimator.estimate`` (also used for every
|
|
153
|
+
bootstrap re-fit).
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
--------
|
|
157
|
+
dict
|
|
158
|
+
The estimation results, extended with ``ks_statistic``,
|
|
159
|
+
``ks_pvalue`` (parametric bootstrap), ``ks_n_bootstrap`` and
|
|
160
|
+
``simulated_increments`` (an observation-sized sample kept for
|
|
161
|
+
:meth:`plot_comparison`).
|
|
162
|
+
"""
|
|
163
|
+
estimator = JumpDiffusionEstimator(
|
|
164
|
+
self.data, self.dt, jump_distribution=jump_distribution
|
|
165
|
+
)
|
|
166
|
+
result = dict(estimator.estimate(**estimate_kwargs))
|
|
167
|
+
|
|
168
|
+
fitted_model = JumpDiffusionModel(
|
|
169
|
+
jump_distribution=jump_distribution, **result["parameters"]
|
|
170
|
+
)
|
|
171
|
+
n_obs = len(self.data)
|
|
172
|
+
reference_size = (
|
|
173
|
+
ks_reference_size
|
|
174
|
+
if ks_reference_size is not None
|
|
175
|
+
else max(20 * n_obs, 20000)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Stable KS distance to the fitted model, via a large reference
|
|
179
|
+
# sample so the statistic is not dominated by simulation noise.
|
|
180
|
+
ref_seed = None if seed is None else seed
|
|
181
|
+
reference_increments = self._simulate_increments(
|
|
182
|
+
fitted_model, reference_size, ref_seed
|
|
183
|
+
)
|
|
184
|
+
ks_statistic = float(ks_2samp(self.data, reference_increments).statistic)
|
|
185
|
+
|
|
186
|
+
# A same-size sample kept only for the histogram overlay in
|
|
187
|
+
# plot_comparison (not used by the statistic).
|
|
188
|
+
plot_seed = None if seed is None else seed + 1
|
|
189
|
+
simulated_increments = self._simulate_increments(fitted_model, n_obs, plot_seed)
|
|
190
|
+
|
|
191
|
+
ks_pvalue = self._parametric_bootstrap_ks_pvalue(
|
|
192
|
+
jump_distribution,
|
|
193
|
+
fitted_model,
|
|
194
|
+
ks_statistic,
|
|
195
|
+
n_obs,
|
|
196
|
+
reference_size,
|
|
197
|
+
n_bootstrap,
|
|
198
|
+
seed,
|
|
199
|
+
estimate_kwargs,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
result["ks_statistic"] = ks_statistic
|
|
203
|
+
result["ks_pvalue"] = ks_pvalue
|
|
204
|
+
result["ks_n_bootstrap"] = n_bootstrap
|
|
205
|
+
result["simulated_increments"] = simulated_increments
|
|
206
|
+
|
|
207
|
+
self.results[name] = result
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
def compare(self) -> pd.DataFrame:
|
|
211
|
+
"""
|
|
212
|
+
Summarize all fitted distributions, ranked by AIC (best first).
|
|
213
|
+
"""
|
|
214
|
+
if not self.results:
|
|
215
|
+
print("No fits yet. Call fit() first.")
|
|
216
|
+
return pd.DataFrame()
|
|
217
|
+
|
|
218
|
+
rows = [
|
|
219
|
+
{
|
|
220
|
+
"distribution": name,
|
|
221
|
+
"log_likelihood": result["log_likelihood"],
|
|
222
|
+
"aic": result["aic"],
|
|
223
|
+
"bic": result["bic"],
|
|
224
|
+
"ks_statistic": result["ks_statistic"],
|
|
225
|
+
"ks_pvalue": result["ks_pvalue"],
|
|
226
|
+
"convergence": result["convergence"],
|
|
227
|
+
}
|
|
228
|
+
for name, result in self.results.items()
|
|
229
|
+
]
|
|
230
|
+
return pd.DataFrame(rows).sort_values("aic").reset_index(drop=True)
|
|
231
|
+
|
|
232
|
+
def plot_comparison(
|
|
233
|
+
self,
|
|
234
|
+
bins: int = 50,
|
|
235
|
+
figsize: Tuple[float, float] = (12, 6),
|
|
236
|
+
) -> None:
|
|
237
|
+
"""
|
|
238
|
+
Overlay a histogram of the real data with each fitted
|
|
239
|
+
distribution's simulated sample.
|
|
240
|
+
"""
|
|
241
|
+
if not self.results:
|
|
242
|
+
print("No fits yet. Call fit() first.")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
plt.figure(figsize=figsize)
|
|
246
|
+
plt.hist(
|
|
247
|
+
self.data,
|
|
248
|
+
bins=bins,
|
|
249
|
+
density=True,
|
|
250
|
+
alpha=0.35,
|
|
251
|
+
color="black",
|
|
252
|
+
label="Datos reales",
|
|
253
|
+
)
|
|
254
|
+
for name, result in self.results.items():
|
|
255
|
+
plt.hist(
|
|
256
|
+
result["simulated_increments"],
|
|
257
|
+
bins=bins,
|
|
258
|
+
density=True,
|
|
259
|
+
alpha=0.35,
|
|
260
|
+
histtype="step",
|
|
261
|
+
linewidth=2,
|
|
262
|
+
label=f"Simulado ({name})",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
plt.title("Comparación de distribuciones de salto")
|
|
266
|
+
plt.xlabel("Incremento")
|
|
267
|
+
plt.ylabel("Densidad")
|
|
268
|
+
plt.legend()
|
|
269
|
+
plt.grid(True, alpha=0.3)
|
|
270
|
+
plt.show()
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Monte Carlo validation experiments for jump-diffusion estimators.
|
|
3
|
+
|
|
4
|
+
This module provides tools to validate estimation methods through
|
|
5
|
+
controlled experiments with known parameters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
from typing import Dict, Any, List, Optional
|
|
12
|
+
from ..simulation import JumpDiffusionSimulator
|
|
13
|
+
from ..estimation import JumpDiffusionEstimator
|
|
14
|
+
from ..distributions import JumpDistribution
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ValidationExperiment:
|
|
18
|
+
"""
|
|
19
|
+
Monte Carlo validation experiment for jump-diffusion estimators.
|
|
20
|
+
|
|
21
|
+
This class orchestrates validation experiments where we simulate
|
|
22
|
+
data with known parameters and test how well our estimators
|
|
23
|
+
can recover those parameters.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
true_params: Dict[str, float],
|
|
29
|
+
jump_distribution: Optional[JumpDistribution] = None,
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
Initialize validation experiment.
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
35
|
+
-----------
|
|
36
|
+
true_params : dict
|
|
37
|
+
True parameter values to use for simulation
|
|
38
|
+
jump_distribution : JumpDistribution, optional
|
|
39
|
+
Jump size distribution to use for simulation and estimation
|
|
40
|
+
"""
|
|
41
|
+
self.true_params = true_params
|
|
42
|
+
self.jump_distribution = jump_distribution
|
|
43
|
+
self.results: pd.DataFrame = pd.DataFrame()
|
|
44
|
+
self.completed_experiments = 0
|
|
45
|
+
|
|
46
|
+
def run_experiment(
|
|
47
|
+
self,
|
|
48
|
+
n_simulations: int = 10,
|
|
49
|
+
T: float = 1.0,
|
|
50
|
+
n_steps: int = 252,
|
|
51
|
+
x0: float = 100.0,
|
|
52
|
+
seed_base: int = 42,
|
|
53
|
+
) -> pd.DataFrame:
|
|
54
|
+
"""
|
|
55
|
+
Run Monte Carlo validation experiment.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
-----------
|
|
59
|
+
n_simulations : int
|
|
60
|
+
Number of simulation runs
|
|
61
|
+
T : float
|
|
62
|
+
Time horizon for each simulation
|
|
63
|
+
n_steps : int
|
|
64
|
+
Number of time steps per simulation
|
|
65
|
+
x0 : float
|
|
66
|
+
Initial value
|
|
67
|
+
seed_base : int
|
|
68
|
+
Base seed for reproducibility
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
--------
|
|
72
|
+
pd.DataFrame
|
|
73
|
+
Results of all experiments
|
|
74
|
+
"""
|
|
75
|
+
print(f"Running {n_simulations} validation experiments...")
|
|
76
|
+
print(f"True parameters: {self.true_params}")
|
|
77
|
+
|
|
78
|
+
# Create simulator.
|
|
79
|
+
simulator = JumpDiffusionSimulator(
|
|
80
|
+
jump_distribution=self.jump_distribution,
|
|
81
|
+
**self.true_params, # type: ignore[arg-type]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
results: List[Dict[str, Any]] = []
|
|
85
|
+
successful_runs = 0
|
|
86
|
+
|
|
87
|
+
for i in range(n_simulations):
|
|
88
|
+
try:
|
|
89
|
+
# Simulate data
|
|
90
|
+
times, path, jumps = simulator.simulate_path(
|
|
91
|
+
T=T, n_steps=n_steps, x0=x0, seed=seed_base + i
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Estimate parameters
|
|
95
|
+
increments = np.diff(path)
|
|
96
|
+
dt = times[1] - times[0]
|
|
97
|
+
|
|
98
|
+
estimator = JumpDiffusionEstimator(
|
|
99
|
+
increments,
|
|
100
|
+
dt,
|
|
101
|
+
jump_distribution=self.jump_distribution,
|
|
102
|
+
)
|
|
103
|
+
est_results = estimator.estimate()
|
|
104
|
+
|
|
105
|
+
if est_results["convergence"]:
|
|
106
|
+
# Store results
|
|
107
|
+
row = {
|
|
108
|
+
"simulation_id": i + 1,
|
|
109
|
+
"seed": seed_base + i,
|
|
110
|
+
"convergence": True,
|
|
111
|
+
"log_likelihood": est_results["log_likelihood"],
|
|
112
|
+
"aic": est_results["aic"],
|
|
113
|
+
"bic": est_results["bic"],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Add estimated parameters
|
|
117
|
+
for param, value in est_results["parameters"].items():
|
|
118
|
+
row[f"{param}_est"] = value
|
|
119
|
+
row[f"{param}_true"] = self.true_params[param]
|
|
120
|
+
row[f"{param}_error"] = value - self.true_params[param]
|
|
121
|
+
row[f"{param}_rel_error"] = (
|
|
122
|
+
(value - self.true_params[param])
|
|
123
|
+
/ self.true_params[param]
|
|
124
|
+
* 100
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
results.append(row)
|
|
128
|
+
successful_runs += 1
|
|
129
|
+
print(
|
|
130
|
+
f"✓ Experiment {i+1}/{n_simulations} completed "
|
|
131
|
+
f"successfully",
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
message = "✗ Experiment {}/{} failed to converge".format(
|
|
135
|
+
i + 1,
|
|
136
|
+
n_simulations,
|
|
137
|
+
)
|
|
138
|
+
print(message)
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(
|
|
142
|
+
f"✗ Experiment {i+1}/{n_simulations} failed with error: "
|
|
143
|
+
f"{str(e)}",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
print(
|
|
147
|
+
f"\nCompleted: {successful_runs}/{n_simulations} successful "
|
|
148
|
+
f"experiments",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if successful_runs == 0:
|
|
152
|
+
print("No successful experiments. Cannot proceed with analysis.")
|
|
153
|
+
return pd.DataFrame()
|
|
154
|
+
|
|
155
|
+
self.results = pd.DataFrame(results)
|
|
156
|
+
self.completed_experiments = successful_runs
|
|
157
|
+
|
|
158
|
+
return self.results
|
|
159
|
+
|
|
160
|
+
def analyze_results(self) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Analyze validation results and compute statistics.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
--------
|
|
166
|
+
dict
|
|
167
|
+
Analysis results including bias, RMSE, etc.
|
|
168
|
+
"""
|
|
169
|
+
if len(self.results) == 0:
|
|
170
|
+
print("No results to analyze. Run experiment first.")
|
|
171
|
+
return {}
|
|
172
|
+
|
|
173
|
+
param_names = [col[:-4] for col in self.results.columns if col.endswith("_est")]
|
|
174
|
+
analysis = {}
|
|
175
|
+
|
|
176
|
+
print("\n" + "=" * 60)
|
|
177
|
+
print("VALIDATION ANALYSIS RESULTS")
|
|
178
|
+
print("=" * 60)
|
|
179
|
+
|
|
180
|
+
for param in param_names:
|
|
181
|
+
true_val = self.true_params[param]
|
|
182
|
+
estimated_vals = self.results[f"{param}_est"]
|
|
183
|
+
errors = self.results[f"{param}_error"]
|
|
184
|
+
rel_errors = self.results[f"{param}_rel_error"]
|
|
185
|
+
|
|
186
|
+
stats = {
|
|
187
|
+
"true_value": true_val,
|
|
188
|
+
"mean_estimate": np.mean(estimated_vals),
|
|
189
|
+
"bias": np.mean(errors),
|
|
190
|
+
"rmse": np.sqrt(np.mean(errors**2)),
|
|
191
|
+
"mae": np.mean(np.abs(errors)),
|
|
192
|
+
"mean_rel_error": np.mean(rel_errors),
|
|
193
|
+
"std_estimate": np.std(estimated_vals),
|
|
194
|
+
"coverage_95": np.mean(
|
|
195
|
+
np.abs(rel_errors) <= 5
|
|
196
|
+
), # Within 5% of true value
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
analysis[param] = stats
|
|
200
|
+
|
|
201
|
+
print(f"\n{param.upper()} (true: {true_val:.4f})")
|
|
202
|
+
print(f" Mean estimate: {stats['mean_estimate']:.6f}")
|
|
203
|
+
print(f" Bias: {stats['bias']:.6f}")
|
|
204
|
+
print(f" RMSE: {stats['rmse']:.6f}")
|
|
205
|
+
print(f" Mean rel. error: {stats['mean_rel_error']:.2f}%")
|
|
206
|
+
print(f" Std deviation: {stats['std_estimate']:.6f}")
|
|
207
|
+
print(f" 95% accuracy: {stats['coverage_95']:.1%}")
|
|
208
|
+
|
|
209
|
+
return analysis
|
|
210
|
+
|
|
211
|
+
def plot_results(self, figsize: tuple = (15, 10)):
|
|
212
|
+
"""Create plots summarizing parameter estimation accuracy.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
figsize : tuple, optional
|
|
217
|
+
Width and height of the Matplotlib figure in inches.
|
|
218
|
+
|
|
219
|
+
Notes
|
|
220
|
+
-----
|
|
221
|
+
Generates a grid of subplots showing scatter plots for
|
|
222
|
+
each parameter and a summary bar chart of bias and RMSE. The
|
|
223
|
+
:meth:`run_experiment` method must be called beforehand to
|
|
224
|
+
populate ``self.results``. This function displays the plots
|
|
225
|
+
directly and returns ``None``.
|
|
226
|
+
"""
|
|
227
|
+
if len(self.results) == 0:
|
|
228
|
+
print("No results to plot. Run experiment first.")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
param_names = [col[:-4] for col in self.results.columns if col.endswith("_est")]
|
|
232
|
+
label_map = {
|
|
233
|
+
"mu": "Drift (μ)",
|
|
234
|
+
"sigma": "Volatility (σ)",
|
|
235
|
+
"jump_prob": "Jump Prob (p)",
|
|
236
|
+
"jump_scale": "Jump Scale",
|
|
237
|
+
"jump_skew": "Skewness (α)",
|
|
238
|
+
"jump_loc": "Jump Location",
|
|
239
|
+
"jump_nu": "Kurtosis (ν)",
|
|
240
|
+
"jump_xi": "Asymmetry (ξ)",
|
|
241
|
+
"jump_df": "Degrees of Freedom (df)",
|
|
242
|
+
"jump_prob_up": "Jump Prob Up (p_up)",
|
|
243
|
+
"jump_scale_up": "Jump Scale Up (η_up)",
|
|
244
|
+
"jump_scale_down": "Jump Scale Down (η_down)",
|
|
245
|
+
}
|
|
246
|
+
param_labels = [
|
|
247
|
+
label_map.get(p, p.replace("_", " ").title()) for p in param_names
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
n_plots = len(param_names) + 1
|
|
251
|
+
n_cols = 3
|
|
252
|
+
n_rows = int(np.ceil(n_plots / n_cols))
|
|
253
|
+
|
|
254
|
+
fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize)
|
|
255
|
+
axes = axes.flatten()
|
|
256
|
+
|
|
257
|
+
# Parameter accuracy plots
|
|
258
|
+
for i, (param, label) in enumerate(zip(param_names, param_labels)):
|
|
259
|
+
ax = axes[i]
|
|
260
|
+
|
|
261
|
+
true_val = self.true_params[param]
|
|
262
|
+
estimated_vals = self.results[f"{param}_est"]
|
|
263
|
+
|
|
264
|
+
# Scatter plot: estimated vs true
|
|
265
|
+
ax.scatter(
|
|
266
|
+
np.full_like(estimated_vals, true_val),
|
|
267
|
+
estimated_vals,
|
|
268
|
+
alpha=0.6,
|
|
269
|
+
s=50,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Perfect estimation line
|
|
273
|
+
ax.axline(
|
|
274
|
+
(true_val, true_val),
|
|
275
|
+
slope=1,
|
|
276
|
+
color="red",
|
|
277
|
+
linestyle="--",
|
|
278
|
+
alpha=0.8,
|
|
279
|
+
label="Perfect Estimation",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Confidence bands (±10% and ±20%)
|
|
283
|
+
ax.axhspan(
|
|
284
|
+
true_val * 0.9,
|
|
285
|
+
true_val * 1.1,
|
|
286
|
+
alpha=0.2,
|
|
287
|
+
color="green",
|
|
288
|
+
label="±10% band",
|
|
289
|
+
)
|
|
290
|
+
ax.axhspan(
|
|
291
|
+
true_val * 0.8,
|
|
292
|
+
true_val * 1.2,
|
|
293
|
+
alpha=0.1,
|
|
294
|
+
color="yellow",
|
|
295
|
+
label="±20% band",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
ax.set_xlabel("True Value")
|
|
299
|
+
ax.set_ylabel("Estimated Value")
|
|
300
|
+
ax.set_title(f"{label}")
|
|
301
|
+
ax.grid(True, alpha=0.3)
|
|
302
|
+
|
|
303
|
+
if i == 0: # Add legend to first plot
|
|
304
|
+
ax.legend()
|
|
305
|
+
|
|
306
|
+
# Summary statistics plot
|
|
307
|
+
ax = axes[len(param_names)]
|
|
308
|
+
param_biases = []
|
|
309
|
+
for param in param_names:
|
|
310
|
+
param_biases.append(np.mean(self.results[f"{param}_rel_error"]))
|
|
311
|
+
param_rmses = [
|
|
312
|
+
np.sqrt(np.mean(self.results[f"{param}_error"] ** 2))
|
|
313
|
+
/ self.true_params[param]
|
|
314
|
+
* 100
|
|
315
|
+
for param in param_names
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
x = np.arange(len(param_names))
|
|
319
|
+
width = 0.35
|
|
320
|
+
|
|
321
|
+
ax.bar(
|
|
322
|
+
x - width / 2,
|
|
323
|
+
np.abs(param_biases),
|
|
324
|
+
width,
|
|
325
|
+
label="|Bias| (%)",
|
|
326
|
+
alpha=0.7,
|
|
327
|
+
)
|
|
328
|
+
ax.bar(
|
|
329
|
+
x + width / 2,
|
|
330
|
+
param_rmses,
|
|
331
|
+
width,
|
|
332
|
+
label="Rel. RMSE (%)",
|
|
333
|
+
alpha=0.7,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
ax.set_xlabel("Parameters")
|
|
337
|
+
ax.set_ylabel("Error (%)")
|
|
338
|
+
ax.set_title("Estimation Accuracy Summary")
|
|
339
|
+
ax.set_xticks(x)
|
|
340
|
+
ax.set_xticklabels(param_labels, rotation=45)
|
|
341
|
+
ax.legend()
|
|
342
|
+
ax.grid(True, alpha=0.3)
|
|
343
|
+
|
|
344
|
+
# Delete unused subplots
|
|
345
|
+
for j in range(n_plots, len(axes)):
|
|
346
|
+
fig.delaxes(axes[j])
|
|
347
|
+
|
|
348
|
+
plt.tight_layout()
|
|
349
|
+
plt.show()
|