nipals-pca
Version:
A NIPALS implementation of PCA that handles missing values
129 lines (87 loc) • 3.52 kB
JavaScript
const { Matrix } = require('ml-matrix');
var fs = require('fs');
var Papa = require('papaparse');
class Dataset {
constructor(matrix, transpose = false) {
//set zero length string values to NaN to prevent missing values being parsed as zeroes
matrix = matrix.map(r => r.map(v => v.length ? v : NaN))
this.matrix = new Matrix(matrix);
if (transpose) {
this.matrix = this.matrix.transpose();
}
let n = this.matrix.rows;
let k = this.matrix.columns;
this.xmask = new Matrix(n, k)
this.means = [];
this.stdevs = [];
for (let i = 0; i < n; i++) {
this.xmask.setRow(i, this.matrix.getRow(i).map(v => isNaN(v) ? 0 : 1));
}
this.missing = this.xmask.size != this.xmask.sum();
let mvCutoffPct = 0.5;
let mvCutoff = mvCutoffPct * n;
let columns = this.xmask.sum("column").map((numVals, i) => numVals > mvCutoff ? i : -1).filter(idx => idx >= 0);
let rowMvCutoffPct = 0.2;
let rowMvCutoff = rowMvCutoffPct * k;
let rows = this.xmask.sum("row").map((numVals, i) => numVals > rowMvCutoff ? i : -1).filter(idx => idx >= 0);
this.matrix = this.matrix.selection(rows, columns);
this.xmask = this.xmask.selection(rows, columns);
n = this.matrix.rows;
k = this.matrix.columns;
this.setZeroes();
for (let i = 0; i < k; i++) {
let dataColumn = this.matrix.getColumn(i);
let maskColumn = this.xmask.getColumn(i);
let idxs = maskColumn.map((v, i) => {
if (v) {
return i;
}
}).filter(idx => idx != undefined);
let values = idxs.map(i => dataColumn[i]);
let nVals = values.length;
let mean = values.reduce((sum, value) => sum + value, 0) / nVals;
this.means.push(mean);
const std = Math.sqrt(values.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / (nVals - 1));
this.stdevs.push(std);
}
this.matrix.subRowVector(this.means);
this.matrix.divRowVector(this.stdevs);
this.setZeroes()
}
setZeroes() {
let n = this.xmask.rows;
for (let row = 0; row < n; row++) {
let maskRow = this.xmask.getRow(row);
let nanIndices = maskRow.map((v, i) => {
if (!v) {
return i;
}
}).filter(idx => idx != undefined);
nanIndices.map(nanIdx => this.matrix.set(row, nanIdx, 0));
}
}
static async parse(filePath, numObsIds, numVarIds, transpose = false) {
let filePromise = this.parseExampleInput(filePath);
let jsonMatrix = await filePromise;
jsonMatrix.splice(0, numVarIds);
jsonMatrix.map(r => r.splice(0, numObsIds));
return new Dataset(jsonMatrix, transpose);
}
static parseExampleInput(filePath) {
let file = fs.readFileSync(filePath, 'utf8')
return new Promise((resolve, reject) => {
Papa.parse(file, {
header: false,
skipEmptyLines: true,
complete(results, file) {
let data = results.data;
resolve(data)
},
error(err, file) {
reject(err)
}
})
})
}
}
module.exports.Dataset = Dataset;