pie
ExamplesDocs

Topics

  • Canvas
  • Reactivity
  • Broadcasting
  • Autodiff
  • Animation
  • Sorting
  • Line fit
  • Convex hull
  • Live convex hull
  • Dijkstra's algorithm
  • k-means
  • Gaussian mixture
  • PCA
  • SVM
  • Differential equations
  • Python
unitgridOpacitycanvasColororiginXoriginY

Principal component analysis

The direction the points spread out most (teal), and the one at right angles to it (amber). Drag the points.

pie.clear()
pie.scalar(0, out='gridOpacity')
import numpy as np
coords = np.random.default_rng().multivariate_normal([0, 0], [[4, 2.4], [2.4, 2]], 14).clip(-4, 4).round(2)
pts = [pie.point(x, y, fill='crimson') for x, y in coords]

def directions(ps):
    """The spread along each direction, largest first, and the directions as columns."""
    spreads, dirs = np.linalg.eigh(np.cov(np.array(ps).T))
    return spreads[::-1], dirs[:, ::-1]

def average(*ps):
    return tuple(np.mean(ps, axis=0))

def axis_end(which, sign, *ps):
    """One end of direction `which` (0: first, 1: second), 2 standard deviations from the average point."""
    spreads, dirs = directions(ps)
    return tuple(np.mean(ps, axis=0) + sign * 2 * np.sqrt(spreads[which]) * dirs[:, which])

def radius(which, *ps):
    return 2 * np.sqrt(directions(ps)[0][which])

def angle(*ps):
    """The angle of the first direction, in degrees."""
    dirs = directions(ps)[1]
    return np.degrees(np.arctan2(dirs[1, 0], dirs[0, 0]))

def projection(p, *ps):
    """Where point p lands on the line of the first direction."""
    m = np.mean(ps, axis=0)
    d = directions(ps)[1][:, 0]
    return tuple(m + np.dot(np.array(p) - m, d) * d)

def describe(*ps):
    spreads = directions(ps)[0]
    return f'first direction: {spreads[0] / spreads.sum():.0%} of the spread'

end = pie.lift(axis_end, pie.Point)
size = pie.lift(radius, pie.Scalar)
onto = pie.lift(projection, pie.Point)

m = pie.lift(average, pie.Point)(*pts, hidden=True)
pie.ellipse(m, size(0, *pts), size(1, *pts), pie.lift(angle, pie.Scalar)(*pts), fill='rgba(13, 148, 136, 0.1)', stroke='none')
for p in pts:
    pie.line(p, onto(p, *pts, hidden=True), stroke='#a8a29e')
pie.line(end(0, -1, *pts, hidden=True), end(0, 1, *pts, hidden=True), stroke='teal')
pie.line(end(1, -1, *pts, hidden=True), end(1, 1, *pts, hidden=True), stroke='#d97706')
pie.text(pie.lift(describe, str)(*pts), 0, 4.8, 14, stroke='#1c1917')
pie.text('Drag a point to turn the directions', 0, -4.8, 14, stroke='#1c1917')

The teal line makes the gray lines as short as possible. Unlike Line fit, the gaps are at right angles to the line, not vertical.