├── Dockerfile ├── Makefile ├── README.md ├── docs ├── gopher.png ├── index.html ├── main.wasm └── wasm_exec.js ├── img ├── basics.png ├── incomes-predict-murders.png ├── inhabitants-predict-murders.png ├── line-plot.png ├── remove1.png └── unemployment-predict-murders.png ├── main.go ├── runner.go └── utils.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | # docker build -t vanessa/regression-wasm . 3 | RUN apt-get update && apt-get install -y git nginx build-essential python autoconf automake libtool bc 4 | WORKDIR /opt 5 | RUN git clone https://github.com/emscripten-core/emsdk.git && \ 6 | cd emsdk && \ 7 | git pull && \ 8 | ./emsdk install latest && \ 9 | ./emsdk activate latest 10 | 11 | ENV PATH /opt/emsdk:/opt/emsdk/fastcomp/emscripten:/opt/emsdk/node/12.9.1_64bit/bin:$PATH 12 | 13 | WORKDIR /var/www/html 14 | COPY . /var/www/html 15 | RUN make 16 | EXPOSE 80 17 | CMD ["nginx", "-g", "daemon off;"] 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | go get github.com/sajari/regression 4 | GOOS=js GOARCH=wasm go build -o docs/main.wasm 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Regression Wasm 2 | 3 | This repository serves a simple [web assembly](https://webassembly.org/) (wasm) application 4 | to perform a regression, using data from a table in the browser, which can be loaded as a delimited file 5 | by the user. We use a simple [regression library](https://github.com/sajari/regression) to do 6 | the work. See the demo [here](https://vsoch.github.io/regression-wasm/) or continue reading. 7 | 8 | ## Summary 9 | 10 | - Run a multiple or single regression using Web Assembly 11 | - Two variables (one predictor, one regression) will generate a line plot showing X vs. Y and predictions 12 | - More than two variables (one predictor, multiple regressors) performs multiple regression to generate a residual histogram 13 | - Upload your own data file, change the delimiter, the file name to be saved, or the predictor column 14 | 15 | ## Overview 16 | 17 | When you load the page, you are presented with a loaded data frame. The data is a bit dark, 18 | but it's a nice dataset to show how this works. The first column is the number of murders (per 19 | million habitants) for some city, and each of the remaining columns are variables that might 20 | be used to predict it (inhabitants, percent with incomes below $5000, and percent unemployed). 21 | This is what you see: 22 | 23 | ![img/basics.png](img/basics.png) 24 | 25 | ### Formula 26 | 27 | The formula for our regression model is shown below the plot, in human friendly terms. 28 | 29 | ``` 30 | Predicted = -36.7649 + Inhabitants*0.0000 + Percent with incomes below $5000*1.1922 + Percent unemployed*4.7198 31 | ``` 32 | 33 | ### Residual Plot 34 | 35 | Given that we have more than one regressor variable, we need to run a multiple regression, 36 | and so the plot in the upper right is a histogram of the residuals. 37 | 38 | > the residuals are the difference between the actual values (number of murders per million habitants) and the values predicted by our model. 39 | 40 | ### Filtering 41 | 42 | If you remove any single value from a row, it invalidates it, and it won't be included 43 | in the plot. If you remove a column heading, it's akin to removing the entire column. 44 | 45 | ### Line Plot 46 | 47 | But what if we want to plot the relationship between one of the variables X, and our Y? 48 | This is where the tool gets interesting! By removing a column header, we essentially 49 | remove the column from the dataset. Let's first try removing just one, Inhabitants: 50 | 51 | ![img/remove1.png](img/remove1.png) 52 | 53 | 54 | We still see a residual plot because it would require more than two dimensions to plot. 55 | Let's remove another one, the percent unemployed: 56 | 57 | ![img/line-plot.png](img/line-plot.png) 58 | 59 | Now we see a line plot, along with the plotting of the predictions! By simply removing 60 | each column one at a time (and leaving only one Y, and one X) we are actually running 61 | a single regression, and we can do this for each variable: 62 | 63 | #### Inhabitants to predict murders 64 | 65 | ![img/inhabitants-predict-murders.png](img/inhabitants-predict-murders.png) 66 | 67 |
68 | 69 | #### Unemployment to predict murders 70 | 71 | ![img/unemployment-predict-murders.png](img/unemployment-predict-murders.png) 72 | 73 |
74 | 75 | #### Low Income Percentage to predict murders 76 | 77 | ![img/incomes-predict-murders.png](img/incomes-predict-murders.png) 78 | 79 | 80 | As we can see, the number of inhabitants (on its own) is fairly useless. The variables 81 | that are strongest here are unemployment and income. 82 | 83 | ## Download Data 84 | 85 | This of course is a very superficial overview, you would want to download the full model data to get more detail: 86 | The "Download Results" will appear after you generate any kind of plot, and it downloads 87 | a text file with the model output. Here is an example: 88 | 89 | ``` 90 | Dinosaur Regression Wasm 91 | Predicted = -36.7649 + Inhabitants*0.0000 + Percent with incomes below $5000*1.1922 + Percent unemployed*4.7198 92 | Murders per annum per one million inhabitants| Inhabitants| Percent with incomes below $5000| Percent unemployed 93 | 11.20| 587000.00| 16.50| 6.20 94 | 13.40| 643000.00| 20.50| 6.40 95 | 40.70| 635000.00| 26.30| 9.30 96 | 5.30| 692000.00| 16.50| 5.30 97 | 24.80| 1248000.00| 19.20| 7.30 98 | 12.70| 643000.00| 16.50| 5.90 99 | 20.90| 1964000.00| 20.20| 6.40 100 | 35.70| 1531000.00| 21.30| 7.60 101 | 8.70| 713000.00| 17.20| 4.90 102 | 9.60| 749000.00| 14.30| 6.40 103 | 14.50| 7895000.00| 18.10| 6.00 104 | 26.90| 762000.00| 23.10| 7.40 105 | 15.70| 2793000.00| 19.10| 5.80 106 | 36.20| 741000.00| 24.70| 8.60 107 | 18.10| 625000.00| 18.60| 6.50 108 | 28.90| 854000.00| 24.90| 8.30 109 | 14.90| 716000.00| 17.90| 6.70 110 | 25.80| 921000.00| 22.40| 8.60 111 | 21.70| 595000.00| 20.20| 8.40 112 | 25.70| 3353000.00| 16.90| 6.70 113 | 114 | N = 20 115 | Variance observed = 92.76010000000001 116 | Variance Predicted = 75.90724706481737 117 | R2 = 0.8183178658153383 118 | ``` 119 | 120 | ## About 121 | 122 | ### Why? 123 | 124 | Web assembly can allow us to interact with compiled code directly in the browser, 125 | doing away with any need for a server. While I don't do a large amount of data analysis 126 | for my role proper, I realize that many researchers do, and so with this in mind, 127 | I wanted to create a starting point for developers to interact with data in the browser. 128 | The minimum conditions for success meant: 129 | 130 | 1. being able to load a delimited file into the browser 131 | 2. having the file render as a table 132 | 3. having the data be processed by a compiled wasm 133 | 4. updating a plot based on output from 3. 134 | 135 | Thus, the application performs a simple regression based on loading data in the table, 136 | and then plotting the result. To make it fun, I added a cute gopher logo and used an xkcd 137 | plotting library for the result. 138 | 139 | ### Customization 140 | 141 | The basics are here for a developer to create (some GoLang based) functions to 142 | perform data analysis on an input file, and render back to the screen as a plot. 143 | If you need any help, or want to request a custom tool, please don't hesitate to 144 | [open up an issue](https://www.github.com/vsoch/regression-wasm/issues). 145 | 146 | ## Development 147 | 148 | ### Local 149 | 150 | If you are comfortable with GoLang, and have installed [emscripten](https://emscripten.org), 151 | you can clone the repository into your $GOPATH under the github folder: 152 | 153 | ```bash 154 | $ mkdir -p $GOPATH/src.github.com/vsoch 155 | $ cd $GOPATH/src.github.com/vsoch 156 | $ git clone https://www.github.com/vsoch/regression-wasm 157 | ``` 158 | 159 | And then build the wasm. 160 | 161 | ```bash 162 | $ cd regression-wasm 163 | $ make 164 | ``` 165 | 166 | Add your own Go version specific `wasm_exec.js` file : 167 | 168 | ```bash 169 | $ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./docs 170 | ``` 171 | 172 | And cd into the "docs" folder and start a server to see the result. 173 | 174 | ```bash 175 | $ cd docs 176 | $ python -m http.server 9999 177 | ``` 178 | 179 | Open the browser to http://localhost:9999 180 | 181 | 182 | ## Docker 183 | 184 | If you don't want to install dependencies, just clone the repository, and 185 | build the Docker image: 186 | 187 | ```bash 188 | $ docker build -t vanessa/regression-wasm . 189 | ``` 190 | 191 | It will install [emscripten](https://emscripten.org/docs/getting_started/FAQ.html), 192 | add the source code to the repository, and compile to wasm. You can then 193 | run the container and expose port 80 to see the compiled interface: 194 | 195 | ```bash 196 | $ docker run -it --rm -p 80:80 vanessa/regression-wasm 197 | ``` 198 | 199 | Then you can proceed to use the interface. 200 | -------------------------------------------------------------------------------- /docs/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsoch/regression-wasm/91f1adb3dfe185c92f6aac59916829295a8a5aee/docs/gopher.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Regression Wasm 9 | 10 | 11 | 12 | 19 | 20 | 21 | 28 |
29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |

