Skip to content

Commit 02bd306

Browse files
committed
.
1 parent 98696e2 commit 02bd306

10 files changed

Lines changed: 1002 additions & 0 deletions

File tree

vecxt/src/intarray.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import scala.util.control.Breaks.*
55

66
import vecxt.BooleanArrays.trues
77
import vecxt.BoundsCheck.BoundsCheck
8+
import vecxt.arrays.sumSIMD
89

910
object IntArrays:
1011

@@ -35,6 +36,25 @@ object IntArrays:
3536
out
3637
end select
3738

39+
inline def countsToIdx: Array[Int] =
40+
var total = arr.sumSIMD
41+
var i = 0
42+
val out = new Array[Int](total)
43+
var j = 0
44+
while i < arr.length do
45+
val count = arr(i)
46+
val idx = i + 1
47+
var k = 0
48+
while k < count do
49+
out(j) = idx
50+
j += 1
51+
k += 1
52+
end while
53+
i += 1
54+
end while
55+
out
56+
end countsToIdx
57+
3858
inline def contiguous: Boolean =
3959
var i = 1
4060
var out = true

vecxt/test/src/intarray.test.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,31 @@ class IntArrayExtensionSuite extends munit.FunSuite:
133133
assertVecEquals(base.select(idx), Array.emptyIntArray)
134134
}
135135

136+
test("countsToIdx basic") {
137+
val counts = Array(2, 3, 1)
138+
// 2 ones, 3 twos, 1 three => [1, 1, 2, 2, 2, 3]
139+
assertVecEquals(counts.countsToIdx, Array(1, 1, 2, 2, 2, 3))
140+
}
141+
142+
test("countsToIdx with zeros") {
143+
val counts = Array(1, 0, 2, 0, 1)
144+
// 1 one, 0 twos, 2 threes, 0 fours, 1 five => [1, 3, 3, 5]
145+
assertVecEquals(counts.countsToIdx, Array(1, 3, 3, 5))
146+
}
147+
148+
test("countsToIdx empty array") {
149+
val counts = Array.emptyIntArray
150+
assertVecEquals(counts.countsToIdx, Array.emptyIntArray)
151+
}
152+
153+
test("countsToIdx all zeros") {
154+
val counts = Array(0, 0, 0)
155+
assertVecEquals(counts.countsToIdx, Array.emptyIntArray)
156+
}
157+
158+
test("countsToIdx single element") {
159+
val counts = Array(5)
160+
assertVecEquals(counts.countsToIdx, Array(1, 1, 1, 1, 1))
161+
}
162+
136163
end IntArrayExtensionSuite
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"$schema": "https://vega.github.io/schema/vega-lite/v6.json",
3+
"title": "Hill Plot",
4+
"width": "container",
5+
"height": "container",
6+
"data": {
7+
"values": [
8+
{ "k": 10, "estimate": 2.1 },
9+
{ "k": 20, "estimate": 2.05 },
10+
{ "k": 30, "estimate": 1.98 },
11+
{ "k": 40, "estimate": 2.02 },
12+
{ "k": 50, "estimate": 1.95 }
13+
]
14+
},
15+
"mark": {
16+
"type": "line",
17+
"point": { "filled": true, "size": 40 },
18+
"tooltip": { "content": "data" }
19+
},
20+
"encoding": {
21+
"x": {
22+
"field": "k",
23+
"type": "quantitative",
24+
"title": "k (number of upper order statistics)"
25+
},
26+
"y": {
27+
"field": "estimate",
28+
"type": "quantitative",
29+
"title": "Tail Index Estimate α̂(k)"
30+
},
31+
"color": { "value": "steelblue" }
32+
}
33+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"$schema": "https://vega.github.io/schema/vega-lite/v6.json",
3+
"title": "Log-Log CDF Plot",
4+
"width": "container",
5+
"height": "container",
6+
"data": {
7+
"values": [
8+
{ "x": 1, "y": 0.9, "source": "model" },
9+
{ "x": 10, "y": 0.5, "source": "model" },
10+
{ "x": 100, "y": 0.1, "source": "model" },
11+
{ "x": 1000, "y": 0.01, "source": "model" },
12+
{ "x": 2, "y": 0.85, "source": "empirical" },
13+
{ "x": 15, "y": 0.55, "source": "empirical" },
14+
{ "x": 80, "y": 0.15, "source": "empirical" },
15+
{ "x": 500, "y": 0.05, "source": "empirical" }
16+
]
17+
},
18+
"layer": [
19+
{
20+
"transform": [{ "filter": "datum.source === 'model'" }],
21+
"mark": "line",
22+
"encoding": {
23+
"x": {
24+
"field": "x",
25+
"type": "quantitative",
26+
"scale": { "type": "log", "domainMin": 1 },
27+
"title": "X (log scale)"
28+
},
29+
"y": {
30+
"field": "y",
31+
"type": "quantitative",
32+
"scale": { "type": "log" },
33+
"title": "Survival Probability S(x)"
34+
},
35+
"color": { "value": "steelblue" }
36+
}
37+
},
38+
{
39+
"transform": [{ "filter": "datum.source === 'empirical'" }],
40+
"mark": { "type": "point", "shape": "cross", "size": 100, "tooltip": {"content": "data"} },
41+
"encoding": {
42+
"x": {
43+
"field": "x",
44+
"type": "quantitative",
45+
"scale": { "type": "log", "domainMin": 1 }
46+
},
47+
"y": {
48+
"field": "y",
49+
"type": "quantitative",
50+
"scale": { "type": "log" }
51+
},
52+
"color": { "value": "red" }
53+
}
54+
}
55+
]
56+
}

vecxt_re/src-jvm/all.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package vecxt_re
33
object all:
44
export vecxt_re.Scenario
55
export vecxt_re.Scenarr
6+
export vecxt_re.Tower
67
export vecxt_re.Plots.*
78
export vecxt_re.TrendAnalysis.*
89
export vecxt_re.TrendFitResult
@@ -16,4 +17,8 @@ object all:
1617
export vecxt_re.Poisson
1718
export vecxt_re.Empirical
1819
export vecxt_re.Pareto
20+
export vecxt_re.Mixed
21+
export vecxt_re.HillEstimator
22+
export vecxt_re.PickandsEstimator
23+
1924
end all

vecxt_re/src-jvm/plots.scala

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package vecxt_re
22

33
import io.circe.syntax.*
44
import io.github.quafadas.plots.SetupVega.{*, given}
5+
import vecxt_re.HillEstimator.HillPlotResult
6+
import vecxt_re.PickandsEstimator.PickandsPlotResult
57

68
object 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

Comments
 (0)