jekyll-theme-pca-cloud 0.1.0

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.
data/assets/js/pca.js ADDED
@@ -0,0 +1,91 @@
1
+ // All analysis stays in the browser. No dependencies or network requests.
2
+ export function parseCSV(text) {
3
+ const rows = []; let row = [], cell = '', quoted = false;
4
+ text = text.replace(/^\uFEFF/, '');
5
+ for (let i = 0; i < text.length; i++) {
6
+ const c = text[i];
7
+ if (c === '"') {
8
+ if (quoted && text[i + 1] === '"') { cell += '"'; i++; }
9
+ else if (quoted || cell === '') quoted = !quoted;
10
+ else cell += c;
11
+ } else if (!quoted && (c === ',' || c === '\n' || c === '\r')) {
12
+ row.push(cell.trim()); cell = '';
13
+ if (c !== ',') { if (row.some(v => v !== '')) rows.push(row); row = []; if (c === '\r' && text[i + 1] === '\n') i++; }
14
+ } else cell += c;
15
+ }
16
+ if (quoted) throw new Error('A quoted field is unfinished. Please check your CSV.');
17
+ row.push(cell.trim()); if (row.some(v => v !== '')) rows.push(row);
18
+ if (rows.length < 3) throw new Error('Include a header and at least two data rows.');
19
+ const headers = rows.shift();
20
+ if (headers.length > 100) throw new Error('Please use a CSV with 100 columns or fewer.');
21
+ const numeric = v => v !== undefined && v !== '' && Number.isFinite(Number(v));
22
+ const columns = headers.map((name, index) => ({ name: name || `Column ${index + 1}`, index }))
23
+ .filter(col => rows.some(r => numeric(r[col.index])) && rows.every(r => r[col.index] === undefined || r[col.index] === '' || numeric(r[col.index])));
24
+ if (columns.length < 2) throw new Error('PCA needs at least two numeric columns. Remove text values from measurement columns.');
25
+ const valid = rows.filter(r => r.length === headers.length && columns.every(c => numeric(r[c.index])));
26
+ if (valid.length < 2) throw new Error('At least two complete numeric rows are needed.');
27
+ const count = Math.min(valid.length, 2500);
28
+ const data = Array.from({ length: count }, (_, i) => columns.map(c => Number(valid[Math.floor(i * valid.length / count)][c.index])));
29
+ return { data, columns: columns.map(c => c.name), skipped: rows.length - valid.length, sampled: valid.length > count, sourceRows: valid.length };
30
+ }
31
+
32
+ export function prepareData(data, standardize = true) {
33
+ const n = data.length, d = data[0]?.length;
34
+ if (n < 2 || !d || data.some(row => row.length !== d || row.some(v => !Number.isFinite(v)))) throw new Error('PCA requires a finite rectangular matrix with at least two rows.');
35
+ // Scaling all values first avoids overflow for very large CSV measurements.
36
+ const magnitudes = Array.from({ length: d }, (_, j) => Math.max(...data.map(r => Math.abs(r[j]))) || 1);
37
+ const commonScale = Math.max(...magnitudes);
38
+ const scaled = data.map(r => r.map((v, j) => v / (standardize ? magnitudes[j] : commonScale)));
39
+ const mean = Array.from({ length: d }, (_, j) => scaled.reduce((s, r) => s + r[j], 0) / n);
40
+ const scales = mean.map((m, j) => standardize ? Math.sqrt(scaled.reduce((s, r) => s + (r[j] - m) ** 2, 0) / (n - 1)) || 1 : 1);
41
+ const centered = scaled.map(r => r.map((v, j) => (v - mean[j]) / scales[j]));
42
+ return centered;
43
+ }
44
+
45
+ export function pca(data, standardize = true) {
46
+ const centered = prepareData(data, standardize);
47
+ const n = data.length, d = data[0].length;
48
+ const a = Array.from({ length: d }, () => Array(d).fill(0));
49
+ for (let j = 0; j < d; j++) for (let k = j; k < d; k++) a[j][k] = a[k][j] = centered.reduce((s, r) => s + r[j] * r[k], 0) / (n - 1);
50
+ const total = a.reduce((s, r, j) => s + r[j], 0);
51
+ if (total < 1e-25) throw new Error('These columns have no variation. Try data with changing measurements.');
52
+ const vectors = Array.from({ length: d }, (_, i) => Array.from({ length: d }, (_, j) => +(i === j)));
53
+ // Cyclic Jacobi eigendecomposition of the symmetric covariance matrix.
54
+ for (let sweep = 0; sweep < 60; sweep++) {
55
+ let largest = 0;
56
+ for (let p = 0; p < d; p++) for (let q = p + 1; q < d; q++) {
57
+ largest = Math.max(largest, Math.abs(a[p][q]));
58
+ if (Math.abs(a[p][q]) < total * 1e-13) continue;
59
+ const angle = .5 * Math.atan2(2 * a[p][q], a[q][q] - a[p][p]);
60
+ const c = Math.cos(angle), s = Math.sin(angle), pp = a[p][p], qq = a[q][q], pq = a[p][q];
61
+ a[p][p] = c*c*pp - 2*s*c*pq + s*s*qq;
62
+ a[q][q] = s*s*pp + 2*s*c*pq + c*c*qq; a[p][q] = a[q][p] = 0;
63
+ for (let k = 0; k < d; k++) {
64
+ if (k !== p && k !== q) { const kp = a[k][p], kq = a[k][q]; a[k][p] = a[p][k] = c*kp-s*kq; a[k][q] = a[q][k] = s*kp+c*kq; }
65
+ const vp = vectors[k][p], vq = vectors[k][q]; vectors[k][p] = c*vp-s*vq; vectors[k][q] = s*vp+c*vq;
66
+ }
67
+ }
68
+ if (largest < total * 1e-11) break;
69
+ }
70
+ const fullOrder = Array.from({ length: d }, (_, i) => i).sort((i, j) => a[j][j] - a[i][i]);
71
+ const eigenvalues = fullOrder.map(i => Math.max(0, a[i][i]));
72
+ const spectrum = eigenvalues.map(value => value / total);
73
+ const order = fullOrder.slice(0, 3);
74
+ const components = order.map(i => vectors.map(r => r[i]));
75
+ const variance = order.map(i => Math.max(0, a[i][i]) / total);
76
+ const points = centered.map(row => components.map(v => row.reduce((s, x, j) => s + x*v[j], 0)));
77
+ while (variance.length < 3) { variance.push(0); points.forEach(p => p.push(0)); }
78
+ const reconstructionError = Math.max(0, 1 - variance.reduce((s, v) => s + v, 0));
79
+ return { points, variance, components, reconstructionError, spectrum, eigenvalues };
80
+ }
81
+
82
+ export function sampleData() {
83
+ let seed = 42;
84
+ const random = () => { seed = (1664525 * seed + 1013904223) >>> 0; return (seed + 1) / 4294967297; };
85
+ const normal = () => Math.sqrt(-2 * Math.log(random())) * Math.cos(2 * Math.PI * random());
86
+ const data = Array.from({ length: 1600 }, () => {
87
+ const cluster = random(), x = normal() * .7 + (cluster > .6 ? 1.7 : -1), y = normal() * .6 + (cluster > .6 ? .5 : 0), z = normal() * .4;
88
+ return [18+4*x+y, 65+8*x-4*y+z, 1013+3*x+4*y, 12+2*x+3*z, 8+x-y+2*z, 30+4*x+2*y-z].map(v => v + normal() * .17);
89
+ });
90
+ return { data, columns: ['Temperature', 'Humidity', 'Pressure', 'Wind', 'Visibility', 'Dew point'], skipped: 0, sampled: false };
91
+ }
@@ -0,0 +1,7 @@
1
+ import { tsne } from './tsne.js';
2
+ self.onmessage=({data})=>{
3
+ try {
4
+ const result=tsne(data.rows,data.options,progress=>self.postMessage({type:'progress',...progress}));
5
+ self.postMessage({type:'result',result});
6
+ }catch(error){self.postMessage({type:'error',message:error.message});}
7
+ };
data/assets/js/tsne.js ADDED
@@ -0,0 +1,78 @@
1
+ import { prepareData } from './pca.js';
2
+
3
+ // Exact t-SNE: symmetric Gaussian affinities, Student-t kernel (one degree
4
+ // of freedom), early exaggeration, momentum and adaptive per-coordinate gains.
5
+ // Reference: van der Maaten & Hinton, JMLR 9 (2008), Algorithm 1.
6
+ export function affinities(data, perplexity) {
7
+ const n = data.length;
8
+ if (!(perplexity >= 1 && perplexity < n)) throw new Error('Perplexity must be at least 1 and smaller than the row count.');
9
+ const conditional = new Float64Array(n*n), distances = new Float64Array(n);
10
+ for (let i=0;i<n;i++) {
11
+ let minimum=Infinity;
12
+ for(let j=0;j<n;j++) {
13
+ distances[j]=data[i].reduce((s,x,k)=>s+(x-data[j][k])**2,0);
14
+ if(i!==j)minimum=Math.min(minimum,distances[j]);
15
+ }
16
+ let beta=1, low=0, high=Infinity;
17
+ for(let search=0;search<60;search++) {
18
+ let sum=0, weighted=0;
19
+ for(let j=0;j<n;j++) {
20
+ const distance=distances[j]-minimum;
21
+ const probability=i===j?0:Math.exp(-distance*beta);
22
+ conditional[i*n+j]=probability;sum+=probability;weighted+=probability*distance;
23
+ }
24
+ const entropy=Math.log(sum)+beta*weighted/sum;
25
+ for(let j=0;j<n;j++)conditional[i*n+j]/=sum;
26
+ const difference=entropy-Math.log(perplexity);
27
+ if(Math.abs(difference)<1e-7)break;
28
+ if(difference>0){low=beta;beta=Number.isFinite(high)?(beta+high)/2:beta*2;}
29
+ else {high=beta;beta=(beta+low)/2;}
30
+ }
31
+ }
32
+ const p=new Float64Array(n*n);
33
+ for(let i=0;i<n;i++)for(let j=i+1;j<n;j++)p[i*n+j]=p[j*n+i]=(conditional[i*n+j]+conditional[j*n+i])/(2*n);
34
+ return p;
35
+ }
36
+
37
+ export function objective(points, p, exaggeration=1) {
38
+ const n=points.length, weights=new Float64Array(n*n), gradient=points.map(()=>[0,0,0]);
39
+ let sum=0, kl=0;
40
+ for(let i=0;i<n;i++)for(let j=i+1;j<n;j++) {
41
+ const w=1/(1+points[i].reduce((s,x,k)=>s+(x-points[j][k])**2,0));
42
+ weights[i*n+j]=weights[j*n+i]=w;sum+=2*w;
43
+ }
44
+ for(let i=0;i<n;i++)for(let j=i+1;j<n;j++) {
45
+ const probability=p[i*n+j], q=weights[i*n+j]/sum;
46
+ if(probability>0)kl+=2*probability*Math.log(probability/Math.max(q,1e-300));
47
+ const multiplier=4*(exaggeration*probability-q)*weights[i*n+j];
48
+ for(let k=0;k<3;k++){const g=multiplier*(points[i][k]-points[j][k]);gradient[i][k]+=g;gradient[j][k]-=g;}
49
+ }
50
+ return {gradient,kl};
51
+ }
52
+
53
+ export function tsne(data, {standardize=true,perplexity=30,iterations=500,seed=42}={}, onProgress=()=>{}) {
54
+ if(data.length>500)throw new Error('t-SNE supports at most 500 rows per run.');
55
+ if(!Number.isInteger(iterations)||iterations<1||iterations>2000)throw new Error('Invalid iteration count.');
56
+ const centered=prepareData(data,standardize),n=centered.length;
57
+ if(!centered.some(row=>row.some(x=>Math.abs(x)>1e-12)))throw new Error('These columns have no variation.');
58
+ // Common scaling changes no affinities and improves bandwidth conditioning.
59
+ const rms=Math.sqrt(centered.reduce((s,r)=>s+r.reduce((a,x)=>a+x*x,0),0)/n)||1;
60
+ const p=affinities(centered.map(r=>r.map(x=>x/rms)),perplexity);
61
+ const random=()=>{seed=(1664525*seed+1013904223)>>>0;return (seed+1)/4294967297;};
62
+ const points=Array.from({length:n},()=>Array.from({length:3},()=>1e-4*Math.sqrt(-2*Math.log(random()))*Math.cos(2*Math.PI*random())));
63
+ const velocity=points.map(()=>[0,0,0]),gains=points.map(()=>[1,1,1]);
64
+ for(let step=0;step<iterations;step++) {
65
+ const {gradient}=objective(points,p,step<100?4:1);
66
+ const mean=[0,0,0];
67
+ for(let i=0;i<n;i++)for(let k=0;k<3;k++) {
68
+ gains[i][k]=Math.sign(gradient[i][k])!==Math.sign(velocity[i][k])?gains[i][k]+.2:Math.max(.01,gains[i][k]*.8);
69
+ velocity[i][k]=(step<100?.5:.8)*velocity[i][k]-100*gains[i][k]*gradient[i][k];
70
+ points[i][k]+=velocity[i][k];mean[k]+=points[i][k]/n;
71
+ }
72
+ for(const point of points)for(let k=0;k<3;k++)point[k]-=mean[k];
73
+ if((step+1)%25===0)onProgress({iteration:step+1,iterations});
74
+ }
75
+ const {kl}=objective(points,p);
76
+ if(!Number.isFinite(kl)||points.some(p=>p.some(x=>!Number.isFinite(x))))throw new Error('t-SNE could not find a finite embedding. Try a different perplexity.');
77
+ return {points,kl,iterations,perplexity};
78
+ }
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-theme-pca-cloud
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Archisa Bhattacharya
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jekyll
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '4.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '4.4'
26
+ executables: []
27
+ extensions: []
28
+ extra_rdoc_files: []
29
+ files:
30
+ - LICENSE
31
+ - README.md
32
+ - _includes/footer.html
33
+ - _includes/head.html
34
+ - _includes/header.html
35
+ - _includes/learning.html
36
+ - _includes/studio.html
37
+ - _layouts/default.html
38
+ - _layouts/home.html
39
+ - _layouts/page.html
40
+ - _layouts/post.html
41
+ - assets/cloud.png
42
+ - assets/css/style.css
43
+ - assets/js/app.js
44
+ - assets/js/kaggle.js
45
+ - assets/js/learn.js
46
+ - assets/js/pca.js
47
+ - assets/js/projection-worker.js
48
+ - assets/js/tsne.js
49
+ homepage: https://github.com/archipelagoing/PCACloud
50
+ licenses:
51
+ - Apache-2.0
52
+ metadata: {}
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.1'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 4.0.22
68
+ specification_version: 4
69
+ summary: PCA Cloud's sky and glass interface as a reusable Jekyll theme.
70
+ test_files: []