Skip to content

Commit d38bf05

Browse files
committed
.
1 parent 42e1795 commit d38bf05

7 files changed

Lines changed: 495 additions & 3 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
3+
"description": "Empirical CDF (optionally weighted)",
4+
"width": "container",
5+
"height": "container",
6+
"data": { "values": [ { "x": 0.0, "cdf": 0.0 } ] },
7+
"mark": { "type": "line", "interpolate": "step-after" },
8+
"encoding": {
9+
"x": { "field": "x", "type": "quantitative", "title": "x" },
10+
"y": { "field": "cdf", "type": "quantitative", "title": "CDF" }
11+
}
12+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
3+
"description": "Empirical distribution PDF via (optionally weighted) histogram",
4+
"width": "container",
5+
"height": "container",
6+
"data": { "values": [ { "x": 0.0, "w": 1.0 } ] },
7+
"transform": [
8+
{ "bin": { "maxbins": 60 }, "field": "x", "as": ["x0", "x1"] },
9+
{ "aggregate": [ { "op": "sum", "field": "w", "as": "binWeight" } ], "groupby": ["x0", "x1"] },
10+
{ "joinaggregate": [ { "op": "sum", "field": "binWeight", "as": "W" } ] },
11+
{ "calculate": "datum.binWeight / (datum.W * (datum.x1 - datum.x0))", "as": "density" }
12+
],
13+
"mark": { "type": "bar", "opacity": 0.65 },
14+
"encoding": {
15+
"x": { "field": "x0", "type": "quantitative", "title": "x" },
16+
"x2": { "field": "x1" },
17+
"y": { "field": "density", "type": "quantitative", "title": "PDF" },
18+
"tooltip": [
19+
{ "field": "binWeight", "type": "quantitative", "title": "Bin weight" },
20+
{ "field": "density", "type": "quantitative", "title": "Density" }
21+
]
22+
}
23+
}