Data Table

37 | 38 |
39 | 40 | 41 |
42 | 43 |
44 |
45 |
46 | 50 | custom delimiter (default ,) 51 |
52 |
53 |
54 |
55 | 59 | download file name 60 |
61 |
62 |
63 | 64 |
65 | 66 |
67 |
68 |
69 | 70 |
71 |
72 | 74 | 75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 |
83 | 84 | 86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 383 | 384 | 385 | -------------------------------------------------------------------------------- /docs/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsoch/regression-wasm/91f1adb3dfe185c92f6aac59916829295a8a5aee/docs/main.wasm -------------------------------------------------------------------------------- /docs/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | 15 | if (typeof global !== "undefined") { 16 | // global already exists 17 | } else if (typeof window !== "undefined") { 18 | window.global = window; 19 | } else if (typeof self !== "undefined") { 20 | self.global = self; 21 | } else { 22 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 23 | } 24 | 25 | if (!global.require && typeof require !== "undefined") { 26 | global.require = require; 27 | } 28 | 29 | if (!global.fs && global.require) { 30 | global.fs = require("fs"); 31 | } 32 | 33 | if (!global.fs) { 34 | let outputBuf = ""; 35 | global.fs = { 36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 37 | writeSync(fd, buf) { 38 | outputBuf += decoder.decode(buf); 39 | const nl = outputBuf.lastIndexOf("\n"); 40 | if (nl != -1) { 41 | console.log(outputBuf.substr(0, nl)); 42 | outputBuf = outputBuf.substr(nl + 1); 43 | } 44 | return buf.length; 45 | }, 46 | write(fd, buf, offset, length, position, callback) { 47 | if (offset !== 0 || length !== buf.length || position !== null) { 48 | throw new Error("not implemented"); 49 | } 50 | const n = this.writeSync(fd, buf); 51 | callback(null, n); 52 | }, 53 | open(path, flags, mode, callback) { 54 | const err = new Error("not implemented"); 55 | err.code = "ENOSYS"; 56 | callback(err); 57 | }, 58 | read(fd, buffer, offset, length, position, callback) { 59 | const err = new Error("not implemented"); 60 | err.code = "ENOSYS"; 61 | callback(err); 62 | }, 63 | fsync(fd, callback) { 64 | callback(null); 65 | }, 66 | }; 67 | } 68 | 69 | if (!global.crypto) { 70 | const nodeCrypto = require("crypto"); 71 | global.crypto = { 72 | getRandomValues(b) { 73 | nodeCrypto.randomFillSync(b); 74 | }, 75 | }; 76 | } 77 | 78 | if (!global.performance) { 79 | global.performance = { 80 | now() { 81 | const [sec, nsec] = process.hrtime(); 82 | return sec * 1000 + nsec / 1000000; 83 | }, 84 | }; 85 | } 86 | 87 | if (!global.TextEncoder) { 88 | global.TextEncoder = require("util").TextEncoder; 89 | } 90 | 91 | if (!global.TextDecoder) { 92 | global.TextDecoder = require("util").TextDecoder; 93 | } 94 | 95 | // End of polyfills for common API. 96 | 97 | const encoder = new TextEncoder("utf-8"); 98 | const decoder = new TextDecoder("utf-8"); 99 | 100 | global.Go = class { 101 | constructor() { 102 | this.argv = ["js"]; 103 | this.env = {}; 104 | this.exit = (code) => { 105 | if (code !== 0) { 106 | console.warn("exit code:", code); 107 | } 108 | }; 109 | this._exitPromise = new Promise((resolve) => { 110 | this._resolveExitPromise = resolve; 111 | }); 112 | this._pendingEvent = null; 113 | this._scheduledTimeouts = new Map(); 114 | this._nextCallbackTimeoutID = 1; 115 | 116 | const mem = () => { 117 | // The buffer may change when requesting more memory. 118 | return new DataView(this._inst.exports.mem.buffer); 119 | } 120 | 121 | const setInt64 = (addr, v) => { 122 | mem().setUint32(addr + 0, v, true); 123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); 124 | } 125 | 126 | const getInt64 = (addr) => { 127 | const low = mem().getUint32(addr + 0, true); 128 | const high = mem().getInt32(addr + 4, true); 129 | return low + high * 4294967296; 130 | } 131 | 132 | const loadValue = (addr) => { 133 | const f = mem().getFloat64(addr, true); 134 | if (f === 0) { 135 | return undefined; 136 | } 137 | if (!isNaN(f)) { 138 | return f; 139 | } 140 | 141 | const id = mem().getUint32(addr, true); 142 | return this._values[id]; 143 | } 144 | 145 | const storeValue = (addr, v) => { 146 | const nanHead = 0x7FF80000; 147 | 148 | if (typeof v === "number") { 149 | if (isNaN(v)) { 150 | mem().setUint32(addr + 4, nanHead, true); 151 | mem().setUint32(addr, 0, true); 152 | return; 153 | } 154 | if (v === 0) { 155 | mem().setUint32(addr + 4, nanHead, true); 156 | mem().setUint32(addr, 1, true); 157 | return; 158 | } 159 | mem().setFloat64(addr, v, true); 160 | return; 161 | } 162 | 163 | switch (v) { 164 | case undefined: 165 | mem().setFloat64(addr, 0, true); 166 | return; 167 | case null: 168 | mem().setUint32(addr + 4, nanHead, true); 169 | mem().setUint32(addr, 2, true); 170 | return; 171 | case true: 172 | mem().setUint32(addr + 4, nanHead, true); 173 | mem().setUint32(addr, 3, true); 174 | return; 175 | case false: 176 | mem().setUint32(addr + 4, nanHead, true); 177 | mem().setUint32(addr, 4, true); 178 | return; 179 | } 180 | 181 | let ref = this._refs.get(v); 182 | if (ref === undefined) { 183 | ref = this._values.length; 184 | this._values.push(v); 185 | this._refs.set(v, ref); 186 | } 187 | let typeFlag = 0; 188 | switch (typeof v) { 189 | case "string": 190 | typeFlag = 1; 191 | break; 192 | case "symbol": 193 | typeFlag = 2; 194 | break; 195 | case "function": 196 | typeFlag = 3; 197 | break; 198 | } 199 | mem().setUint32(addr + 4, nanHead | typeFlag, true); 200 | mem().setUint32(addr, ref, true); 201 | } 202 | 203 | const loadSlice = (addr) => { 204 | const array = getInt64(addr + 0); 205 | const len = getInt64(addr + 8); 206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 207 | } 208 | 209 | const loadSliceOfValues = (addr) => { 210 | const array = getInt64(addr + 0); 211 | const len = getInt64(addr + 8); 212 | const a = new Array(len); 213 | for (let i = 0; i < len; i++) { 214 | a[i] = loadValue(array + i * 8); 215 | } 216 | return a; 217 | } 218 | 219 | const loadString = (addr) => { 220 | const saddr = getInt64(addr + 0); 221 | const len = getInt64(addr + 8); 222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 223 | } 224 | 225 | const timeOrigin = Date.now() - performance.now(); 226 | this.importObject = { 227 | go: { 228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 231 | // This changes the SP, thus we have to update the SP used by the imported function. 232 | 233 | // func wasmExit(code int32) 234 | "runtime.wasmExit": (sp) => { 235 | const code = mem().getInt32(sp + 8, true); 236 | this.exited = true; 237 | delete this._inst; 238 | delete this._values; 239 | delete this._refs; 240 | this.exit(code); 241 | }, 242 | 243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 244 | "runtime.wasmWrite": (sp) => { 245 | const fd = getInt64(sp + 8); 246 | const p = getInt64(sp + 16); 247 | const n = mem().getInt32(sp + 24, true); 248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 249 | }, 250 | 251 | // func nanotime() int64 252 | "runtime.nanotime": (sp) => { 253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 254 | }, 255 | 256 | // func walltime() (sec int64, nsec int32) 257 | "runtime.walltime": (sp) => { 258 | const msec = (new Date).getTime(); 259 | setInt64(sp + 8, msec / 1000); 260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); 261 | }, 262 | 263 | // func scheduleTimeoutEvent(delay int64) int32 264 | "runtime.scheduleTimeoutEvent": (sp) => { 265 | const id = this._nextCallbackTimeoutID; 266 | this._nextCallbackTimeoutID++; 267 | this._scheduledTimeouts.set(id, setTimeout( 268 | () => { 269 | this._resume(); 270 | while (this._scheduledTimeouts.has(id)) { 271 | // for some reason Go failed to register the timeout event, log and try again 272 | // (temporary workaround for https://github.com/golang/go/issues/28975) 273 | console.warn("scheduleTimeoutEvent: missed timeout event"); 274 | this._resume(); 275 | } 276 | }, 277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 278 | )); 279 | mem().setInt32(sp + 16, id, true); 280 | }, 281 | 282 | // func clearTimeoutEvent(id int32) 283 | "runtime.clearTimeoutEvent": (sp) => { 284 | const id = mem().getInt32(sp + 8, true); 285 | clearTimeout(this._scheduledTimeouts.get(id)); 286 | this._scheduledTimeouts.delete(id); 287 | }, 288 | 289 | // func getRandomData(r []byte) 290 | "runtime.getRandomData": (sp) => { 291 | crypto.getRandomValues(loadSlice(sp + 8)); 292 | }, 293 | 294 | // func stringVal(value string) ref 295 | "syscall/js.stringVal": (sp) => { 296 | storeValue(sp + 24, loadString(sp + 8)); 297 | }, 298 | 299 | // func valueGet(v ref, p string) ref 300 | "syscall/js.valueGet": (sp) => { 301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 302 | sp = this._inst.exports.getsp(); // see comment above 303 | storeValue(sp + 32, result); 304 | }, 305 | 306 | // func valueSet(v ref, p string, x ref) 307 | "syscall/js.valueSet": (sp) => { 308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 309 | }, 310 | 311 | // func valueIndex(v ref, i int) ref 312 | "syscall/js.valueIndex": (sp) => { 313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 314 | }, 315 | 316 | // valueSetIndex(v ref, i int, x ref) 317 | "syscall/js.valueSetIndex": (sp) => { 318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 319 | }, 320 | 321 | // func valueCall(v ref, m string, args []ref) (ref, bool) 322 | "syscall/js.valueCall": (sp) => { 323 | try { 324 | const v = loadValue(sp + 8); 325 | const m = Reflect.get(v, loadString(sp + 16)); 326 | const args = loadSliceOfValues(sp + 32); 327 | const result = Reflect.apply(m, v, args); 328 | sp = this._inst.exports.getsp(); // see comment above 329 | storeValue(sp + 56, result); 330 | mem().setUint8(sp + 64, 1); 331 | } catch (err) { 332 | storeValue(sp + 56, err); 333 | mem().setUint8(sp + 64, 0); 334 | } 335 | }, 336 | 337 | // func valueInvoke(v ref, args []ref) (ref, bool) 338 | "syscall/js.valueInvoke": (sp) => { 339 | try { 340 | const v = loadValue(sp + 8); 341 | const args = loadSliceOfValues(sp + 16); 342 | const result = Reflect.apply(v, undefined, args); 343 | sp = this._inst.exports.getsp(); // see comment above 344 | storeValue(sp + 40, result); 345 | mem().setUint8(sp + 48, 1); 346 | } catch (err) { 347 | storeValue(sp + 40, err); 348 | mem().setUint8(sp + 48, 0); 349 | } 350 | }, 351 | 352 | // func valueNew(v ref, args []ref) (ref, bool) 353 | "syscall/js.valueNew": (sp) => { 354 | try { 355 | const v = loadValue(sp + 8); 356 | const args = loadSliceOfValues(sp + 16); 357 | const result = Reflect.construct(v, args); 358 | sp = this._inst.exports.getsp(); // see comment above 359 | storeValue(sp + 40, result); 360 | mem().setUint8(sp + 48, 1); 361 | } catch (err) { 362 | storeValue(sp + 40, err); 363 | mem().setUint8(sp + 48, 0); 364 | } 365 | }, 366 | 367 | // func valueLength(v ref) int 368 | "syscall/js.valueLength": (sp) => { 369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 370 | }, 371 | 372 | // valuePrepareString(v ref) (ref, int) 373 | "syscall/js.valuePrepareString": (sp) => { 374 | const str = encoder.encode(String(loadValue(sp + 8))); 375 | storeValue(sp + 16, str); 376 | setInt64(sp + 24, str.length); 377 | }, 378 | 379 | // valueLoadString(v ref, b []byte) 380 | "syscall/js.valueLoadString": (sp) => { 381 | const str = loadValue(sp + 8); 382 | loadSlice(sp + 16).set(str); 383 | }, 384 | 385 | // func valueInstanceOf(v ref, t ref) bool 386 | "syscall/js.valueInstanceOf": (sp) => { 387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); 388 | }, 389 | 390 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 391 | "syscall/js.copyBytesToGo": (sp) => { 392 | const dst = loadSlice(sp + 8); 393 | const src = loadValue(sp + 32); 394 | if (!(src instanceof Uint8Array)) { 395 | mem().setUint8(sp + 48, 0); 396 | return; 397 | } 398 | const toCopy = src.subarray(0, dst.length); 399 | dst.set(toCopy); 400 | setInt64(sp + 40, toCopy.length); 401 | mem().setUint8(sp + 48, 1); 402 | }, 403 | 404 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 405 | "syscall/js.copyBytesToJS": (sp) => { 406 | const dst = loadValue(sp + 8); 407 | const src = loadSlice(sp + 16); 408 | if (!(dst instanceof Uint8Array)) { 409 | mem().setUint8(sp + 48, 0); 410 | return; 411 | } 412 | const toCopy = src.subarray(0, dst.length); 413 | dst.set(toCopy); 414 | setInt64(sp + 40, toCopy.length); 415 | mem().setUint8(sp + 48, 1); 416 | }, 417 | 418 | "debug": (value) => { 419 | console.log(value); 420 | }, 421 | } 422 | }; 423 | } 424 | 425 | async run(instance) { 426 | this._inst = instance; 427 | this._values = [ // TODO: garbage collection 428 | NaN, 429 | 0, 430 | null, 431 | true, 432 | false, 433 | global, 434 | this, 435 | ]; 436 | this._refs = new Map(); 437 | this.exited = false; 438 | 439 | const mem = new DataView(this._inst.exports.mem.buffer) 440 | 441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 442 | let offset = 4096; 443 | 444 | const strPtr = (str) => { 445 | const ptr = offset; 446 | const bytes = encoder.encode(str + "\0"); 447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes); 448 | offset += bytes.length; 449 | if (offset % 8 !== 0) { 450 | offset += 8 - (offset % 8); 451 | } 452 | return ptr; 453 | }; 454 | 455 | const argc = this.argv.length; 456 | 457 | const argvPtrs = []; 458 | this.argv.forEach((arg) => { 459 | argvPtrs.push(strPtr(arg)); 460 | }); 461 | 462 | const keys = Object.keys(this.env).sort(); 463 | argvPtrs.push(keys.length); 464 | keys.forEach((key) => { 465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 466 | }); 467 | 468 | const argv = offset; 469 | argvPtrs.forEach((ptr) => { 470 | mem.setUint32(offset, ptr, true); 471 | mem.setUint32(offset + 4, 0, true); 472 | offset += 8; 473 | }); 474 | 475 | this._inst.exports.run(argc, argv); 476 | if (this.exited) { 477 | this._resolveExitPromise(); 478 | } 479 | await this._exitPromise; 480 | } 481 | 482 | _resume() { 483 | if (this.exited) { 484 | throw new Error("Go program has already exited"); 485 | } 486 | this._inst.exports.resume(); 487 | if (this.exited) { 488 | this._resolveExitPromise(); 489 | } 490 | } 491 | 492 | _makeFuncWrapper(id) { 493 | const go = this; 494 | return function () { 495 | const event = { id: id, this: this, args: arguments }; 496 | go._pendingEvent = event; 497 | go._resume(); 498 | return event.result; 499 | }; 500 | } 501 | } 502 | 503 | if ( 504 | global.require && 505 | global.require.main === module && 506 | global.process && 507 | global.process.versions && 508 | !global.process.versions.electron 509 | ) { 510 | if (process.argv.length < 3) { 511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 512 | process.exit(1); 513 | } 514 | 515 | const go = new Go(); 516 | go.argv = process.argv.slice(2); 517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 518 | go.exit = process.exit; 519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 521 | if (code === 0 && !go.exited) { 522 | // deadlock, make Go print error and stack traces 523 | go._pendingEvent = { id: 0 }; 524 | go._resume(); 525 | } 526 | }); 527 | return go.run(result.instance); 528 | }).catch((err) => { 529 | console.error(err); 530 | process.exit(1); 531 | }); 532 | } 533 | })(); 534 | -------------------------------------------------------------------------------- /img/basics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsoch/regression-wasm/91f1adb3dfe185c92f6aac59916829295a8a5aee/img/basics.png -------------------------------------------------------------------------------- /img/incomes-predict-murders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsoch/regression-wasm/91f1adb3dfe185c92f6aac59916829295a8a5aee/img/incomes-predict-murders.png -------------------------------------------------------------------------------- /img/inhabitants-predict-murders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsoch/regression-wasm/91f1adb3dfe185c92f6aac59916829295a8a5aee/img/inhabitants-predict-murders.png -------------------------------------------------------------------------------- /img/line-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsoch/regression-wasm/91f1adb3dfe185c92f6aac59916829295a8a5aee/img/line-plot.png -------------------------------------------------------------------------------- /img/remove1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsoch/regression-wasm/91f1adb3dfe185c92f6aac59916829295a8a5aee/img/remove1.png -------------------------------------------------------------------------------- /img/unemployment-predict-murders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsoch/regression-wasm/91f1adb3dfe185c92f6aac59916829295a8a5aee/img/unemployment-predict-murders.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vanessa Sochat. All rights reserved. 2 | // Use of this source code is governed by the Polyform Strict license 3 | // that can be found in the LICENSE file and available at 4 | // https://polyformproject.org/licenses/noncommercial/1.0.0 5 | 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "syscall/js" 11 | ) 12 | 13 | func main() { 14 | 15 | c := make(chan struct{}, 0) 16 | js.Global().Set("runRegression", js.FuncOf(runRegression)) 17 | <-c 18 | } 19 | 20 | // runRegression is the entrypoint 21 | func runRegression(this js.Value, val []js.Value) interface{} { 22 | fmt.Println("The input csv string is:", val[0]) 23 | fmt.Println("Header is present:", val[1]) 24 | fmt.Println("Delimiter is:", val[2]) 25 | fmt.Println("Predictor column is:", val[3]) 26 | 27 | runner := RegressionRunner{} 28 | 29 | // Browser index is 1, Golang is 0 30 | runner.predictCol = val[3].Int() - 1 31 | 32 | // read string, true/false for header, and delim 33 | if err := runner.readCsv(val[0].String(), val[1].Bool(), val[2].String()); err != nil { 34 | sendMessage("There was an error reading the csv.", "message") 35 | return nil 36 | } 37 | 38 | // Ensure that we have data, period 39 | if len(runner.records) == 0 { 40 | sendMessage("No records were provided in this dataset.", "message") 41 | return nil 42 | } 43 | 44 | // Need more than one columns 45 | if len(runner.records[0]) == 1 { 46 | sendMessage("You must provide more than one column of data.", "message") 47 | return nil 48 | } 49 | 50 | // Ensure the predictor column is present in the data 51 | if len(runner.records[0]) < runner.predictCol { 52 | message := fmt.Sprintf("The predictor col %s is > number cols, %s", val[3].Int(), len(runner.records[0])) 53 | sendMessage(message, "message") 54 | return nil 55 | } 56 | 57 | fmt.Println("Header is:", runner.header) 58 | fmt.Println("Records are:", runner.records) 59 | 60 | // All goes well, hide previous messages 61 | hideMessage("message") 62 | 63 | // run, calculate residuals, and plot. This could be broken up 64 | // into separate functions for the user to control, if desired, but 65 | // we would need to maintain state of the model somewhere 66 | runner.runRegression() 67 | runner.calculateResiduals() 68 | runner.plotRegression() 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vanessa Sochat. All rights reserved. 2 | // Use of this source code is governed by the Polyform Strict license 3 | // that can be found in the LICENSE file and available at 4 | // https://polyformproject.org/licenses/noncommercial/1.0.0 5 | 6 | package main 7 | 8 | import ( 9 | "encoding/csv" 10 | "fmt" 11 | "strings" 12 | "strconv" 13 | "syscall/js" 14 | 15 | "github.com/sajari/regression" 16 | ) 17 | 18 | // RegressionRunner stores input data for the file 19 | type RegressionRunner struct { 20 | records [][]string // array of records 21 | y []float64 // array of parsed data (y only) 22 | x [][]float64 // array of parsed data (x only) 23 | header []string // first row of headers 24 | predictCol int // prediction column 25 | residuals []float64 // final array of residuals 26 | predictions []float64 // final array of predictions 27 | model *regression.Regression // the regression model 28 | } 29 | 30 | // readCsv file and set the records on the runner 31 | func (runner *RegressionRunner) readCsv(csvString string, hasHeader bool, delim string) error { 32 | 33 | reader := csv.NewReader(strings.NewReader(csvString)) 34 | records, err := reader.ReadAll() 35 | var header []string 36 | 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // Remove header row, if we have it 42 | if hasHeader { 43 | header, records = records[0], records[1:] 44 | runner.header = header 45 | } else { 46 | // Add dummy names 47 | for i := range records[0] { 48 | header = append(header, fmt.Sprintf("Element %d", i)) 49 | } 50 | } 51 | 52 | runner.records = records 53 | return nil 54 | } 55 | 56 | // runRegression and generate model to save to runner 57 | func (runner *RegressionRunner) runRegression() { 58 | 59 | r := new(regression.Regression) 60 | 61 | // Iterate through headers to generate variables 62 | count := 0 63 | 64 | for index, element := range runner.header { 65 | 66 | // Add as regression or observed variable 67 | if (index != runner.predictCol) { 68 | r.SetVar(count, element) 69 | count++ 70 | } else { 71 | r.SetObserved(element) 72 | } 73 | } 74 | 75 | // We will unwrap an array of data points 76 | var dataPoints regression.DataPoints 77 | var predictor float64 78 | var regressors []float64 79 | 80 | // Iterate through records to generate dataPoints 81 | for _, row := range runner.records { 82 | 83 | regressors = nil 84 | for index, record := range row { 85 | 86 | if (index != runner.predictCol) { 87 | if n, err := strconv.ParseFloat(record, 64); err == nil { 88 | regressors = append(regressors, n) 89 | } 90 | } else { 91 | if n, err := strconv.ParseFloat(record, 64); err == nil { 92 | predictor = n 93 | } 94 | } 95 | } 96 | 97 | dataPoints = append(dataPoints, regression.DataPoint(predictor, regressors)) 98 | runner.x = append(runner.x, regressors) 99 | runner.y = append(runner.y, predictor) 100 | } 101 | 102 | fmt.Printf("X variables:\n%v\n", runner.x) 103 | fmt.Printf("Y variables:\n%v\n", runner.y) 104 | 105 | // Unwrap data points into function 106 | r.Train(dataPoints...) 107 | r.Run() 108 | 109 | // Show and save the regression model 110 | fmt.Printf("Regression formula:\n%v\n", r.Formula) 111 | //fmt.Printf("Regression:\n%s\n", r) 112 | runner.model = r 113 | } 114 | 115 | 116 | // Calculate residuals using the model. If we have more than two covariates, 117 | // then we will plot residuals in a histogram. 118 | func (runner *RegressionRunner) calculateResiduals() { 119 | 120 | // Calculate residuals and predictions 121 | var predictions []float64 122 | var residuals []float64 123 | 124 | for i, row := range runner.x { 125 | if prediction, err := runner.model.Predict(row); err == nil { 126 | predictions = append(predictions, prediction) 127 | residuals = append(residuals, runner.y[i] - prediction) 128 | } 129 | } 130 | 131 | fmt.Println("Residuals:", residuals) 132 | fmt.Println("Predictions:", predictions) 133 | 134 | // Save to the runner! 135 | runner.residuals = residuals 136 | runner.predictions = predictions 137 | } 138 | 139 | // plotResult will generate a histogram for multivariate regression (of 140 | // residuals) or a line plot given only two variables. 141 | func (runner *RegressionRunner) plotRegression() { 142 | 143 | // Greater than two variables -> regression line 144 | if len(runner.header) != 2 { 145 | runner.plotResiduals() 146 | } else { 147 | runner.plotLinear() 148 | } 149 | } 150 | 151 | // plotLinear is called given 2 variables, and we create a line plot 152 | func (runner *RegressionRunner) plotLinear() { 153 | 154 | plotFunc := js.Global().Get("plotLinear") 155 | describeFunc := js.Global().Get("describePlot") 156 | 157 | // Convert data to string to send back to browser 158 | X := floatArrayToString(runner.x) // only uses first entry 159 | Y := floatToString(runner.y) 160 | predictions := floatToString(runner.predictions) 161 | headers := strings.Join(runner.header, ",") 162 | 163 | result := fmt.Sprintf("Dinosaur Regression Wasm\n%s\n%s", runner.model.Formula, runner.model) 164 | 165 | // provide the title, X, Y, headers, and result 166 | plotFunc.Invoke(runner.header[runner.predictCol], X, Y, predictions, headers, result) 167 | describeFunc.Invoke(runner.model.Formula) 168 | } 169 | 170 | // plotResiduals calls plotResiduals on the front end and passes residuals 171 | func (runner *RegressionRunner) plotResiduals() { 172 | 173 | // Get the functions to do and describe the plot 174 | plotFunc := js.Global().Get("plotResiduals") 175 | describeFunc := js.Global().Get("describePlot") 176 | 177 | // Comma separated list of residuals 178 | resultString := floatToString(runner.residuals) 179 | 180 | 181 | // provide the title (predictor) and string data 182 | result := fmt.Sprintf("Dinosaur Regression Wasm\n%s\n%s", runner.model.Formula, runner.model) 183 | plotFunc.Invoke(runner.header[runner.predictCol], resultString, result) 184 | describeFunc.Invoke(runner.model.Formula) 185 | } 186 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vanessa Sochat. All rights reserved. 2 | // Use of this source code is governed by the Polyform Strict license 3 | // that can be found in the LICENSE file and available at 4 | // https://polyformproject.org/licenses/noncommercial/1.0.0 5 | 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "strings" 11 | "syscall/js" 12 | ) 13 | 14 | // convert an array of floats to a string to send back to browser 15 | func floatToString(floats []float64) string { 16 | var values []string 17 | var stringValues string 18 | for i := range floats { 19 | text := fmt.Sprintf("%f", floats[i]) 20 | values = append(values, text) 21 | } 22 | stringValues = strings.Join(values, ",") 23 | return stringValues 24 | } 25 | 26 | // floatArrayToString converts an array of floats to a single string 27 | // we expect there only to be one value per array entry 28 | func floatArrayToString(floats [][]float64) string { 29 | var values []string 30 | var stringValues string 31 | for i := range floats { 32 | text := fmt.Sprintf("%f", floats[i][0]) 33 | values = append(values, text) 34 | } 35 | stringValues = strings.Join(values, ",") 36 | return stringValues 37 | } 38 | 39 | 40 | // returnResult back to the browser, in the innerHTML of the result element 41 | func returnResult(output string, divid string) { 42 | js.Global().Get("document"). 43 | Call("getElementById", divid). 44 | Set("innerHTML", output) 45 | } 46 | 47 | func sendMessage (message string, div_id string) { 48 | messageFunc := js.Global().Get("showMessage") 49 | messageFunc.Invoke(message, div_id) 50 | } 51 | 52 | func hideMessage (div_id string) { 53 | messageFunc := js.Global().Get("hideMessage") 54 | messageFunc.Invoke(div_id) 55 | } 56 | --------------------------------------------------------------------------------