@@ -2,6 +2,8 @@ package vecxt_re
22
33import io .circe .syntax .*
44import io .github .quafadas .plots .SetupVega .{* , given }
5+ import vecxt_re .HillEstimator .HillPlotResult
6+ import vecxt_re .PickandsEstimator .PickandsPlotResult
57
68object Plots :
79 // These must be private otherwise scaladoc get crazy.
@@ -13,6 +15,8 @@ object Plots:
1315 private lazy val rootogram = VegaPlot .fromResource(" rootogram.vl.json" ) // hanging rootogram
1416 private lazy val pearsonResiduals = VegaPlot .fromResource(" pearsonResiduals.vl.json" ) // residual plot
1517 private lazy val poissonTrend = VegaPlot .fromResource(" poissonTrend.vl.json" ) // Poisson GLM trend
18+ private lazy val logLogPlot = VegaPlot .fromResource(" loglogCdf.vl.json" ) // log-log CDF plot
19+ private lazy val hillPlotSpec = VegaPlot .fromResource(" hillPlot.vl.json" ) // Hill plot for tail index
1620
1721 extension (idx : CalendarYearIndex )
1822 def plotIndex (reportingThreshold : Double )(using viz.LowPriorityPlotTarget ) =
@@ -351,6 +355,108 @@ object Plots:
351355 )
352356 end extension
353357
358+ extension (mixed : Mixed )
359+ /** Plot log-log comparison of theoretical Mixed distribution vs empirical sample data.
360+ *
361+ * This plot shows the complementary CDF (1 - CDF) on log-log scale, which is useful for visualizing tail behavior
362+ * of heavy-tailed distributions. The x-axis shows log(value) and y-axis shows log(1 - CDF).
363+ */
364+ def plotLogLogVsSample (samples : IndexedSeq [Double ], threshold : Double )(using viz.LowPriorityPlotTarget ) =
365+ val sortedSamples = samples.sorted
366+ val n = sortedSamples.length.toDouble
367+
368+ // Empirical survival function using Hazen plotting position
369+ val empiricalData = sortedSamples.zipWithIndex.collect {
370+ case (x, i) if x > 0 =>
371+ val survivalProb = (n - i - 0.5 ) / n
372+ if survivalProb > 0 then Some ((x = x, y = survivalProb, source = " empirical" ))
373+ else None
374+ end if
375+ }.flatten
376+
377+ // Theoretical survival function (1 - CDF)
378+ // For Pareto: S(x) = (xₘ/x)^α, so log(S) = α·log(xₘ) - α·log(x) is linear
379+ val minX = sortedSamples.filter(_ > 0 ).headOption.getOrElse(1.0 )
380+ val maxX = sortedSamples.last
381+ val numPoints = 200
382+ val theoreticalData = (0 until numPoints).flatMap { i =>
383+ val x = minX * math.pow(maxX / minX, i.toDouble / (numPoints - 1 ))
384+ val survivalProb = 1.0 - mixed.cdf(x)
385+ if survivalProb > 1e-10 && x > 0 then Some ((x = x, y = survivalProb, source = " model" ))
386+ else None
387+ end if
388+ }
389+
390+ val allData = theoreticalData ++ empiricalData
391+
392+ logLogPlot.plot(
393+ _.title(s " Mixed Distribution Log-Log Plot " ),
394+ _.data.values := allData.asJson,
395+ _.layer._0.encoding.x.scale.domainMin := threshold
396+ )
397+ end plotLogLogVsSample
398+ end extension
399+
400+ extension (hp : HillPlotResult )
401+ /** Plot a Hill plot showing tail index estimates α̂(k) vs k.
402+ *
403+ * A Hill plot helps identify the optimal number of upper order statistics to use for Pareto tail estimation. Look
404+ * for a stable plateau region where the estimate is relatively constant - this indicates the range of k where the
405+ * Pareto assumption holds. Too small k gives high variance; too large k includes non-tail observations.
406+ */
407+ def plotHill (using viz.LowPriorityPlotTarget ) =
408+ val data = hp.kValues.zip(hp.estimates).map { case (k, est) =>
409+ (k = k, estimate = est)
410+ }
411+
412+ hillPlotSpec.plot(
413+ _.title(" Hill Plot for Pareto Tail Index Estimation" ),
414+ _.data.values := data.asJson
415+ )
416+ end plotHill
417+ end extension
418+
419+ extension (pp : PickandsPlotResult )
420+ /** Plot a Pickands plot showing tail index estimates α̂(k) = 1/γ̂(k) vs k.
421+ *
422+ * The Pickands estimator is more robust than Hill to model misspecification but has higher variance. Look for a
423+ * stable plateau region. Unlike the Hill plot, the Pickands estimator can give negative γ values for light-tailed
424+ * distributions; only positive γ (and thus positive α) indicates heavy tails.
425+ */
426+ def plotPickands (using viz.LowPriorityPlotTarget ) =
427+ val data = pp.kValues
428+ .zip(pp.alphaEstimates)
429+ .filter { case (_, est) => ! est.isNaN && est.isFinite && est > 0 }
430+ .map { case (k, est) =>
431+ (k = k, estimate = est)
432+ }
433+
434+ hillPlotSpec.plot(
435+ _.title(" Pickands Plot for Pareto Tail Index Estimation" ),
436+ _.data.values := data.asJson
437+ )
438+ end plotPickands
439+
440+ /** Plot the raw extreme value index γ̂(k) from Pickands estimator.
441+ *
442+ * For heavy-tailed data, γ > 0. For light-tailed data (e.g., exponential), γ = 0. For bounded distributions, γ <
443+ * 0.
444+ */
445+ def plotPickandsGamma (using viz.LowPriorityPlotTarget ) =
446+ val data = pp.kValues
447+ .zip(pp.gammaEstimates)
448+ .filter { case (_, est) => ! est.isNaN && est.isFinite }
449+ .map { case (k, est) =>
450+ (k = k, estimate = est)
451+ }
452+
453+ hillPlotSpec.plot(
454+ _.title(" Pickands Plot for Extreme Value Index γ" ),
455+ _.data.values := data.asJson
456+ )
457+ end plotPickandsGamma
458+ end extension
459+
354460 // extension (negBin: NegativeBinomial)
355461 // inline def plotPdf(using viz.LowPriorityPlotTarget) =
356462 // val numPoints = 1000
0 commit comments