vecxt_re/resources/hist.vg.json

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"$schema": "https://vega.github.io/schema/vega/v5.json",
3+
"description": "An interactive histogram for visualizing a univariate distribution.",
4+
"width": 500,
5+
"height": 100,
6+
"padding": 5,
7+
8+
"signals": [
9+
{ "name": "binOffset", "value": 0,
10+
"bind": {"input": "range", "min": 1, "max": 10} },
11+
{ "name": "binStep", "value": 1,
12+
"bind": {"input": "range", "min": 0.001, "max":0, "step": 1} }
13+
],
14+
15+
"data": [
16+
{
17+
"name": "points",
18+
"values": [
19+
{"u": 0}, {"u": 1}, {"u": 2}, {"u": 3}, {"u": 4},
20+
{"u": 5}, {"u": 6}, {"u": 7}, {"u": 8}, {"u": 9}, {"u": 10}
21+
22+
]
23+
},
24+
{
25+
"name": "binned",
26+
"source": "points",
27+
"transform": [
28+
{
29+
"type": "bin", "field": "u",
30+
"extent": [0, 10],
31+
"anchor": {"signal": "binOffset"},
32+
"step": {"signal": "binStep"},
33+
"nice": false
34+
},
35+
{
36+
"type": "aggregate",
37+
"key": "bin0", "groupby": ["bin0", "bin1"],
38+
"fields": ["bin0"], "ops": ["count"], "as": ["count"]
39+
}
40+
]
41+
}
42+
],
43+
44+
"scales": [
45+
{
46+
"name": "xscale",
47+
"type": "linear",
48+
"range": "width",
49+
"domain": [0, 10]
50+
},
51+
{
52+
"name": "yscale",
53+
"type": "linear",
54+
"range": "height", "round": true,
55+
"domain": {"data": "binned", "field": "count"},
56+
"zero": true, "nice": true
57+
}
58+
],
59+
60+
"axes": [
61+
{"orient": "bottom", "scale": "xscale", "zindex": 1},
62+
{"orient": "left", "scale": "yscale", "tickCount": 5, "zindex": 1}
63+
],
64+
65+
"marks": [
66+
{
67+
"type": "rect",
68+
"from": {"data": "binned"},
69+
"encode": {
70+
"update": {
71+
"x": {"scale": "xscale", "field": "bin0"},
72+
"x2": {"scale": "xscale", "field": "bin1",
73+
"offset": {"signal": "binStep > 0.02 ? -0.5 : 0"}},
74+
"y": {"scale": "yscale", "field": "count"},
75+
"y2": {"scale": "yscale", "value": 0},
76+
"fill": {"value": "steelblue"}
77+
},
78+
"hover": { "fill": {"value": "firebrick"} }
79+
}
80+
},
81+
{
82+
"type": "rect",
83+
"from": {"data": "points"},
84+
"encode": {
85+
"enter": {
86+
"x": {"scale": "xscale", "field": "u"},
87+
"width": {"value": 1},
88+
"y": {"value": 25, "offset": {"signal": "height"}},
89+
"height": {"value": 5},
90+
"fill": {"value": "steelblue"},
91+
"fillOpacity": {"value": 0.4}
92+
}
93+
}
94+
}
95+
]
96+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
3+
"description": "Pareto PDF with log-binned histogram and analytic overlay",
4+
"width": "container",
5+
"height": "container",
6+
"resolve": {
7+
"scale": {
8+
"x": "shared"
9+
}
10+
},
11+
"layer": [
12+
{
13+
"data": { "values": [ { "x": 1.02 }, { "x": 1.05 } ] },
14+
"transform": [
15+
{ "calculate": "log(datum.x)", "as": "logx" },
16+
{ "bin": { "maxbins": 50 }, "field": "logx", "as": ["logx0", "logx1"] },
17+
{ "aggregate": [ { "op": "count", "as": "count" } ], "groupby": ["logx0", "logx1"] },
18+
{ "joinaggregate": [ { "op": "sum", "field": "count", "as": "N" } ] },
19+
{ "calculate": "exp(datum.logx0)", "as": "x0" },
20+
{ "calculate": "exp(datum.logx1)", "as": "x1" },
21+
{ "calculate": "datum.count / (datum.N * (datum.x1 - datum.x0))", "as": "density" }
22+
],
23+
"mark": { "type": "bar", "opacity": 0.6 },
24+
"encoding": {
25+
"x": { "field": "x0", "type": "quantitative", "scale": { "type": "log" }, "title": "x" },
26+
"x2": { "field": "x1" },
27+
"y": { "field": "density", "type": "quantitative", "title": "PDF" }
28+
}
29+
},
30+
{
31+
"data": { "sequence": { "start": 1, "stop": 20, "step": 0.05 } },
32+
"transform": [ { "calculate": "pow(datum.data, -2)", "as": "pdf" } ],
33+
"mark": { "type": "line", "strokeWidth": 2 },
34+
"encoding": {
35+
"x": { "field": "data", "type": "quantitative", "scale": { "type": "log" } },
36+
"y": { "field": "pdf", "type": "quantitative" }
37+
}
38+
}
39+
]
40+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package vecxt_re
2+
3+
import org.apache.commons.rng.simple.RandomSource
4+
import io.github.quafadas.plots.SetupVega.{*, given}
5+
import io.circe.syntax.*
6+
7+
/** Empirical distribution (JVM only).
8+
*
9+
* This is a nonparametric distribution built directly from observed samples.
10+
* It supports positive weights $w_i$.
11+
*
12+
* The distribution is represented as a discrete measure on the (possibly repeated) sample values:
13+
* $$\mathbb{P}(X = x) = \sum_{i: x_i = x} \frac{w_i}{\sum_k w_k}.$$
14+
*
15+
* Consequently, the CDF is a right-continuous step function
16+
* $$F(t) = \mathbb{P}(X \le t) = \sum_{x \le t} \mathbb{P}(X=x).$$
17+
*
18+
* Sampling is performed by inverse-transform sampling on the cumulative weights.
19+
*
20+
* Note: Since this is an empirical (atomic) distribution, a density/PDF in the usual continuous sense is not defined.
21+
* The `plot` method instead displays a (weighted) histogram density estimate.
22+
*/
23+
case class Empirical(values: IArray[Double], weights: IArray[Double])
24+
extends DiscreteDistr[Double]
25+
with HasMean[Double]
26+
with HasVariance[Double]
27+
with HasCdf
28+
with HasInverseCdf:
29+
30+
require(values.nonEmpty, "values must not be empty")
31+
require(values.forall(_.isFinite), "all values must be finite")
32+
require(weights.length == values.length, "weights must be the same length as values")
33+
require(weights.forall(w => w > 0 && w.isFinite), "weights must be positive and finite")
34+
35+
private val rng = RandomSource.XO_RO_SHI_RO_128_PP.create()
36+
37+
private val n = values.length
38+
39+
private val pairs: Array[(Double, Double)] =
40+
val out = new Array[(Double, Double)](n)
41+
var j = 0
42+
while j < n do
43+
out(j) = (values(j), weights(j))
44+
j += 1
45+
end while
46+
out
47+
48+
scala.util.Sorting.stableSort(pairs, (a: (Double, Double), b: (Double, Double)) => a._1 < b._1)
49+
50+
// Compress duplicates so we have unique support points.
51+
private val xsBuf = scala.collection.mutable.ArrayBuffer.empty[Double]
52+
private val wBuf = scala.collection.mutable.ArrayBuffer.empty[Double]
53+
54+
private var totalWeight = 0.0
55+
private var i = 0
56+
while i < pairs.length do
57+
val x = pairs(i)._1
58+
var wSum = 0.0
59+
while i < pairs.length && pairs(i)._1 == x do
60+
val w = pairs(i)._2
61+
wSum += w
62+
totalWeight += w
63+
i += 1
64+
end while
65+
xsBuf += x
66+
wBuf += wSum
67+
end while
68+
69+
private val xs: Array[Double] = xsBuf.toArray
70+
private val probs: Array[Double] = wBuf.toArray.map(_ / totalWeight)
71+
72+
private val cdfVals: Array[Double] =
73+
val out = new Array[Double](probs.length)
74+
var acc = 0.0
75+
var j = 0
76+
while j < probs.length do
77+
acc += probs(j)
78+
out(j) = acc
79+
j += 1
80+
end while
81+
out
82+
83+
private val meanVal: Double =
84+
var s = 0.0
85+
var j = 0
86+
while j < xs.length do
87+
s += xs(j) * probs(j)
88+
j += 1
89+
end while
90+
s
91+
92+
private val varVal: Double =
93+
var s2 = 0.0
94+
var j = 0
95+
while j < xs.length do
96+
val d = xs(j) - meanVal
97+
s2 += probs(j) * d * d
98+
j += 1
99+
end while
100+
s2
101+
102+
def mean: Double = meanVal
103+
104+
def variance: Double = varVal
105+
106+
/** Probability mass at exactly `x` (sums weights for duplicates). */
107+
def probabilityOf(x: Double): Double =
108+
val idx = java.util.Arrays.binarySearch(xs, x)
109+
if idx >= 0 then probs(idx) else 0.0
110+
111+
/** Draw a sample using inverse CDF sampling over the atomic masses. */
112+
def draw: Double =
113+
val u = rng.nextDouble()
114+
inverseCdf(u)
115+
116+
/** CDF $F(t)=P(X\le t)$ (right-continuous). */
117+
def cdf(x: Double): Double =
118+
if x < xs(0) then 0.0
119+
else if x >= xs(xs.length - 1) then 1.0
120+
else
121+
// Find the last index with xs(idx) <= x
122+
val ip = java.util.Arrays.binarySearch(xs, x)
123+
val idx = if ip >= 0 then ip else -ip - 2
124+
cdfVals(idx)
125+
126+
/** Probability that $x < X \le y$. */
127+
def probability(x: Double, y: Double): Double =
128+
if y <= x then 0.0
129+
else cdf(y) - cdf(x)
130+
131+
/** Inverse CDF (quantile function): returns the smallest `x` with `F(x) >= p`. */
132+
def inverseCdf(p: Double): Double =
133+
require(p >= 0.0 && p <= 1.0, "p must be in [0,1]")
134+
if p <= 0.0 then xs(0)
135+
else
136+
val ip = java.util.Arrays.binarySearch(cdfVals, p)
137+
val idx = if ip >= 0 then ip else -ip - 1
138+
xs(math.min(idx, xs.length - 1))
139+
140+
/** Plot a (weighted) histogram density estimate. */
141+
def plot(using viz.LowPriorityPlotTarget) =
142+
val plot = VegaPlot.fromResource("empiricalPdf.vl.json")
143+
val data = (0 until n).map { i => (x = values(i), w = weights(i)) }
144+
plot.plot(
145+
_.data.values := data.asJson,
146+
_ += (title = s"Empirical Distribution (n=$n)").asJson
147+
)
148+
149+
/** Plot the empirical CDF (step function). */
150+
def plotCdf(using viz.LowPriorityPlotTarget) =
151+
val plot = VegaPlot.fromResource("empiricalCdf.vl.json")
152+
153+
// Add an initial point at (min, 0) so the step is visible from the left.
154+
val points =
155+
val pts = scala.collection.mutable.ArrayBuffer.empty[(x: Double, cdf: Double)]
156+
pts += ((x = xs(0), cdf = 0.0))
157+
var j = 0
158+
while j < xs.length do
159+
pts += ((x = xs(j), cdf = cdfVals(j)))
160+
j += 1
161+
end while
162+
pts.toVector
163+
164+
plot.plot(
165+
_.data.values := points.asJson,
166+
_ += (title = s"Empirical CDF (n=$n)").asJson
167+
)
168+
169+
end Empirical
170+
171+
object Empirical:
172+
/** Construct an unweighted empirical distribution (all weights equal to $1$).
173+
*
174+
* Note: We intentionally avoid `apply` overloads here because `IArray[Double]` erases to `Array[Double]`
175+
* on the JVM, which can create signature collisions with the case class companion methods.
176+
*/
177+
inline def equalWeights(values: Array[Double]): Empirical =
178+
Empirical(
179+
IArray.from(values),
180+
IArray.from(Array.fill(values.length)(1.0))
181+
)
182+
183+
/** Construct a weighted empirical distribution from arrays. */
184+
inline def weighted(values: Array[Double], weights: Array[Double]): Empirical =
185+
Empirical(IArray.from(values), IArray.from(weights))
186+

0 commit comments

Comments
 (0)